./PaxHeaders/prosody-13.0.10000644000000000000000000000013014773555365012453 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.503710184 29 ctime=1743706869.95171197 prosody-13.0.1/0000755000175000017500000000000014773555365014577 5ustar00prosodyprosody00000000000000prosody-13.0.1/PaxHeaders/.busted0000644000000000000000000000011714773555365013667 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.507710199 prosody-13.0.1/.busted0000644000175000017500000000025614773555365016071 0ustar00prosodyprosody00000000000000return { _all = { }, default = { ["exclude-tags"] = "mod_bosh,storage,SLOW"; }; bosh = { tags = "mod_bosh"; }; storage = { tags = "storage"; }; } prosody-13.0.1/PaxHeaders/.editorconfig0000644000000000000000000000011714773555365015055 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.507710199 prosody-13.0.1/.editorconfig0000644000175000017500000000057214773555365017260 0ustar00prosodyprosody00000000000000# 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 indent_style = space [configure] indent_size = 3 indent_style = space [*.xml] # xmllint --nsclean --encode UTF-8 --noent --format - indent_size = 2 indent_style = space prosody-13.0.1/PaxHeaders/.hg_archival.txt0000644000000000000000000000011714773555365015466 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.503710184 prosody-13.0.1/.hg_archival.txt0000644000175000017500000000016714773555365017671 0ustar00prosodyprosody00000000000000repo: 3e3171b59028ee70122cfec6ecf98f518f946b59 node: e78e79f1b5f5d29cdbda7ce269ebc87fc126193e branch: 13.0 tag: 13.0.1 prosody-13.0.1/PaxHeaders/.lua-format0000644000000000000000000000011714773555365014450 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.507710199 prosody-13.0.1/.lua-format0000644000175000017500000000160214773555365016646 0ustar00prosodyprosody00000000000000align_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 prosody-13.0.1/PaxHeaders/.luacheckrc0000644000000000000000000000011714773555365014505 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.507710199 prosody-13.0.1/.luacheckrc0000644000175000017500000000776314773555365016721 0ustar00prosodyprosody00000000000000cache = true codes = true ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV", "431/log", "214", "581" } std = "lua54c" max_line_length = 150 read_globals = { "prosody", "import", }; files["prosody"] = { allow_defined_top = true; module = true; globals = { "prosody"; } } files["prosodyctl"] = { allow_defined_top = true; module = true; }; files["core/"] = { globals = { "prosody.hosts.?", }; } files["util/"] = { -- Ignore unwrapped license text max_comment_line_length = false; } files["util/jsonschema.lua"] = { ignore = { "211" }; } files["util/datamapper.lua"] = { ignore = { "211" }; } files["plugins/"] = { module = true; allow_defined_top = true; read_globals = { -- Module instance "module.name", "module.host", "module._log", "module.event_handlers", "module.reloading", "module.saved_state", "module.global", "module.path", "module.items", -- Module API "module.add_extension", "module.add_feature", "module.add_identity", "module.add_item", "module.add_timer", "module.weekly", "module.daily", "module.hourly", "module.broadcast", "module.context", "module.could", "module.default_permission", "module.default_permissions", "module.depends", "module.fire_event", "module.get_directory", "module.get_host", "module.get_host_items", "module.get_host_type", "module.get_name", "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", "module.get_status", "module.handle_items", "module.hook", "module.hook_global", "module.hook_object_event", "module.hook_tag", "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", "module.require", "module.send", "module.send_iq", "module.set_global", "module.set_status", "module.shared", "module.unhook", "module.unhook_object_event", "module.wrap_event", "module.wrap_global", "module.wrap_object_event", -- mod_http API "module.http_url", }; globals = { -- Methods that can be set on module API "module.ready", "module.unload", "module.add_host", "module.load", "module.add_host", "module.save", "module.restore", "module.command", "module.environment", }; } files["spec/"] = { std = "+busted"; globals = { "randomize" }; } files["spec/tls"] = { -- luacheck complains about the config files here, -- but we don't really care about them only = {}; } files["prosody.cfg.lua"] = { ignore = { "131" }; globals = { "Host", "host", "VirtualHost", "Component", "component", "Include", "include", "FileContents", "FileLine", "FileLines", "Credential", "RunScript" }; } if os.getenv("PROSODY_STRICT_LINT") ~= "1" then -- These files have not yet been brought up to standard -- Do not add more files here, but do help us fix these! local exclude_files = { "doc/net.server.lua"; "fallbacks/bit.lua"; "fallbacks/lxp.lua"; "net/dns.lua"; "net/server_select.lua"; "spec/core_moduleapi_spec.lua"; "spec/util_http_spec.lua"; "spec/util_ip_spec.lua"; "spec/util_multitable_spec.lua"; "spec/util_throttle_spec.lua"; "tools/ejabberd2prosody.lua"; "tools/ejabberdsql2prosody.lua"; "tools/erlparse.lua"; "tools/jabberd14sql2prosody.lua"; "tools/migration/migrator.cfg.lua"; "tools/migration/migrator/jabberd14.lua"; "tools/migration/migrator/mtools.lua"; "tools/migration/migrator/prosody_files.lua"; "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 files[file] = { only = {} } end end prosody-13.0.1/PaxHeaders/.luacov0000644000000000000000000000011714773555365013672 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.507710199 prosody-13.0.1/.luacov0000644000175000017500000000003414773555365016066 0ustar00prosodyprosody00000000000000exclude = { "^%./spec/"; } prosody-13.0.1/PaxHeaders/.semgrep.yml0000644000000000000000000000011714773555365014643 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.511710215 prosody-13.0.1/.semgrep.yml0000644000175000017500000000232214773555365017041 0ustar00prosodyprosody00000000000000rules: - id: log-variable-fmtstring patterns: - pattern: log("...", $A) - pattern-not: log("...", "...") message: Variable passed as format string to logging languages: [lua] severity: ERROR - id: module-log-variable-fmtstring patterns: - pattern: module:log("...", $A) - pattern-not: module:log("...", "...") message: Variable passed as format string to logging languages: [lua] severity: ERROR - id: module-getopt-string-default patterns: - pattern: module:get_option_string("...", $A) - pattern-not: module:get_option_string("...", "...") - pattern-not: module:get_option_string("...", host) - pattern-not: module:get_option_string("...", module.host) 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] - id: require-unprefixed-module patterns: - pattern: require("$X") - metavariable-regex: metavariable: $X regex: '^(core|net|util)\.' message: Prefix required module path with 'prosody.' severity: ERROR languages: [lua] prosody-13.0.1/PaxHeaders/AUTHORS0000644000000000000000000000011714773555365013450 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.511710215 prosody-13.0.1/AUTHORS0000644000175000017500000000056714773555365015657 0ustar00prosodyprosody00000000000000 The Prosody project is open to contributions (see HACKERS file), but is maintained daily by: - Matthew Wild (mail: matthew [at] prosody.im) - Waqas Hussain (mail: waqas [at] prosody.im) - Kim Alvefur (mail: zash [at] prosody.im) You can reach us collectively by email: developers [at] prosody.im or in realtime in the Prosody chatroom: prosody@conference.prosody.im prosody-13.0.1/PaxHeaders/CHANGES0000644000000000000000000000011714773555365013373 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.511710215 prosody-13.0.1/CHANGES0000644000175000017500000002064414773555365015600 0ustar00prosodyprosody00000000000000TRUNK ===== 13.0.0 ====== **2025-03-13** ## New ## Modules - mod_account_activity - mod_cloud_notify - mod_flags - mod_http_altconnect - mod_s2s_auth_dane_in - mod_server_info ### 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 - New mod_account_activity plugin records last login/logout time of a user account - New 'prosodyctl check features' recommends configuration improvements ### 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 - Owners can set MUC avatars (functionality previously in community module mod_vcard_muc) ### 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 - No longer check certificate Common Names per RFC 9525 ### Storage - New 'keyval+' combined keyval/map store type - Performance improvements in internal archive stores - Ability to use SQLite3 storage using LuaSQLite3 instead of LuaDBI - SQLCipher support ### 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 - module:depends() now accepts a second parameter 'soft' to enable soft dependencies ### 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 - Convenience functions for reading values from files, with variant meant for credentials or secrets ## 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 - mod_cloud_notify imported from community modules for push notification support - mod_http_altconnect imported from community modules, simplifying web clients ## Removed - Lua 5.1 support - XEP-0090 support removed from mod_time - util.rfc6724 0.12.0 ====== **2022-03-14** ## New ### Modules - mod_mimicking: Prevent address spoofing - mod_s2s_bidi: Bi-directional server-to-server (XEP-0288) - mod_external_services: generic XEP-0215 support - mod_turn_external: easy setup XEP-0215 for STUN+TURN - mod_http_file_share: File sharing via HTTP (XEP-0363) - mod_http_openmetrics for exposing metrics to stats collectors - mod_smacks: Stream management and resumption (XEP-0198) - mod_auth_ldap: LDAP authentication - mod_cron: One module to rule all the periodic tasks - mod_admin_shell: New home of the Console admin interface - mod_admin_socket: Enable secure connections to the Console - mod_tombstones: Prevent registration of deleted accounts - mod_invites: Create and manage invites - mod_invites_register: Create accounts using invites - mod_invites_adhoc: Create invites via AdHoc command - mod_bookmarks: Synchronise open rooms between clients ### Security and authentication - SNI support (including automatic certificate selection) - ALPN support in mod_net_multiplex - DANE support in low-level network layer - Direct TLS support (c2s and s2s) - SCRAM-SHA-256 - Direct TLS (including https) certificates updated on reload - Pluggable authorization providers (mod_authz_) - Easy use of Mozilla TLS recommendations presets - Unencrypted HTTP port (5280) restricted to loopback by default - require_encryption options default to 'true' if unspecified - Authentication module defaults to 'internal_hashed' if unspecified ### HTTP - CORS handling now provided by mod_http - Built-in HTTP server now handles HEAD requests - Uploads can be handled incrementally ### API - Module statuses (API change) - util.error for encapsulating errors - Promise based API for sending queries - API for adding periodic tasks - More APIs supporting ES6 Promises - Async can be used during shutdown ### Other - Plugin installer - MUC presence broadcast controls - MUC: support for XEP-0421 occupant identifiers - `prosodyctl check connectivity` via observe.jabber.network - STUN/TURN server tests in `prosodyctl check` - libunbound for DNS queries - The POSIX poll() API used by server_epoll on \*nix other than Linux ## Changes - Improved rules for mobile optimizations - Improved rules for what messages should be archived - mod_limits: Exempted JIDs - mod_server_contact_info now loaded on components if enabled - Statistics now based on OpenMetrics - Statistics scheduling can be done by plugin - Offline messages aren't sent to MAM clients - Archive quotas (means?) - Rewritten migrator with archive support - Improved automatic certificate locating and selecting - Logging to syslog no longer missing startup messages - Graceful shutdown sequence that closes ports first and waits for connections to close ## Removed - `daemonize` option deprecated - SASL DIGEST-MD5 removed - mod_auth_cyrus (older LDAP support) - Network backend server_select deprecated (not actually removed yet) 0.11.0 ====== **2018-11-18** New features ------------ - Rewritten more extensible MUC module - Store inactive rooms to disk - Store rooms to disk on shutdown - Voice requests - Tombstones in place of destroyed rooms - PubSub features - Persistence - Affiliations - Access models - "publish-options" - PEP now uses our pubsub code and now shares the above features - Asynchronous operations - Busted for tests - mod\_muc\_mam (XEP-0313 in groupchats) - mod\_vcard\_legacy (XEP-0398) - mod\_vcard4 (XEP-0292) - mod\_csi, mod\_csi\_simple (XEP-0352) - New experimental network backend "epoll" 0.10.0 ====== **2017-10-02** New features ------------ - Rewritten SQL storage module with Archive support - SCRAM-SHA-1-PLUS - `prosodyctl check` - Statistics - Improved TLS configuration - Lua 5.2 support - mod\_blocklist (XEP-0191) - mod\_carbons (XEP-0280) - Pluggable connection timeout handling - mod\_websocket (RFC 7395) - mod\_mam (XEP-0313) Removed ------- - mod\_privacy (XEP-0016) - mod\_compression (XEP-0138) prosody-13.0.1/PaxHeaders/CONTRIBUTING0000644000000000000000000000011714773555365014232 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.511710215 prosody-13.0.1/CONTRIBUTING0000644000175000017500000000053314773555365016432 0ustar00prosodyprosody00000000000000Thanks for your interest in contributing to the project! There are many ways to contribute, such as helping improve the documentation, reporting bugs, spreading the word or testing the latest development version. You can find more information on how to contribute at See also the HACKERS and README files. prosody-13.0.1/PaxHeaders/COPYING0000644000000000000000000000011714773555365013433 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.511710215 prosody-13.0.1/COPYING0000644000175000017500000000336714773555365015643 0ustar00prosodyprosody00000000000000All source code in this project is released under the below MIT license. Some components are not authored by the Prosody maintainers, but such code is itself either released under a MIT license or declared public domain. --- Copyright (C) 2008-2022 Matthew Wild Copyright (C) 2008-2020 Waqas Hussain Copyright (C) 2010-2022 Kim Alvefur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- util-src/encodings.c: Parts included from Lua 5.3. Copyright (C) 1994-2015 Lua.org, PUC-Rio. util-src/signal.c: Copyright (C) 2007 Patrick J. Donnelly (batrick@batbytes.com) See full copyright notice in the source file. util-src/struct.c: Copyright (C) 2010-2018 Lua.org, PUC-Rio. All rights reserved. See full copyright notice in the source file. net/dns.lua: public domain 20080404 lua@ztact.com prosody-13.0.1/PaxHeaders/DEPENDS0000644000000000000000000000011714773555365013405 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.511710215 prosody-13.0.1/DEPENDS0000644000175000017500000000037714773555365015613 0ustar00prosodyprosody00000000000000 For full information on our dependencies, version requirements, and where to find them, see https://prosody.im/doc/depends If you have luarocks available on your platform, install the following: - luaexpat - luasocket - luafilesystem - luasec prosody-13.0.1/PaxHeaders/GNUmakefile0000644000000000000000000000011714773555365014452 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.515710232 prosody-13.0.1/GNUmakefile0000644000175000017500000001174614773555365016662 0ustar00prosodyprosody00000000000000 include config.unix BIN = $(DESTDIR)$(PREFIX)/bin CONFIG = $(DESTDIR)$(SYSCONFDIR) MODULES = $(DESTDIR)$(LIBDIR)/prosody/modules SOURCE = $(DESTDIR)$(LIBDIR)/prosody DATA = $(DESTDIR)$(DATADIR) MAN = $(DESTDIR)$(PREFIX)/share/man INSTALLEDSOURCE = $(LIBDIR)/prosody INSTALLEDCONFIG = $(SYSCONFDIR) INSTALLEDMODULES = $(LIBDIR)/prosody/modules INSTALLEDDATA = $(DATADIR) INSTALL=install -p INSTALL_DATA=$(INSTALL) -m644 INSTALL_EXEC=$(INSTALL) -m755 MKDIR=install -d MKDIR_PRIVATE=$(MKDIR) -m750 LUACHECK=luacheck BUSTED=busted SCANSION=scansion .PHONY: all test coverage clean install all: prosody.install prosodyctl.install prosody.cfg.lua.install prosody.version $(MAKE) -C util-src install ifeq ($(EXCERTS),yes) -$(MAKE) -C certs localhost.crt example.com.crt endif install-etc: prosody.cfg.lua.install $(MKDIR) $(CONFIG) $(MKDIR) $(CONFIG)/certs $(INSTALL_DATA) certs/* $(CONFIG)/certs test -f $(CONFIG)/prosody.cfg.lua || $(INSTALL_DATA) prosody.cfg.lua.install $(CONFIG)/prosody.cfg.lua install-bin: prosody.install prosodyctl.install $(MKDIR) $(BIN) $(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 $(INSTALL_DATA) core/*.lua $(SOURCE)/core install-net: $(MKDIR) $(SOURCE) $(MKDIR) $(SOURCE)/net $(INSTALL_DATA) net/*.lua $(SOURCE)/net $(MKDIR) $(SOURCE)/net/http $(SOURCE)/net/resolvers $(SOURCE)/net/websocket $(INSTALL_DATA) net/http/*.lua $(SOURCE)/net/http $(INSTALL_DATA) net/resolvers/*.lua $(SOURCE)/net/resolvers $(INSTALL_DATA) net/websocket/*.lua $(SOURCE)/net/websocket install-util: util/encodings.so util/encodings.so util/pposix.so util/signal.so util/struct.so $(MKDIR) $(SOURCE) $(MKDIR) $(SOURCE)/util $(INSTALL_DATA) util/*.lua $(SOURCE)/util $(MAKE) install -C util-src $(INSTALL_DATA) util/*.so $(SOURCE)/util $(MKDIR) $(SOURCE)/util/sasl $(INSTALL_DATA) util/sasl/*.lua $(SOURCE)/util/sasl $(MKDIR) $(SOURCE)/util/human $(INSTALL_DATA) util/human/*.lua $(SOURCE)/util/human $(MKDIR) $(SOURCE)/util/prosodyctl $(INSTALL_DATA) util/prosodyctl/*.lua $(SOURCE)/util/prosodyctl install-plugins: $(MKDIR) $(MODULES) $(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 $(INSTALL_DATA) man/prosodyctl.man $(MAN)/man1/prosodyctl.1 install-meta: -test -f prosody.version && $(INSTALL_DATA) prosody.version $(SOURCE)/prosody.version 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-loader clean: rm -f prosody.install rm -f prosodyctl.install rm -f prosody.cfg.lua.install rm -f prosody.version $(MAKE) clean -C util-src test: $(BUSTED) --helper loader --lua=$(RUNWITH) test-%: $(BUSTED) --helper loader --lua=$(RUNWITH) -r $* integration-test: all $(MKDIR) data $(RUNWITH) prosodyctl --config ./spec/scansion/prosody.cfg.lua start $(SCANSION) -d ./spec/scansion; R=$$? \ $(RUNWITH) prosodyctl --config ./spec/scansion/prosody.cfg.lua stop \ exit $$R integration-test-%: all $(MKDIR) data $(RUNWITH) prosodyctl --config ./spec/scansion/prosody.cfg.lua start $(SCANSION) ./spec/scansion/$*.scs; R=$$? \ $(RUNWITH) prosodyctl --config ./spec/scansion/prosody.cfg.lua stop \ exit $$R integration-test-tls: all cd ./spec/tls && ./run.sh coverage: -rm -- luacov.* $(BUSTED) --lua=$(RUNWITH) -c luacov luacov-console luacov-console -s @echo "To inspect individual files run: luacov-console -l FILENAME" lint: $(LUACHECK) -q $$(HGPLAIN= hg files -I '**.lua') prosody prosodyctl @echo $$(sed -n '/^\tlocal exclude_files/,/^}/p;' .luacheckrc | sed '1d;$d' | wc -l) files ignored shellcheck configure vpath %.tl teal-src/prosody %.lua: %.tl tl -I teal-src/ --gen-compat off --gen-target 5.1 gen $^ -o $@ -lua-format -i $@ teal: util/jsonschema.lua util/datamapper.lua util/jsonpointer.lua util/%.so: $(MAKE) install -C util-src %.install: % sed "1s| lua$$| $(RUNWITH)|; \ s|^CFG_SOURCEDIR=.*;$$|CFG_SOURCEDIR='$(INSTALLEDSOURCE)';|; \ s|^CFG_CONFIGDIR=.*;$$|CFG_CONFIGDIR='$(INSTALLEDCONFIG)';|; \ s|^CFG_DATADIR=.*;$$|CFG_DATADIR='$(INSTALLEDDATA)';|; \ s|^CFG_PLUGINDIR=.*;$$|CFG_PLUGINDIR='$(INSTALLEDMODULES)/';|;" < $^ > $@ prosody.cfg.lua.install: prosody.cfg.lua.dist sed 's|certs/|$(INSTALLEDCONFIG)/certs/|' $^ > $@ %.version: %.release cp $^ $@ %.version: .hg_archival.txt sed -n 's/^node: \(............\).*/\1/p' $^ > $@ %.version: .hg/dirstate hexdump -n6 -e'6/1 "%02x"' $^ > $@ %.version: echo unknown > $@ prosody-13.0.1/PaxHeaders/HACKERS0000644000000000000000000000011714773555365013403 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.515710232 prosody-13.0.1/HACKERS0000644000175000017500000000072714773555365015610 0ustar00prosodyprosody00000000000000Welcome hackers! This project accepts and *encourages* contributions. If you would like to get involved you can join us on our mailing list and discussion rooms. More information on these at https://prosody.im/discuss Patches are welcome, though before sending we would appreciate if you read docs/coding_style.md for guidelines on how to format your code, and other tips. Documentation for developers can be found at https://prosody.im/doc/developers Have fun :) prosody-13.0.1/PaxHeaders/INSTALL0000644000000000000000000000011714773555365013431 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.515710232 prosody-13.0.1/INSTALL0000644000175000017500000000440714773555365015635 0ustar00prosodyprosody00000000000000(This file was created from https://prosody.im/doc/installing_from_source on 2013-03-31) # Installing from source ## Dependencies There are a couple of development packages which Prosody needs installed before you can build it. These are: - The [Lua](http://lua.org/) library, version 5.4 recommended - [OpenSSL](http://openssl.org/) - String processing library, one of - [ICU](https://icu.unicode.org/) (recommended) - [GNU libidn](http://www.gnu.org/software/libidn/) These can be installed on Debian/Ubuntu by running `apt build-dep prosody` or by installing the packages `liblua5.4-dev`, `libicu-dev` and `libssl-dev`. On Mandriva try: urpmi lua liblua-devel libidn-devel libopenssl-devel On Mac OS X, if you have MacPorts installed, you can try: sudo port install lua lua-luasocket lua-luasec lua-luaexpat On other systems... good luck, but please let us know of the best way of getting the dependencies for your system and we can add it here. ## configure The first step of building is to run the configure script. This creates a file called 'config.unix' which is used by the next step to control aspects of the build process. ./configure All options to configure can be seen by running ./configure --help ## make Once you have run configure successfully, then you can simply run: make Simple? :-) If you do happen to have problems at this stage, it is most likely due to the build process not finding the dependencies. Ensure you have them installed, and in the standard library paths for your system. For more help, just ask ;-) ==== install ==== At this stage you should be able to run Prosody simply with: ./prosody There is no problem with this, it is actually the easiest way to do development, as it doesn't spread parts around your system, and you can keep multiple versions around in their own directories without conflict. Should you wish to install it system-wide however, simply run: sudo make install ...it will install into /usr/local/ by default. To change this you can pass to the initial ./configure using the 'prefix' option, or edit config.unix directly. If the new path doesn't require root permission to write to, you also won't need (or want) to use 'sudo' in front of the 'make install'. Have fun, and see you on Jabber! prosody-13.0.1/PaxHeaders/README0000644000000000000000000000011714773555365013260 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.515710232 prosody-13.0.1/README0000644000175000017500000000175214773555365015464 0ustar00prosodyprosody00000000000000# Prosody IM Server ## Description Prosody is a server for Jabber/XMPP written in Lua. It aims to be easy to use and light on resources. For developers, it aims to give a flexible system on which to rapidly develop added functionality or rapidly prototype new protocols. ## Useful links Homepage: https://prosody.im/ Download: https://prosody.im/download Documentation: https://prosody.im/doc/ Issue tracker: https://issues.prosody.im/ Jabber/XMPP Chat: Address: prosody@conference.prosody.im Web interface: https://chat.prosody.im/ Mailing lists: User support and discussion: https://groups.google.com/group/prosody-users Development discussion: https://groups.google.com/group/prosody-dev ## Installation See the accompanying INSTALL file for help on building Prosody from source. Alternatively see our guide at https://prosody.im/doc/install prosody-13.0.1/PaxHeaders/TODO0000644000000000000000000000011714773555365013070 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.515710232 prosody-13.0.1/TODO0000644000175000017500000000007514773555365015271 0ustar00prosodyprosody00000000000000== 1.0 == - Roster providers - Clustering - World domination prosody-13.0.1/PaxHeaders/certs0000644000000000000000000000013114773555365013437 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.515710232 30 ctime=1743706869.519710248 prosody-13.0.1/certs/0000755000175000017500000000000014773555365015717 5ustar00prosodyprosody00000000000000prosody-13.0.1/certs/PaxHeaders/GNUmakefile0000644000000000000000000000011714773555365015572 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.515710232 prosody-13.0.1/certs/GNUmakefile0000644000175000017500000000354714773555365020002 0ustar00prosodyprosody00000000000000.DEFAULT: localhost.crt keysize=2048 # How to: # First, `make yourhost.cnf` which creates a openssl config file. # Then edit this file and fill in the details you want it to have, # and add or change hosts and components it should cover. # Then `make yourhost.key` to create your private key, you can # include keysize=number to change the size of the key. # Then you can either `make yourhost.csr` to generate a certificate # signing request that you can submit to a CA, or `make yourhost.crt` # to generate a self signed certificate. .PRECIOUS: %.cnf %.key # To request a cert %.csr: %.cnf %.key openssl req -new -key $(lastword $^) \ -sha256 -utf8 -config $(firstword $^) -out $@ %.csr: %.cnf umask 0077 && touch $*.key openssl req -new -newkey rsa:$(keysize) -nodes -keyout $*.key \ -sha256 -utf8 -config $^ -out $@ @chmod 400 $*.key %.csr: %.key openssl req -new -key $^ -utf8 -subj /CN=$* -out $@ %.csr: umask 0077 && touch $*.key openssl req -new -newkey rsa:$(keysize) -nodes -keyout $*.key \ -utf8 -subj /CN=$* -out $@ @chmod 400 $*.key # Self signed %.crt: %.cnf %.key openssl req -new -x509 -key $(lastword $^) -days 365 -sha256 -utf8 \ -config $(firstword $^) -out $@ %.crt: %.cnf umask 0077 && touch $*.key openssl req -new -x509 -newkey rsa:$(keysize) -nodes -keyout $*.key \ -days 365 -sha256 -utf8 -config $(firstword $^) -out $@ @chmod 400 $*.key %.crt: %.key openssl req -new -x509 -key $^ -days 365 -sha256 -utf8 -subj /CN=$* -out $@ %.crt: umask 0077 && touch $*.key openssl req -new -x509 -newkey rsa:$(keysize) -nodes -keyout $*.key \ -days 365 -sha256 -out $@ -utf8 -subj /CN=$* @chmod 400 $*.key # Generate a config from the example %.cnf: sed 's,example\.com,$*,g' openssl.cnf > $@ %.key: umask 0077 && openssl genrsa -out $@ $(keysize) @chmod 400 $@ # Generate Diffie-Hellman parameters dh-%.pem: openssl dhparam -out $@ $* prosody-13.0.1/certs/PaxHeaders/localhost.cnf0000644000000000000000000000011714773555365016200 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.519710248 prosody-13.0.1/certs/localhost.cnf0000644000175000017500000000113714773555365020401 0ustar00prosodyprosody00000000000000[v3_extensions] basicConstraints = CA:TRUE subjectAltName = @subject_alternative_name [subject_alternative_name] DNS.0 = localhost otherName.0 = 1.3.6.1.5.5.7.8.7;IA5STRING:_xmpp-client.localhost otherName.1 = 1.3.6.1.5.5.7.8.7;IA5STRING:_xmpp-server.localhost otherName.2 = 1.3.6.1.5.5.7.8.5;FORMAT:UTF8,UTF8:localhost [distinguished_name] countryName = GB organizationName = Prosody IM organizationalUnitName = https://prosody.im/doc/certificates commonName = Example certificate [req] prompt = no x509_extensions = v3_extensions req_extensions = v3_extensions distinguished_name = distinguished_name prosody-13.0.1/certs/PaxHeaders/makefile0000644000000000000000000000011714773555365015220 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.519710248 prosody-13.0.1/certs/makefile0000644000175000017500000000131714773555365017421 0ustar00prosodyprosody00000000000000.DEFAULT: localhost.crt keysize=2048 # How to: # First, `make yourhost.cnf` which creates a openssl config file. # Then edit this file and fill in the details you want it to have, # and add or change hosts and components it should cover. # Then `make yourhost.key` to create your private key, you can # include keysize=number to change the size of the key. # Then you can either `make yourhost.csr` to generate a certificate # signing request that you can submit to a CA, or `make yourhost.crt` # to generate a self signed certificate. ${.TARGETS:M*.crt}: openssl req -new -x509 -newkey rsa:$(keysize) -nodes -keyout ${.TARGET:R}.key \ -days 365 -sha256 -out $@ -utf8 -subj /CN=${.TARGET:R} .SUFFIXES: .key .crt prosody-13.0.1/certs/PaxHeaders/openssl.cnf0000644000000000000000000000011714773555365015673 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.519710248 prosody-13.0.1/certs/openssl.cnf0000644000175000017500000000307114773555365020073 0ustar00prosodyprosody00000000000000oid_section = new_oids [ new_oids ] # RFC 6120 section 13.7.1.4. defines this OID xmppAddr = 1.3.6.1.5.5.7.8.5 # RFC 4985 defines this OID SRVName = 1.3.6.1.5.5.7.8.7 [ req ] default_bits = 4096 default_keyfile = example.com.key distinguished_name = distinguished_name req_extensions = certrequest x509_extensions = selfsigned # ask about the DN? prompt = no [ distinguished_name ] commonName = example.com countryName = GB localityName = The Internet organizationName = Your Organisation organizationalUnitName = XMPP Department emailAddress = xmpp@example.com [ certrequest ] # for certificate requests (req_extensions) basicConstraints = CA:FALSE keyUsage = digitalSignature,keyEncipherment extendedKeyUsage = serverAuth,clientAuth subjectAltName = @subject_alternative_name [ selfsigned ] # and self-signed certificates (x509_extensions) basicConstraints = CA:TRUE subjectAltName = @subject_alternative_name [ subject_alternative_name ] # 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 otherName.1 = SRVName;IA5STRING:_xmpp-client.example.com otherName.2 = SRVName;IA5STRING:_xmpp-server.example.com DNS.1 = conference.example.com otherName.3 = xmppAddr;FORMAT:UTF8,UTF8:conference.example.com otherName.4 = SRVName;IA5STRING:_xmpp-server.conference.example.com prosody-13.0.1/PaxHeaders/configure0000644000000000000000000000011714773555365014304 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.519710248 prosody-13.0.1/configure0000755000175000017500000003606414773555365016517 0ustar00prosodyprosody00000000000000#!/bin/sh # Defaults APP_NAME="Prosody" APP_DIRNAME="prosody" PREFIX="/usr/local" SYSCONFDIR="$PREFIX/etc/$APP_DIRNAME" LIBDIR="$PREFIX/lib" DATADIR="$PREFIX/var/lib/$APP_DIRNAME" LUA_SUFFIX="" LUA_DIR="/usr" LUA_BINDIR="/usr/bin" LUA_INCDIR="/usr/include" LUA_LIBDIR="/usr/lib" IDN_LIB="idn" ICU_FLAGS="-licui18n -licudata -licuuc" OPENSSL_LIB="crypto" CC="gcc" LD="gcc" RUNWITH="lua" EXCERTS="yes" PRNG= PRNGLIBS= CFLAGS="-fPIC -std=c99" CFLAGS="$CFLAGS -Wall -pedantic -Wextra -Wshadow -Wformat=2" LDFLAGS="-shared" IDN_LIBRARY="icu" # Help show_help() { cat </dev/null) if [ -n "$prog" ] then dirname "$prog" fi } die() { echo "$*" echo echo "configure failed." echo exit 1 } # COMPAT SC2039 has been phased out, remove in the future # shellcheck disable=SC2039,SC3037 case $(echo -n x) in -n*) echo_n_flag='';; *) echo_n_flag='-n';; esac echo_n() { echo $echo_n_flag "$*" } # ---------------------------------------------------------------------------- # MAIN PROGRAM # ---------------------------------------------------------------------------- # Parse options while [ -n "$1" ] do value=$(echo "$1" | sed 's/[^=]*.\(.*\)/\1/') key=$(echo "$1" | sed 's/=.*//') # shellcheck disable=SC2088 if echo "$value" | grep "~" >/dev/null 2>/dev/null then echo echo '*WARNING*: the "~" sign is not expanded in flags.' # shellcheck disable=SC2016 echo 'If you mean the home directory, use $HOME instead.' echo fi case "$key" in --help) show_help exit 0 ;; --prefix) [ -n "$value" ] || die "Missing value in flag $key." PREFIX="$value" PREFIX_SET=yes ;; --sysconfdir) [ -n "$value" ] || die "Missing value in flag $key." SYSCONFDIR="$value" SYSCONFDIR_SET=yes ;; --ostype) OSPRESET="$value" OSPRESET_SET="yes" ;; --libdir) LIBDIR="$value" LIBDIR_SET=yes ;; --datadir) DATADIR="$value" DATADIR_SET=yes ;; --lua-suffix) [ -n "$value" ] || die "Missing value in flag $key." LUA_SUFFIX="$value" LUA_SUFFIX_SET=yes ;; --lua-version|--with-lua-version) [ -n "$value" ] || die "Missing value in flag $key." LUA_VERSION="$value" [ "$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) [ -n "$value" ] || die "Missing value in flag $key." LUA_DIR="$value" LUA_DIR_SET=yes ;; --with-lua-bin) [ -n "$value" ] || die "Missing value in flag $key." LUA_BINDIR="$value" LUA_BINDIR_SET=yes ;; --with-lua-include) [ -n "$value" ] || die "Missing value in flag $key." LUA_INCDIR="$value" LUA_INCDIR_SET=yes ;; --with-lua-lib) [ -n "$value" ] || die "Missing value in flag $key." LUA_LIBDIR="$value" LUA_LIBDIR_SET=yes ;; --with-idn) IDN_LIB="$value" ;; --idn-library) IDN_LIBRARY="$value" ;; --with-ssl) OPENSSL_LIB="$value" ;; --with-random) case "$value" in getrandom) PRNG=GETRANDOM ;; openssl) PRNG=OPENSSL ;; arc4random) PRNG=ARC4RANDOM ;; esac ;; --cflags) CFLAGS="$value" ;; --add-cflags) CFLAGS="$CFLAGS $value" ;; --ldflags) LDFLAGS="$value" ;; --add-ldflags) LDFLAGS="$LDFLAGS $value" ;; --c-compiler) CC="$value" ;; --linker) LD="$value" ;; --runwith) RUNWITH="$value" RUNWITH_SET=yes ;; --no-example-certs) EXCERTS= ;; --compiler-wrapper) CC="$value $CC" LD="$value $LD" ;; *) die "Error: Unknown flag: $1" ;; esac shift done if [ "$OSPRESET_SET" = "yes" ]; then # TODO make this a switch? if [ "$OSPRESET" = "debian" ]; then CFLAGS="$CFLAGS -ggdb" fi if [ "$OSPRESET" = "macosx" ]; then if [ "$LUA_INCDIR_SET" != "yes" ]; then LUA_INCDIR=/usr/local/include; LUA_INCDIR_SET=yes fi if [ "$LUA_LIBDIR_SET" != "yes" ]; then LUA_LIBDIR=/usr/local/lib LUA_LIBDIR_SET=yes fi CFLAGS="$CFLAGS -mmacosx-version-min=10.3" LDFLAGS="-bundle -undefined dynamic_lookup" fi if [ "$OSPRESET" = "linux" ]; then CFLAGS="$CFLAGS -ggdb" fi if [ "$OSPRESET" = "freebsd" ] || [ "$OSPRESET" = "openbsd" ]; then 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="52" LUA_SUFFIX_SET=yes LUA_DIR=/usr/local LUA_DIR_SET=yes CC=cc LD=ld fi if [ "$OSPRESET" = "openbsd" ]; then LUA_INCDIR="/usr/local/include"; LUA_INCDIR_SET="yes" fi if [ "$OSPRESET" = "netbsd" ]; then LUA_INCDIR="/usr/pkg/include/lua-5.2" LUA_INCDIR_SET=yes 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.4"; LUA_SUFFIX_SET=yes fi LUA_CF="$(pkg-config --cflags-only-I lua"$LUA_SUFFIX")" LUA_CF="${LUA_CF#*-I}" LUA_CF="${LUA_CF%% *}" if [ "$LUA_CF" != "" ]; then LUA_INCDIR="$LUA_CF" LUA_INCDIR_SET=yes fi fi fi if [ "$PREFIX_SET" = "yes" ] && [ ! "$SYSCONFDIR_SET" = "yes" ] then if [ "$PREFIX" = "/usr" ] then SYSCONFDIR=/etc/$APP_DIRNAME else SYSCONFDIR=$PREFIX/etc/$APP_DIRNAME fi fi if [ "$PREFIX_SET" = "yes" ] && [ ! "$DATADIR_SET" = "yes" ] then if [ "$PREFIX" = "/usr" ] then DATADIR=/var/lib/$APP_DIRNAME else DATADIR=$PREFIX/var/lib/$APP_DIRNAME fi fi if [ "$PREFIX_SET" = "yes" ] && [ ! "$LIBDIR_SET" = "yes" ] then LIBDIR=$PREFIX/lib fi detect_lua_version() { detected_lua=$("$1" -e 'print(_VERSION:match(" (5%.[234])$"))' 2> /dev/null) if [ "$detected_lua" != "nil" ] then if [ "$LUA_VERSION_SET" != "yes" ] then echo "Lua version detected: $detected_lua" LUA_VERSION=$detected_lua return 0 elif [ "$LUA_VERSION" = "$detected_lua" ] then return 0 fi fi return 1 } search_interpreter() { suffix="$1" if [ "$LUA_BINDIR_SET" = "yes" ] then find_lua="$LUA_BINDIR" elif [ "$LUA_DIR_SET" = "yes" ] then LUA_BINDIR="$LUA_DIR/bin" if [ -f "$LUA_BINDIR/lua$suffix" ] then find_lua="$LUA_BINDIR" fi else find_lua=$(find_program lua"$suffix") fi if [ -n "$find_lua" ] && [ -x "$find_lua/lua$suffix" ] then if detect_lua_version "$find_lua/lua$suffix" then echo "Lua interpreter found: $find_lua/lua$suffix..." if [ "$LUA_BINDIR_SET" != "yes" ] then LUA_BINDIR="$find_lua" fi if [ "$LUA_DIR_SET" != "yes" ] then LUA_DIR=$(dirname "$find_lua") fi LUA_SUFFIX="$suffix" return 0 fi fi return 1 } lua_interp_found=no if [ "$LUA_SUFFIX_SET" != "yes" ] then 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" ] then suffixes="5.3 53 -5.3 -53" elif [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.4" ] then suffixes="5.4 54 -5.4 -54" else suffixes="5.2 52 -5.2 -52" suffixes="$suffixes 5.3 53 -5.3 -53" suffixes="$suffixes 5.4 54 -5.4 -54" fi for suffix in "" $suffixes do search_interpreter "$suffix" && { lua_interp_found=yes break } done else search_interpreter "$LUA_SUFFIX" && { lua_interp_found=yes } fi # See #1353 if [ "$LUA_DIR_SET" != "yes" ] && [ "$LUA_DIR" = "/" ] then LUA_DIR="/usr" fi if [ "$lua_interp_found" != "yes" ] && [ "$RUNWITH_SET" != "yes" ] then if [ "$LUA_VERSION_SET" ]; then interp="Lua $LUA_VERSION"; else interp="Lua"; fi if [ "$LUA_DIR_SET" ] || [ "$LUA_BINDIR_SET" ]; then where="$LUA_BINDIR"; else where="\$PATH"; fi echo "$interp interpreter not found in $where" die "You may want to use the flags --with-lua, --with-lua-bin and/or --lua-suffix. See --help." fi if [ "$LUA_VERSION_SET" = "yes" ] && [ "$RUNWITH_SET" != "yes" ] then echo_n "Checking if $LUA_BINDIR/lua$LUA_SUFFIX is Lua version $LUA_VERSION... " if detect_lua_version "$LUA_BINDIR/lua$LUA_SUFFIX" then echo "yes" else echo "no" die "You may want to use the flags --with-lua, --with-lua-bin and/or --lua-suffix. See --help." fi fi if [ "$LUA_INCDIR_SET" != "yes" ] then LUA_INCDIR="$LUA_DIR/include" fi if [ "$LUA_LIBDIR_SET" != "yes" ] then LUA_LIBDIR="$LUA_DIR/lib" fi lua_h="$LUA_INCDIR/lua.h" echo_n "Looking for lua.h at $lua_h..." if [ -f "$lua_h" ] then echo found else echo "not found" for postfix in "$LUA_VERSION" "$LUA_SUFFIX"; do if ! [ "$postfix" = "" ]; then v_dir="$LUA_INCDIR/lua/$postfix"; else v_dir="$LUA_INCDIR/lua"; fi lua_h="$v_dir/lua.h" echo_n "Looking for lua.h at $lua_h..." if [ -f "$lua_h" ] then LUA_INCDIR="$v_dir" echo found break; else echo "not found" d_dir="$LUA_INCDIR/lua$postfix" lua_h="$d_dir/lua.h" echo_n "Looking for lua.h at $lua_h..." if [ -f "$lua_h" ] then echo found LUA_INCDIR="$d_dir" break; else echo "not found" fi fi done if [ ! -f "$lua_h" ]; then echo "lua.h not found." echo die "You may want to use the flag --with-lua or --with-lua-include. See --help." fi fi if [ "$lua_interp_found" = "yes" ] then echo_n "Checking if Lua header version matches that of the interpreter... " header_version=$(sed -n 's/.*LUA_VERSION_NUM.*5.\(.\).*/5.\1/p' "$lua_h") if [ "$header_version" = "$LUA_VERSION" ] then echo "yes" else echo "no" echo "lua.h version mismatch (interpreter: $LUA_VERSION; lua.h: $header_version)." die "You may want to use the flag --with-lua or --with-lua-include. See --help." fi fi if [ "$IDN_LIBRARY" = "icu" ] then IDNA_LIBS="$ICU_FLAGS" IDNA_FLAGS="-DUSE_STRINGPREP_ICU" fi if [ "$IDN_LIBRARY" = "idn" ] then IDNA_LIBS="-l$IDN_LIB" fi if [ -f config.unix ]; then rm -f config.unix fi if [ "$RUNWITH_SET" != yes ]; then RUNWITH="lua$LUA_SUFFIX" fi OPENSSL_LIBS="-l$OPENSSL_LIB" if [ "$PRNG" = "OPENSSL" ]; then PRNGLIBS=$OPENSSL_LIBS elif [ "$PRNG" = "ARC4RANDOM" ] && [ "$(uname)" = "Linux" ]; then PRNGLIBS="-lbsd" fi # Write config echo "Writing configuration..." echo rm -f built cat < config.unix # This file was automatically generated by the configure script. # Run "./configure --help" for details. LUA_VERSION=$LUA_VERSION PREFIX=$PREFIX SYSCONFDIR=$SYSCONFDIR LIBDIR=$LIBDIR DATADIR=$DATADIR LUA_SUFFIX=$LUA_SUFFIX LUA_DIR=$LUA_DIR LUA_DIR_SET=$LUA_DIR_SET LUA_INCDIR=$LUA_INCDIR LUA_LIBDIR=$LUA_LIBDIR LUA_BINDIR=$LUA_BINDIR IDN_LIB=$IDN_LIB IDNA_FLAGS=$IDNA_FLAGS IDNA_LIBS=$IDNA_LIBS OPENSSL_LIBS=$OPENSSL_LIBS CFLAGS=$CFLAGS LDFLAGS=$LDFLAGS CC=$CC LD=$LD RUNWITH=$RUNWITH EXCERTS=$EXCERTS RANDOM=$PRNG RANDOM_LIBS=$PRNGLIBS EOF echo "Installation prefix: $PREFIX" echo "$APP_NAME configuration directory: $SYSCONFDIR" echo "Using Lua from: $LUA_DIR" make clean > /dev/null 2> /dev/null echo echo "Done. You can now run 'make' to build." echo prosody-13.0.1/PaxHeaders/core0000644000000000000000000000013114773555365013247 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.519710248 30 ctime=1743706869.539710327 prosody-13.0.1/core/0000755000175000017500000000000014773555365015527 5ustar00prosodyprosody00000000000000prosody-13.0.1/core/PaxHeaders/certmanager.lua0000644000000000000000000000011714773555365016323 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.519710248 prosody-13.0.1/core/certmanager.lua0000644000175000017500000003357714773555365020541 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 "prosody.util.x509"; local lfs = require "lfs"; local tonumber, tostring = tonumber, tostring; local pairs = pairs; local t_remove = table.remove; local type = type; local io_open = io.open; local select = select; local now = os.time; local next = next; local pcall = pcall; local prosody = prosody; local pathutil = require"prosody.util.paths"; local resolve_path = pathutil.resolve_relative_path; local config_path = prosody.paths.config or "."; local _ENV = nil; -- luacheck: std none -- Global SSL options if not overridden per-host local global_ssl_config = configmanager.get("*", "ssl"); local global_certificates = configmanager.get("*", "certificates") or "certs"; local crt_try = { "", "/%s.crt", "/%s/fullchain.pem", "/%s.pem", }; local key_try = { "", "/%s.key", "/%s/privkey.pem", "/%s.pem", }; local function find_cert(user_certs, name) local certs = resolve_path(config_path, user_certs or global_certificates); log("debug", "Searching %s for a key and certificate for %s...", certs, name); for i = 1, #crt_try do local crt_path = certs .. crt_try[i]:format(name); local key_path = certs .. key_try[i]:format(name); if stat(crt_path, "mode") == "file" then if crt_path == key_path then if key_path:sub(-4) == ".crt" then key_path = key_path:sub(1, -4) .. "key"; elseif key_path:sub(-14) == "/fullchain.pem" then key_path = key_path:sub(1, -14) .. "privkey.pem"; end end if stat(key_path, "mode") == "file" then log("debug", "Selecting certificate %s with key %s for %s", crt_path, key_path, name); return { certificate = crt_path, key = key_path }; end end end log("debug", "No certificate/key found for %s", name); end local function find_matching_key(cert_path) return (cert_path:gsub("%.crt$", ".key"):gsub("fullchain", "privkey")); end local function index_certs(dir, files_by_name, depth_limit) files_by_name = files_by_name or {}; depth_limit = depth_limit or 3; if depth_limit <= 0 then return files_by_name; end local ok, iter, v, i = pcall(lfs.dir, dir); if not ok then log("error", "Error indexing certificate directory %s: %s", dir, iter); -- Return an empty index, otherwise this just triggers a nil indexing -- error, plus this function would get called again. -- Reloading the config after correcting the problem calls this again so -- that's what should be done. return {}, iter; end for file in iter, v, i do local full = pathutil.join(dir, file); if lfs.attributes(full, "mode") == "directory" then if file:sub(1,1) ~= "." then index_certs(full, files_by_name, depth_limit-1); end elseif file:find("%.crt$") or file:find("fullchain") then -- This should catch most fullchain files local f, err = io_open(full); if f then -- TODO look for chained certificates local firstline = f:read(); if firstline == "-----BEGIN CERTIFICATE-----" and lfs.attributes(find_matching_key(full), "mode") == "file" then f:seek("set") 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 -- TODO also check if there's a corresponding key if cert:validat(now()) then local names = x509.get_identities(cert); log("debug", "Found certificate %s with identities %q", full, names); for name, services in pairs(names) do -- TODO check services if files_by_name[name] then files_by_name[name][full] = services; else files_by_name[name] = { [full] = services; }; end end else log("debug", "Skipping expired certificate: %s", full); end else log("debug", "Skipping non-certificate (based on contents): %s", full); end f:close(); elseif err then log("debug", "Skipping file due to error: %s", err); end else log("debug", "Skipping non-certificate (based on filename): %s", full); end end -- | hostname | filename | service | return files_by_name; end local cert_index; local function find_cert_in_index(index, host) if not host then return nil; end if not index then return nil; end local wildcard_host = host:gsub("^[^.]+%.", "*."); local certs = index[host] or index[wildcard_host]; if certs then local cert_filename, services = next(certs); if services["*"] then log("debug", "Using cert %q from index for host %q", cert_filename, host); return { certificate = cert_filename, key = find_matching_key(cert_filename), } end end return nil end local function find_host_cert(host) if not host then return nil; end if not cert_index then cert_index = index_certs(resolve_path(config_path, global_certificates)); end return find_cert_in_index(cert_index, host) or find_cert(configmanager.get(host, "certificate"), host) or find_host_cert(host:match("%.(.+)$")); end local function find_service_cert(service, port) if not cert_index then cert_index = index_certs(resolve_path(config_path, global_certificates)); end for _, certs in pairs(cert_index) do for cert_filename, services in pairs(certs) do if services[service] or services["*"] then log("debug", "Using cert %q from index for service %s port %d", cert_filename, service, port); return { certificate = cert_filename, key = find_matching_key(cert_filename), } end end end local cert_config = configmanager.get("*", service.."_certificate"); if type(cert_config) == "table" then cert_config = cert_config[port] or cert_config.default; end return find_cert(cert_config, service); end -- Built-in defaults local core_defaults = { capath = "/etc/ssl/certs"; depth = 9; protocol = "tlsv1+"; verify = "none"; options = { 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; }; curve = tls.features.algorithms.ec and not tls.features.capabilities.curves_list and "secp384r1"; curveslist = { "X25519", "P-384", "P-256", "P-521", }; ciphers = { -- Enabled ciphers in order of preference: "HIGH+kEECDH", -- Ephemeral Elliptic curve Diffie-Hellman key exchange "HIGH+kEDH", -- Ephemeral Diffie-Hellman key exchange, if a 'dhparam' file is set "HIGH", -- Other "High strength" ciphers -- Disabled cipher suites: "!PSK", -- Pre-Shared Key - not used for XMPP "!SRP", -- Secure Remote Password - not used for XMPP "!3DES", -- 3DES - slow and of questionable security "!aNULL", -- Ciphers that does not authenticate the connection }; 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 modern = { protocol = "tlsv1_3"; options = { cipher_server_preference = false }; ciphers = "DEFAULT"; -- TLS 1.3 uses 'ciphersuites' rather than these curveslist = { "X25519"; "prime256v1"; "secp384r1" }; ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" }; }; intermediate = { protocol = "tlsv1_2+"; dhparam = ffdhe2048; options = { cipher_server_preference = false }; ciphers = { "ECDHE-ECDSA-AES128-GCM-SHA256"; "ECDHE-RSA-AES128-GCM-SHA256"; "ECDHE-ECDSA-AES256-GCM-SHA384"; "ECDHE-RSA-AES256-GCM-SHA384"; "ECDHE-ECDSA-CHACHA20-POLY1305"; "ECDHE-RSA-CHACHA20-POLY1305"; "DHE-RSA-AES128-GCM-SHA256"; "DHE-RSA-AES256-GCM-SHA384"; "DHE-RSA-CHACHA20-POLY1305"; }; curveslist = { "X25519"; "prime256v1"; "secp384r1" }; ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" }; }; old = { protocol = "tlsv1+"; dhparam = nil; -- openssl dhparam 1024 options = { cipher_server_preference = true }; ciphers = { "ECDHE-ECDSA-AES128-GCM-SHA256"; "ECDHE-RSA-AES128-GCM-SHA256"; "ECDHE-ECDSA-AES256-GCM-SHA384"; "ECDHE-RSA-AES256-GCM-SHA384"; "ECDHE-ECDSA-CHACHA20-POLY1305"; "ECDHE-RSA-CHACHA20-POLY1305"; "DHE-RSA-AES128-GCM-SHA256"; "DHE-RSA-AES256-GCM-SHA384"; "DHE-RSA-CHACHA20-POLY1305"; "ECDHE-ECDSA-AES128-SHA256"; "ECDHE-RSA-AES128-SHA256"; "ECDHE-ECDSA-AES128-SHA"; "ECDHE-RSA-AES128-SHA"; "ECDHE-ECDSA-AES256-SHA384"; "ECDHE-RSA-AES256-SHA384"; "ECDHE-ECDSA-AES256-SHA"; "ECDHE-RSA-AES256-SHA"; "DHE-RSA-AES128-SHA256"; "DHE-RSA-AES256-SHA256"; "AES128-GCM-SHA256"; "AES256-GCM-SHA384"; "AES128-SHA256"; "AES256-SHA256"; "AES128-SHA"; "AES256-SHA"; "DES-CBC3-SHA"; }; curveslist = { "X25519"; "prime256v1"; "secp384r1" }; ciphersuites = { "TLS_AES_128_GCM_SHA256"; "TLS_AES_256_GCM_SHA384"; "TLS_CHACHA20_POLY1305_SHA256" }; }; }; if tls.features.curves then for i = #core_defaults.curveslist, 1, -1 do if not tls.features.curves[ core_defaults.curveslist[i] ] then t_remove(core_defaults.curveslist, i); end end else core_defaults.curveslist = nil; end local function create_context(host, mode, ...) local cfg = new_config(); cfg:apply(core_defaults); local service_name, port = host:match("^(%S+) port (%d+)$"); -- port 0 is used with client-only things that normally don't need certificates, e.g. https if service_name and port ~= "0" then log("debug", "Automatically locating certs for service %s on port %s", service_name, port); cfg:apply(find_service_cert(service_name, tonumber(port))); else log("debug", "Automatically locating certs for host %s", host); cfg:apply(find_host_cert(host)); end cfg:apply({ mode = mode, -- We can't read the password interactively when daemonized password = function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end; }); local profile = configmanager.get("*", "tls_profile") or "intermediate"; if mozilla_ssl_configs[profile] then cfg:apply(mozilla_ssl_configs[profile]); elseif profile ~= "legacy" then log("error", "Invalid value for 'tls_profile': expected one of \"modern\", \"intermediate\" (default), \"old\" or \"legacy\" but got %q", profile); return nil, "Invalid configuration, 'tls_profile' had an unknown value."; end cfg:apply(global_ssl_config); for i = select('#', ...), 1, -1 do cfg:apply(select(i, ...)); end local user_ssl_config = cfg:final(); if mode == "server" then if not user_ssl_config.certificate then 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 local ctx, err = cfg:build(); if not ctx then err = err or "invalid ssl config" local file = err:match("^error loading (.-) %("); if file then local typ; if file == "private key" then typ = file; file = user_ssl_config.key or "your private key"; elseif file == "certificate" then typ = file; file = user_ssl_config.certificate or "your certificate file"; end local reason = err:match("%((.+)%)$") or "some reason"; if reason == "Permission denied" then reason = "Check that the permissions allow Prosody to read this file."; elseif reason == "No such file or directory" then reason = "Check that the path is correct, and the file exists."; elseif reason == "system lib" then reason = "Previous error (see logs), or other system error."; elseif reason == "no start line" then reason = "Check that the file contains a "..(typ or file); elseif reason == "(null)" or not reason then reason = "Check that the file exists and the permissions are correct"; else reason = "Reason: "..tostring(reason):lower(); end log("error", "SSL/TLS: Failed to load '%s': %s (for %s)", file, reason, host); else log("error", "SSL/TLS: Error initialising for %s: %s", host, err); end end return ctx, err, user_ssl_config; end local function reload_ssl_config() global_ssl_config = configmanager.get("*", "ssl"); global_certificates = configmanager.get("*", "certificates") or "certs"; if tls.features.options.no_compression then core_defaults.options.no_compression = configmanager.get("*", "ssl_compression") ~= true; end 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 prosody.events.add_handler("config-reloaded", reload_ssl_config); return { create_context = create_context; reload_ssl_config = reload_ssl_config; find_cert = find_cert; index_certs = index_certs; find_host_cert = find_host_cert; find_cert_in_index = find_cert_in_index; }; prosody-13.0.1/core/PaxHeaders/configmanager.lua0000644000000000000000000000011714773555365016633 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.523710263 prosody-13.0.1/core/configmanager.lua0000644000175000017500000003021714773555365021035 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local _G = _G; 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"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 "prosody.util.debug".get_traceback_table; local errors = require "prosody.util.error"; local log = require "prosody.util.logger".init("config"); local encodings = deps.softreq"prosody.util.encodings"; local nameprep = encodings and encodings.stringprep.nameprep or function (host) return host:lower(); end local _M = {}; local _ENV = nil; -- luacheck: std none _M.resolve_relative_path = resolve_relative_path; -- COMPAT local parser = nil; local config_mt = { __index = function (t, _) return rawget(t, "*"); end}; local config = setmetatable({ ["*"] = { } }, config_mt); local files = {}; local credentials_directory = nil; local credential_fallback_fatal = true; -- When host not found, use global local host_mt = { __index = function(_, k) return config["*"][k] end } function _M.getconfig() return config; end function _M.get(host, key) local v = config[host][key]; if v and errors.is_error(v) then log("warn", "%s:%d: %s", v.context.filename, v.context.fileline, v.text); return nil; end return v; end function _M.rawget(host, key) local hostconfig = rawget(config, host); if hostconfig then return rawget(hostconfig, key); end end local function set(config_table, host, key, value) if host and key then local hostconfig = rawget(config_table, host); if not hostconfig then hostconfig = rawset(config_table, host, setmetatable({}, host_mt))[host]; end hostconfig[key] = value; return true; end return false; end 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 function _M.load(filename, config_format) config_format = config_format or filename:match("%w+$"); if config_format == "lua" then local f, err = io.open(filename); if f then local new_config = setmetatable({ ["*"] = { } }, config_mt); local ok, err = parser.load(f:read("*a"), filename, new_config); f:close(); if ok then config = new_config; end return ok, "parser", err; end return f, "file", err; end if not config_format then return nil, "file", "no parser specified"; else return nil, "file", "no parser for "..(config_format); end end function _M.files() return files; end -- Built-in Lua parser do local pcall = _G.pcall; local function get_line_number(config_file) local tb = get_traceback_table(nil, 2); for i = 1, #tb do if tb[i].info.short_src == config_file then return tb[i].info.currentline; 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; }; -- For reading config values out of files. local function filereader(basepath, defaultmode) return function(filename, mode) local f, err = io.open(resolve_relative_path(basepath, filename)); if not f then error(err, 2); end local content, err = f:read(mode or defaultmode); f:close(); if not content then error(err, 2); end return content; end end -- Collect lines into an array local function linereader(basepath) return function(filename) local ret = {}; for line in io.lines(resolve_relative_path(basepath, filename)) do t_insert(ret, line); end return ret; end 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) local warnings = {}; local env; local config_path = config_file:gsub("[^"..path_sep.."]+$", ""); -- The ' = true' are needed so as not to set off __newindex when we assign the functions below env = setmetatable({ Host = true, host = true, VirtualHost = true, Component = true, component = true, FileContents = true, FileLine = true, FileLines = true, Credential = true, Include = true, include = true, RunScript = true }, { __index = function (_, k) if k:match("^ENV_") then return os.getenv(k:sub(5)); end 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 "*"; local option_path = host.."/"..k; if set_options[option_path] then t_insert(warnings, ("%s:%d: Duplicate option '%s'"):format(config_file, get_line_number(config_file), k)); end set_options[option_path] = true; set(config_table, env.__currenthost or "*", k, v); end }); rawset(env, "__currenthost", "*") -- Default is global function env.VirtualHost(name) if not name then error("Host must have a name", 2); end local prepped_name = nameprep(name); if not prepped_name then error(format("Name of Host %q contains forbidden characters", name), 0); end name = prepped_name; if rawget(config_table, name) and rawget(config_table[name], "component_module") then error(format("Host %q clashes with previously defined %s Component %q, for services use a sub-domain like conference.%s", name, config_table[name].component_module:gsub("^%a+$", { component = "external", muc = "MUC"}), name, name), 0); end rawset(env, "__currenthost", name); -- Needs at least one setting to logically exist :) set(config_table, name or "*", "defined", true); return function (config_options) rawset(env, "__currenthost", "*"); -- Return to global scope if type(config_options) == "string" then error(format("VirtualHost entries do not accept a module name (module '%s' provided for host '%s')", config_options, name), 2); elseif type(config_options) ~= "table" then error("Invalid syntax following VirtualHost, expected options but received a "..type(config_options), 2); end for option_name, option_value in pairs(config_options) do set(config_table, name or "*", option_name, option_value); end end; end env.Host, env.host = env.VirtualHost, env.VirtualHost; function env.Component(name) if not name then error("Component must have a name", 2); end local prepped_name = nameprep(name); if not prepped_name then error(format("Name of Component %q contains forbidden characters", name), 0); end name = prepped_name; if rawget(config_table, name) and rawget(config_table[name], "defined") and not rawget(config_table[name], "component_module") then error(format("Component %q clashes with previously defined VirtualHost %q, for services use a sub-domain like conference.%s", name, name, name), 0); end set(config_table, name, "component_module", "component"); -- Don't load the global modules by default set(config_table, name, "load_global_modules", false); rawset(env, "__currenthost", name); local function handle_config_options(config_options) rawset(env, "__currenthost", "*"); -- Return to global scope for option_name, option_value in pairs(config_options) do set(config_table, name or "*", option_name, option_value); end end return function (module) if type(module) == "string" then set(config_table, name, "component_module", module); return handle_config_options; end return handle_config_options(module); end end env.component = env.Component; function env.Include(file) -- Check whether this is a wildcard Include if file:match("[*?]") then local lfs = deps.softreq "lfs"; if not lfs then error(format("Error expanding wildcard pattern in Include %q - LuaFileSystem not available", file)); end local path_pos, glob = file:match("()([^"..path_sep.."]+)$"); local path = file:sub(1, math_max(path_pos-2,0)); if #path > 0 then path = resolve_relative_path(config_path, path); else path = config_path; end local patt = glob_to_pattern(glob); for f in lfs.dir(path) do if f:sub(1,1) ~= "." and f:match(patt) then env.Include(path..path_sep..f); end end return; end -- Not a wildcard, so resolve (potentially) relative path and run through config parser file = resolve_relative_path(config_path, file); local f, err = io.open(file); if f then local ret, err = parser.load(f:read("*a"), file, config_table); if not ret then error(err:gsub("%[string.-%]", file), 0); end if err then for _, warning in ipairs(err) do t_insert(warnings, warning); end end end if not f then error("Error loading included "..file..": "..err, 0); end return f, err; end env.include = env.Include; function env.RunScript(file) return dofile(resolve_relative_path(config_path, file)); end env.FileContents = filereader(config_path, "*a"); env.FileLine = filereader(config_path, "*l"); env.FileLines = linereader(config_path); if credentials_directory then env.Credential = filereader(credentials_directory, "*a"); elseif credential_fallback_fatal then env.Credential = function() error("Credential() requires the $CREDENTIALS_DIRECTORY environment variable to be set", 2) end else env.Credential = function() return errors.new({ type = "continue"; text = "Credential() requires the $CREDENTIALS_DIRECTORY environment variable to be set"; }, { filename = config_file; fileline = get_line_number(config_file) }); end end local chunk, err = envload(data, "@"..config_file, env); if not chunk then return nil, err; end local ok, err = pcall(chunk); if not ok then return nil, err; end t_insert(files, config_file); return true, warnings; end end function _M.set_credentials_directory(directory) credentials_directory = directory; end function _M.set_credential_fallback_mode(mode) credential_fallback_fatal = mode == "error"; end return _M; prosody-13.0.1/core/PaxHeaders/features.lua0000644000000000000000000000011714773555365015651 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.523710263 prosody-13.0.1/core/features.lua0000644000175000017500000000145214773555365020052 0ustar00prosodyprosody00000000000000local set = require "prosody.util.set"; return { available = set.new{ -- mod_bookmarks bundled "mod_bookmarks"; -- mod_server_info bundled "mod_server_info"; -- mod_flags bundled "mod_flags"; -- mod_cloud_notify bundled "mod_cloud_notify"; -- mod_muc has built-in vcard support "muc_vcard"; -- mod_http_altconnect bundled "http_altconnect"; -- 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"; }; }; prosody-13.0.1/core/PaxHeaders/hostmanager.lua0000644000000000000000000000011714773555365016343 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.523710263 prosody-13.0.1/core/hostmanager.lua0000644000175000017500000001241314773555365020543 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 "prosody.util.logger".init("hostmanager"); local hosts = prosody.hosts; local prosody_events = prosody.events; if not _G.prosody.incoming_s2s then require "prosody.core.s2smanager"; end local incoming_s2s = _G.prosody.incoming_s2s; local core_route_stanza = _G.prosody.core_route_stanza; local pairs, rawget = pairs, rawget; local tostring, type = tostring, type; local setmetatable = setmetatable; local _ENV = nil; -- luacheck: std none local host_mt = { } function host_mt:__tostring() if self.type == "component" then local typ = configmanager.get(self.host, "component_module"); if typ == "component" then return ("Component %q"):format(self.host); end return ("Component %q %q"):format(self.host, typ); elseif self.type == "local" then return ("VirtualHost %q"):format(self.host); end end local hosts_loaded_once; local activate, deactivate; local function load_enabled_hosts(config) local defined_hosts = config or configmanager.getconfig(); local activated_any_host; for host, host_config in pairs(defined_hosts) do if host ~= "*" and host_config.enabled ~= false then if not host_config.component_module then activated_any_host = true; end activate(host, host_config); end end if not activated_any_host then log("error", "No active VirtualHost entries in the config file. This may cause unexpected behaviour as no modules will be loaded."); end prosody_events.fire_event("hosts-activated", defined_hosts); hosts_loaded_once = true; end prosody_events.add_handler("server-starting", load_enabled_hosts); local function host_send(stanza) core_route_stanza(nil, stanza); end function activate(host, host_config) if rawget(hosts, host) then return nil, "The host "..host.." is already activated"; end host_config = host_config or configmanager.getconfig()[host]; if not host_config then return nil, "Couldn't find the host "..tostring(host).." defined in the current config"; end local host_session = { host = host; s2sout = {}; events = events_new(); send = host_send; modules = {}; }; function host_session:close(reason) log("debug", "Attempt to close host session %s with reason: %s", self.host, reason); end setmetatable(host_session, host_mt); if not host_config.component_module then -- host host_session.type = "local"; host_session.sessions = {}; else -- component host_session.type = "component"; end hosts[host] = host_session; if not host_config.disco_hidden and not host:match("[@/]") then disco_items:set(host:match("%.(.*)") or "*", host, host_config.name or true); end for option_name in pairs(host_config) do if option_name:match("_ports$") or option_name:match("_interface$") then log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in the server-wide section instead", host, option_name); end end log((hosts_loaded_once and "info") or "debug", "Activated host: %s", host); prosody_events.fire_event("host-activated", host); return true; end function deactivate(host, reason) local host_session = hosts[host]; if not host_session then return nil, "The host "..tostring(host).." is not activated"; end log("info", "Deactivating host: %s", host); prosody_events.fire_event("host-deactivating", { host = host, host_session = host_session, reason = reason }); if type(reason) ~= "table" then reason = { condition = "host-gone", text = tostring(reason or "This server has stopped serving "..host) }; end -- Disconnect local users, s2s connections -- TODO: These should move to mod_c2s and mod_s2s (how do they know they're being unloaded and not reloaded?) if host_session.sessions then for username, user in pairs(host_session.sessions) do for resource, session in pairs(user.sessions) do log("debug", "Closing connection for %s@%s/%s", username, host, resource); session:close(reason); end end end if host_session.s2sout then for remotehost, session in pairs(host_session.s2sout) do if session.close then log("debug", "Closing outgoing connection to %s", remotehost); session:close(reason); end end end for remote_session in pairs(incoming_s2s) do if remote_session.to_host == host then log("debug", "Closing incoming connection from %s", remote_session.from_host or ""); remote_session:close(reason); end end -- TODO: This should be done in modulemanager if host_session.modules then for module in pairs(host_session.modules) do modulemanager.unload(host, module); end end hosts[host] = nil; if not host:match("[@/]") then disco_items:remove(host:match("%.(.*)") or "*", host); end prosody_events.fire_event("host-deactivated", host); log("info", "Deactivated host: %s", host); return true; end local function get_children(host) return disco_items:get(host) or NULL; end return { activate = activate; deactivate = deactivate; get_children = get_children; } prosody-13.0.1/core/PaxHeaders/loggingmanager.lua0000644000000000000000000000011714773555365017014 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.523710263 prosody-13.0.1/core/loggingmanager.lua0000644000175000017500000001770014773555365021220 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 "prosody.util.termcolours".getstyle, require "prosody.util.termcolours".getstring; local st = require "prosody.util.stanza"; local config = require "prosody.core.configmanager"; local logger = require "prosody.util.logger"; 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 -- The log config used if none specified in the config file (see reload_logging for initialization) local default_logging; local default_file_logging; local default_timestamp = "%b %d %H:%M:%S "; -- The actual config loggingmanager is using local logging_config; local apply_sink_rules; local log_sink_types = setmetatable({}, { __newindex = function (t, k, v) rawset(t, k, v); apply_sink_rules(k); end; }); local get_levels; local logging_levels = { "debug", "info", "warn", "error" } local function id(x) return x end -- Put a rule into action. Requires that the sink type has already been registered. -- This function is called automatically when a new sink type is added [see apply_sink_rules()] local function add_rule(sink_config) local sink_maker = log_sink_types[sink_config.to]; if not sink_maker then return; -- No such sink type end -- Create sink local sink = sink_maker(sink_config); -- Set sink for all chosen levels for level in pairs(get_levels(sink_config.levels or logging_levels)) do logger.add_level_sink(level, sink); end end -- Search for all rules using a particular sink type, and apply -- them. Called automatically when a new sink type is added to -- the log_sink_types table. function apply_sink_rules(sink_type) if type(logging_config) == "table" then for _, level in ipairs(logging_levels) do if type(logging_config[level]) == "string" then local value = logging_config[level]; if sink_type == "file" and not value:match("^%*") then add_rule({ to = sink_type; filename = value; timestamps = true; levels = { min = level }; }); elseif value == "*"..sink_type then add_rule({ to = sink_type; levels = { min = level }; }); end end end for _, sink_config in ipairs(logging_config) do if (type(sink_config) == "table" and sink_config.to == sink_type) then add_rule(sink_config); elseif (type(sink_config) == "string" and sink_config:match("^%*(.+)") == sink_type) then add_rule({ levels = { min = "debug" }, to = sink_type }); end end elseif type(logging_config) == "string" and (not logging_config:match("^%*")) and sink_type == "file" then -- User specified simply a filename, and the "file" sink type -- was just added for _, sink_config in pairs(default_file_logging) do sink_config.filename = logging_config; add_rule(sink_config); sink_config.filename = nil; end elseif type(logging_config) == "string" and logging_config:match("^%*(.+)") == sink_type then -- Log all levels (debug+) to this sink add_rule({ levels = { min = "debug" }, to = sink_type }); end end --- Helper function to get a set of levels given a "criteria" table function get_levels(criteria, set) set = set or {}; if type(criteria) == "string" then set[criteria] = true; return set; end local min, max = criteria.min, criteria.max; if min or max then local in_range; for _, level in ipairs(logging_levels) do if min == level then set[level] = true; in_range = true; elseif max == level then set[level] = true; return set; elseif in_range then set[level] = true; end end end for _, level in ipairs(criteria) do set[level] = true; end return set; end -- Initialize config, etc. -- local function reload_logging() local old_sink_types = {}; for name, sink_maker in pairs(log_sink_types) do old_sink_types[name] = sink_maker; log_sink_types[name] = nil; end logger.reset(); local debug_mode = config.get("*", "debug"); default_logging = { { to = "console" , levels = { min = (debug_mode and "debug") or "info" } } }; default_file_logging = { { to = "file", levels = { min = (debug_mode and "debug") or "info" }, timestamps = true } }; logging_config = config.get("*", "log") or default_logging; for name, sink_maker in pairs(old_sink_types) do log_sink_types[name] = sink_maker; end end --- Definition of built-in logging sinks --- -- Null sink, must enter log_sink_types *first* local function log_to_nowhere() return function () return false; end; end log_sink_types.nowhere = log_to_nowhere; local function log_to_file(sink_config, logfile) logfile = logfile or io_open(sink_config.filename, "a+"); if not logfile then return log_to_nowhere(sink_config); end local write = logfile.write; local timestamps = sink_config.timestamps; if timestamps == true or timestamps == nil then timestamps = default_timestamp; -- Default format elseif timestamps then timestamps = timestamps .. " "; end if sink_config.buffer_mode ~= false then logfile:setvbuf(sink_config.buffer_mode or "line"); end -- Column width for "source" (used by stdout and console) local sourcewidth = sink_config.source_width; local filter = sink_config.filter or id; if sourcewidth then return function (name, level, message, ...) sourcewidth = math_max(#name+2, sourcewidth); write(logfile, timestamps and os_date(timestamps) or "", name, rep(" ", sourcewidth-#name), level, "\t", filter(format(message, ...)), "\n"); end else return function (name, level, message, ...) write(logfile, timestamps and os_date(timestamps) or "", name, "\t", level, "\t", filter(format(message, ...)), "\n"); end end end log_sink_types.file = log_to_file; local function log_to_stdout(sink_config) if not sink_config.timestamps then sink_config.timestamps = false; end if sink_config.source_width == nil then sink_config.source_width = 20; end return log_to_file(sink_config, stdout); end log_sink_types.stdout = log_to_stdout; local do_pretty_printing = not have_pposix or pposix.isatty(stdout); local logstyles, pretty; if do_pretty_printing then logstyles = {}; logstyles["info"] = getstyle("bold"); logstyles["warn"] = getstyle("bold", "yellow"); logstyles["error"] = getstyle("bold", "red"); pretty = st.pretty_print; end local function log_to_console(sink_config) -- Really if we don't want pretty colours then just use plain stdout -- FIXME refactor to allow console logging with colours on stderr if not do_pretty_printing then return log_to_stdout(sink_config); end sink_config.filter = pretty; local logstdout = log_to_stdout(sink_config); return function (name, level, message, ...) local logstyle = logstyles[level]; if logstyle then level = getstring(logstyle, level); end return logstdout(name, level, message, ...); end end log_sink_types.console = log_to_console; if have_pposix then local syslog_opened; local function log_to_syslog(sink_config) -- luacheck: ignore 212/sink_config if not syslog_opened then local facility = sink_config.syslog_facility or config.get("*", "syslog_facility"); pposix.syslog_open(sink_config.syslog_name or "prosody", facility); syslog_opened = true; end local syslog = pposix.syslog_log; return function (name, level, message, ...) syslog(level, name, format(message, ...)); end; end log_sink_types.syslog = log_to_syslog; end local function register_sink_type(name, sink_maker) local old_sink_maker = log_sink_types[name]; log_sink_types[name] = sink_maker; return old_sink_maker; end return { reload_logging = reload_logging; register_sink_type = register_sink_type; } prosody-13.0.1/core/PaxHeaders/moduleapi.lua0000644000000000000000000000011714773555365016012 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.527710279 prosody-13.0.1/core/moduleapi.lua0000644000175000017500000006116314773555365020220 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2012 Matthew Wild -- Copyright (C) 2008-2012 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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; local unpack = table.unpack; local prosody = prosody; local hosts = prosody.hosts; -- FIXME: This assert() is to try and catch an obscure bug (2013-04-05) local core_post_stanza = assert(prosody.core_post_stanza, "prosody.core_post_stanza is nil, please report this as a bug"); -- Registry of shared module data local shared_data = setmetatable({}, { __mode = "v" }); local NULL = {}; local api = {}; -- Returns the name of the current module function api:get_name() return self.name; end -- Returns the host that the current module is serving function api:get_host() return self.host; end function api:get_host_type() return (self.host == "*" and "global") or hosts[self.host].type or "local"; end function api:set_global() self.host = "*"; -- Update the logger local _log = logger.init("mod_"..self.name); self.log = function (self, ...) return _log(...); end; --luacheck: ignore self self._log = _log; self.global = true; end function api:add_feature(xmlns) self:add_item("feature", xmlns); end function api:add_identity(category, identity_type, name) self:add_item("identity", {category = category, type = identity_type, name = name}); end function api:add_extension(data) self:add_item("extension", data); end function api:fire_event(...) return (hosts[self.host] or prosody).events.fire_event(...); end function api:hook_object_event(object, event, handler, priority) self.event_handlers:set(object, event, handler, true); return object.add_handler(event, handler, priority); end function api:unhook_object_event(object, event, handler) self.event_handlers:set(object, event, handler, nil); return object.remove_handler(event, handler); end function api:hook(event, handler, priority) return self:hook_object_event((hosts[self.host] or prosody).events, event, handler, priority); end function api:hook_global(event, handler, priority) return self:hook_object_event(prosody.events, event, handler, priority); end function api:hook_tag(xmlns, name, handler, priority) if not handler and type(name) == "function" then -- If only 2 options then they specified no xmlns xmlns, name, handler, priority = nil, xmlns, name, handler; elseif not (handler and name) then self:log("warn", "Error: Insufficient parameters to module:hook_tag()"); return; end return self:hook("stanza/"..(xmlns and (xmlns..":") or "")..name, function (data) return handler(data.origin, data.stanza, data); end, priority); end api.hook_stanza = api.hook_tag; -- COMPAT w/pre-0.9 function api:unhook(event, handler) return self:unhook_object_event((hosts[self.host] or prosody).events, event, handler); end function api:wrap_object_event(events_object, event, handler) return self:hook_object_event(assert(events_object.wrappers, "no wrappers"), event, handler); end function api:wrap_event(event, handler) return self:wrap_object_event((hosts[self.host] or prosody).events, event, handler); end function api:wrap_global(event, handler) return self:hook_object_event(prosody.events, event, handler); end function api:require(lib) 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, soft) local modulemanager = require"prosody.core.modulemanager"; if self:get_option_inherited_set("modules_disabled", {}):contains(name) then if not soft then error("Dependency on disabled module mod_"..name); end self:log("debug", "Not loading disabled soft dependency mod_%s", name); return nil, "disabled"; end if not self.dependencies then self.dependencies = {}; self:hook("module-reloaded", function (event) if self.dependencies[event.module] and not self.reloading then self:log("info", "Auto-reloading due to reload of %s:%s", event.host, event.module); modulemanager.reload(self.host, self.name); return; end end); self:hook("module-unloaded", function (event) if self.dependencies[event.module] then self:log("info", "Auto-unloading due to unload of %s:%s", event.host, event.module); modulemanager.unload(self.host, self.name); end end); end local mod = modulemanager.get_module(self.host, name) or modulemanager.get_module("*", name); if mod and mod.module.host == "*" and self.host ~= "*" and modulemanager.module_has_method(mod, "add_host") then mod = nil; -- Target is a shared module, so we still want to load it on our host end if not mod then local err; mod, err = modulemanager.load(self.host, name); if not mod then return error(("Unable to load required module, mod_%s: %s"):format(name, ((err or "unknown error"):gsub("%-", " ")) )); 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 local function get_shared_table_from_path(module, tables, path) if path:sub(1,1) ~= "/" then -- Prepend default components local default_path_components = { module.host, module.name }; local n_components = select(2, path:gsub("/", "%1")); path = (n_components<#default_path_components and "/" or "") ..t_concat(default_path_components, "/", 1, #default_path_components-n_components).."/"..path; end local shared = tables[path]; if not shared then shared = {}; if path:match("%-cache$") then setmetatable(shared, { __mode = "kv" }); end tables[path] = shared; end return shared; end -- Returns a shared table at the specified virtual path -- Intentionally does not allow the table to be _set_, it -- is auto-created if it does not exist. function api:shared(path) if not self.shared_data then self.shared_data = {}; end local shared = get_shared_table_from_path(self, shared_data, path); self.shared_data[path] = shared; return shared; end function api:get_option(name, default_value) local config = require "prosody.core.configmanager"; local value = config.get(self.host, name); if value == nil then value = default_value; end return value; end function api:get_option_scalar(name, default_value) local value = self:get_option(name, default_value); if type(value) == "table" then if #value > 1 then self:log("error", "Config option '%s' does not take a list, using just the first item", name); end value = value[1]; end return value; end function api:get_option_string(name, default_value) local value = self:get_option_scalar(name, default_value); if value == nil then return nil; end return tostring(value); end 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 function api:get_option_boolean(name, ...) local value = self:get_option_scalar(name, ...); if value == nil then return nil; end local ret = value == true or value == "true" or value == 1 or nil; if ret == nil then ret = (value == false or value == "false" or value == 0); if ret then ret = false; else ret = nil; end end if ret == nil then self:log("error", "Config option '%s' not understood, expecting true/false", name); end return ret; end function api:get_option_array(name, ...) local value = self:get_option(name, ...); if value == nil then return nil; end if type(value) ~= "table" then return array{ value }; -- Assume any non-list is a single-item list end return array():append(value); -- Clone end function api:get_option_set(name, ...) local value = self:get_option_array(name, ...); if value == nil then return nil; end return set.new(value); end function api:get_option_inherited_set(name, ...) local value = self:get_option_set(name, ...); local global_value = self:context("*"):get_option_set(name, ...); if not value then return global_value; elseif not global_value then return value; end value:include(global_value); return value; end function api:get_option_path(name, default, parent) if parent == nil then parent = self:get_directory(); elseif prosody.paths[parent] then parent = prosody.paths[parent]; end local value = self:get_option_string(name, default); if value == nil then return nil; end 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 }); end function api:add_item(key, value) self.items = self.items or {}; self.items[key] = self.items[key] or {}; t_insert(self.items[key], value); self:fire_event("item-added/"..key, {source = self, item = value}); end function api:remove_item(key, value) local t = self.items and self.items[key] or NULL; for i = #t,1,-1 do if t[i] == value then t_remove(self.items[key], i); self:fire_event("item-removed/"..key, {source = self, item = value}); return value; end end end function api:get_host_items(key) local modulemanager = require"prosody.core.modulemanager"; local result = modulemanager.get_items(key, self.host) or {}; return result; end function api:handle_items(item_type, added_cb, removed_cb, existing) self:hook("item-added/"..item_type, added_cb); self:hook("item-removed/"..item_type, removed_cb); if existing ~= false then local modulemanager = require"prosody.core.modulemanager"; local modules = modulemanager.get_modules(self.host); for _, module in pairs(modules) do local mod = module.module; if mod.items and mod.items[item_type] then for _, item in ipairs(mod.items[item_type]) do added_cb({ source = mod; item = item }); end end end end end function api:provides(name, item) -- if not item then item = setmetatable({}, { __index = function(t,k) return rawget(self.environment, k); end }); end if not item then item = {} for k,v in pairs(self.environment) do if k ~= "module" then item[k] = v; end end end if not item.name then local item_name = self.name; -- Strip a provider prefix to find the item name -- (e.g. "auth_foo" -> "foo" for an auth provider) if item_name:find((name:gsub("%-", "_")).."_", 1, true) == 1 then item_name = item_name:sub(#name+2); end item.name = item_name; end item._provided_by = self.name; self:add_item(name.."-provider", item); end function api:send(stanza, origin) return core_post_stanza(origin or hosts[self.host], stanza); end function api:send_iq(stanza, origin, timeout) local iq_cache = self._iq_cache; if not iq_cache then iq_cache = cache.new(256, function (_, iq) iq.reject(errors.new({ type = "wait", condition = "resource-constraint", text = "evicted from iq tracking cache" })); end); self._iq_cache = iq_cache; end local event_type; if not jid_node(stanza.attr.from) then event_type = "host"; elseif jid_resource(stanza.attr.from) then event_type = "full"; else -- assume bare since we can't hook full jids event_type = "bare"; end local result_event = "iq-result/"..event_type.."/"..stanza.attr.id; local error_event = "iq-error/"..event_type.."/"..stanza.attr.id; local cache_key = event_type.."/"..stanza.attr.id; if event_type == "full" then result_event = "iq/" .. event_type; error_event = "iq/" .. event_type; end local p = promise.new(function (resolve, reject) local function result_handler(event) local response = event.stanza; if response.attr.type == "result" and response.attr.from == stanza.attr.to and response.attr.id == stanza.attr.id then resolve(event); return true; end end local function error_handler(event) local response = event.stanza; if response.attr.type == "error" and response.attr.from == stanza.attr.to and response.attr.id == stanza.attr.id then reject(errors.from_stanza(event.stanza, event)); return true; end end if iq_cache:get(cache_key) then reject(errors.new({ type = "modify", condition = "conflict", text = "IQ stanza id attribute already used", })); return; end self:hook(result_event, result_handler, 1); self:hook(error_event, error_handler, 1); local timeout_handle = self:add_timer(timeout or 120, function () reject(errors.new({ type = "wait", condition = "remote-server-timeout", text = "IQ stanza timed out", })); end); local ok = iq_cache:set(cache_key, { reject = reject, resolve = resolve, timeout_handle = timeout_handle, result_handler = result_handler, error_handler = error_handler; }); if not ok then reject(errors.new({ type = "wait", condition = "internal-server-error", text = "Could not store IQ tracking data" })); return; end local wrapped_origin = setmetatable({ -- XXX Needed in some cases for replies to work correctly when sending queries internally. send = function (reply) if reply.name == stanza.name and reply.attr.id == stanza.attr.id then resolve({ stanza = reply }); end return (origin or hosts[self.host]).send(reply) end; }, { __index = origin or hosts[self.host]; }); self:send(stanza, wrapped_origin); end); p:finally(function () local iq = iq_cache:get(cache_key); if iq then self:unhook(result_event, iq.result_handler); self:unhook(error_event, iq.error_handler); iq.timeout_handle:stop(); iq_cache:set(cache_key, nil); end end); return p; end function api:broadcast(jids, stanza, iter) for jid in (iter or it.values)(jids) do local new_stanza = st.clone(stanza); new_stanza.attr.to = jid; self:send(new_stanza); end end local timer_methods = { } local timer_mt = { __index = timer_methods; } function timer_methods:stop( ) timer.stop(self.id); end timer_methods.disarm = timer_methods.stop function timer_methods:reschedule(delay) timer.reschedule(self.id, delay) end local function timer_callback(now, id, t) --luacheck: ignore 212/id if t.module_env.loaded == false then return; end return t.callback(now, unpack(t, 1, t.n)); end function api:add_timer(delay, callback, ...) local t = pack(...) t.module_env = self; t.callback = callback; t.id = timer.add_task(delay, timer_callback, t); return setmetatable(t, timer_mt); end function api:cron(task_spec) self:depends("cron"); self:add_item("task", task_spec); end function api:hourly(name, fun) if type(name) == "function" then fun, name = name, nil; end self:cron({ name = name; when = "hourly"; run = fun }); end function api:daily(name, fun) if type(name) == "function" then fun, name = name, nil; end self:cron({ name = name; when = "daily"; run = fun }); end function api:weekly(name, fun) if type(name) == "function" then fun, name = name, nil; end self:cron({ name = name; when = "weekly"; run = fun }); end local path_sep = package.config:sub(1,1); function api:get_directory() return self.resource_path or self.path and (self.path:gsub("%"..path_sep.."[^"..path_sep.."]*$", "")) or nil; end function api:load_resource(path, mode) path = resolve_relative_path(self:get_directory(), path); return io.open(path, mode); end function api:open_store(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 "prosody.core.statsmanager".measure; local fixed_label_key, fixed_label_value if self.host ~= "*" then fixed_label_key = "host" fixed_label_value = self.host end -- new_legacy_metric takes care of scoping for us, as it does not accept -- an array of labels -- the prosody_ prefix is automatically added by statsmanager for legacy -- metrics. self:add_item("measure", { name = name, type = stat_type, conf = conf }); return measure(stat_type, "mod_"..self.name.."/"..name, conf, fixed_label_key, fixed_label_value) end function api:metric(type_, name, unit, description, label_keys, conf) local metric = require "prosody.core.statsmanager".metric; local is_scoped = self.host ~= "*" label_keys = label_keys or {}; if is_scoped then -- prepend `host` label to label keys if this is not a global module local orig_labels = label_keys label_keys = array { "host" } label_keys:append(orig_labels) end local mf = metric(type_, "prosody_mod_"..self.name.."/"..name, unit, description, label_keys, conf) self:add_item("metric", { name = name, mf = mf }); if is_scoped then -- make sure to scope the returned metric family to the current host return mf:with_partial_label(self.host) end return mf end 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'", status_type); status_type, priority = "info", status_priorities.info; end local current_priority = status_priorities[self.status_type] or 0; -- By default an 'error' status can only be overwritten by another 'error' status if (current_priority >= status_priorities.error and priority < current_priority and override ~= true) or (override == false and current_priority > priority) then self:log("debug", "moduleapi: ignoring status [prio %d override %s]: %s", priority, override, status_message); return; end self.status_type, self.status_message, self.status_time = status_type, status_message, time_now(); self:fire_event("module-status/updated", { name = self.name }); end function api:log_status(level, msg, ...) self:set_status(level, format(msg, ...)); return self:log(level, msg, ...); end 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; prosody-13.0.1/core/PaxHeaders/modulemanager.lua0000644000000000000000000000011714773555365016653 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.527710279 prosody-13.0.1/core/modulemanager.lua0000644000175000017500000003470114773555365021057 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local array = require "prosody.util.array"; local logger = require "prosody.util.logger"; local log = logger.init("modulemanager"); 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 "prosody.core.features".available; 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 "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; local lua_version = _VERSION:match("5%.%d+$"); local autoload_modules = { "presence", "message", "iq", "offline", "c2s", "s2s", "s2s_auth_certs", }; local component_inheritable_modules = { "tls", "saslauth", "dialback", "iq", "s2s", "s2s_bidi", "smacks", "server_contact_info", }; -- We need this to let modules access the real global namespace local _G = _G; local _ENV = nil; -- luacheck: std none local loader = pluginloader.init({ load_filter_cb = function (path, content) local metadata = {}; for line in content:gmatch("([^\r\n]+)\r?\n") do local key, value = line:match("^%-%-%% *([%w_]+): *(.+)$"); if key then value = value:gsub("%s+$", ""); metadata[key] = value; end end if metadata.lua then local supported = false; for supported_lua_version in metadata.lua:gmatch("[^, ]+") do if supported_lua_version == lua_version then supported = true; break; end end if not supported then log("warn", "Not loading module, we have Lua %s but the module requires one of (%s): %s", lua_version, metadata.lua, path); return; -- Don't load this module end end if metadata.conflicts then local conflicts_features = set.new(array.collect(metadata.conflicts:gmatch("[^, ]+"))); local conflicted_features = set.intersection(conflicts_features, core_features); if not conflicted_features:empty() then log("warn", "Not loading module, due to conflicting features '%s': %s", conflicted_features, path); return; -- Don't load this module end end if metadata.requires then local required_features = set.new(array.collect(metadata.requires:gmatch("[^, ]+"))); local missing_features = required_features - core_features; if not missing_features:empty() then log("warn", "Not loading module, due to missing features '%s': %s", missing_features, path); return; -- Don't load this module end end return path, content, metadata; end; }); local load_modules_for_host, load, unload, reload, get_module, get_items; local get_modules, is_loaded, module_has_method, call_module_method; -- [host] = { [module] = module_env } local modulemap = { ["*"] = {} }; -- Get the list of modules to be loaded on a host local function get_modules_for_host(host) local component = config.get(host, "component_module"); local global_modules_enabled = config.get("*", "modules_enabled"); local global_modules_disabled = config.get("*", "modules_disabled"); local host_modules_enabled = config.get(host, "modules_enabled"); local host_modules_disabled = config.get(host, "modules_disabled"); if host_modules_enabled == global_modules_enabled then host_modules_enabled = nil; end if host_modules_disabled == global_modules_disabled then host_modules_disabled = nil; end local global_modules = set.new(autoload_modules) + set.new(global_modules_enabled) - set.new(global_modules_disabled); if component then global_modules = set.intersection(set.new(component_inheritable_modules), global_modules); end local modules = (global_modules + set.new(host_modules_enabled)) - set.new(host_modules_disabled); if modules:contains("vcard") and modules:contains("vcard_legacy") then log("error", "The mod_vcard_legacy plugin replaces mod_vcard but both are enabled. Please update your config."); modules:remove("vcard"); end return modules, component; end -- Load modules when a host is activated function load_modules_for_host(host) local modules, component_module = get_modules_for_host(host); -- Ensure component module is loaded first if component_module then load(host, component_module); end for module in modules do load(host, module); end end prosody.events.add_handler("host-activated", load_modules_for_host); prosody.events.add_handler("host-deactivated", function (host) modulemap[host] = nil; end); --- Private helpers --- local function do_unload_module(host, name) local mod = get_module(host, name); if not mod then return nil, "module-not-loaded"; end if module_has_method(mod, "unload") then local ok, err = call_module_method(mod, "unload"); if (not ok) and err then log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err); end end for object, event, handler in mod.module.event_handlers:iter(nil, nil, nil) do object.remove_handler(event, handler); end if mod.module.items then -- remove items local events = (host == "*" and prosody.events) or hosts[host].events; for key,t in pairs(mod.module.items) do for i = #t,1,-1 do local value = t[i]; t[i] = nil; events.fire_event("item-removed/"..key, {source = mod.module, item = value}); end end end mod.module.loaded = false; modulemap[host][name] = nil; return true; end local function do_load_module(host, module_name, state) if not (host and module_name) then return nil, "insufficient-parameters"; elseif not hosts[host] and host ~= "*"then return nil, "unknown-host"; end if not modulemap[host] then modulemap[host] = hosts[host].modules; end if modulemap[host][module_name] then if not modulemap["*"][module_name] then log("debug", "%s is already loaded for %s, so not loading again", module_name, host); end return nil, "module-already-loaded"; elseif modulemap["*"][module_name] then local mod = modulemap["*"][module_name]; if module_has_method(mod, "add_host") then local _log = logger.init(host..":"..module_name); local host_module_api = setmetatable({ global = false, host = host, event_handlers = new_multitable(), items = {}; _log = _log, log = function (self, ...) return _log(...); end; --luacheck: ignore 212/self },{ __index = modulemap["*"][module_name].module; }); local host_module = setmetatable({ module = host_module_api }, { __index = mod }); host_module_api.environment = host_module; modulemap[host][module_name] = host_module; local ok, result, module_err = call_module_method(mod, "add_host", host_module_api); if not ok or result == false then modulemap[host][module_name] = nil; return nil, ok and module_err or result; end return host_module; end return nil, "global-module-already-loaded"; end local _log = logger.init(host..":"..module_name); local api_instance = setmetatable({ name = module_name, host = host, _log = _log, log = function (self, ...) return _log(...); end, --luacheck: ignore 212/self event_handlers = new_multitable(), reloading = not not state, saved_state = state~=true and state or nil } , { __index = api }); local pluginenv = setmetatable({ module = api_instance }, { __index = _G }); api_instance.environment = pluginenv; local mod, err, meta = loader:load_code(module_name, nil, pluginenv); if not mod then log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil"); api_instance:set_status("error", "Failed to load (see log)"); return nil, err; end api_instance.path = err; api_instance.meta = meta; local custom_plugins = prosody.paths.installer; if custom_plugins and err:sub(1, #custom_plugins+1) == custom_plugins.."/" then -- Stage 1: Make it work (you are here) -- Stage 2: Make it less hacky (TODO) local manifest = {}; local luarocks_path = custom_plugins.."/lib/luarocks/rocks-"..lua_version; local manifest_filename = luarocks_path.."/manifest"; local load_manifest, err = envload.envloadfile(manifest_filename, manifest); if not load_manifest then -- COMPAT Luarocks 2.x log("debug", "Could not load LuaRocks 3.x manifest, trying 2.x", err); luarocks_path = custom_plugins.."/lib/luarocks/rocks"; manifest_filename = luarocks_path.."/manifest"; load_manifest, err = envload.envloadfile(manifest_filename, manifest); end if not load_manifest then log("error", "Could not load manifest of installed plugins: %s", err, load_manifest); else local ok, err = xpcall(load_manifest, debug_traceback); if not ok then log("error", "Could not load manifest of installed plugins: %s", err); elseif type(manifest.modules) ~= "table" then log("debug", "Expected 'table' but manifest.modules = %q", manifest.modules); log("error", "Can't look up resource path for mod_%s because '%s' does not appear to be a LuaRocks manifest", module_name, manifest_filename); else local versions = manifest.modules["mod_"..module_name]; if type(versions) == "table" and versions[1] then -- Not going to deal with multiple installed versions api_instance.resource_path = luarocks_path.."/"..versions[1]; else log("debug", "mod_%s does not appear in the installation manifest", module_name); end end end end modulemap[host][module_name] = pluginenv; local ok, err = xpcall(mod, debug_traceback); if ok then -- Call module's "load" if module_has_method(pluginenv, "load") then ok, err = call_module_method(pluginenv, "load"); if not ok then log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err or "nil"); api_instance:set_status("warn", "Error during load (see log)"); end end api_instance.reloading, api_instance.saved_state = nil, nil; if api_instance.host == "*" then if not api_instance.global then -- COMPAT w/pre-0.9 if host ~= "*" then log("warn", "mod_%s: Setting module.host = '*' deprecated, call module:set_global() instead", module_name); end api_instance:set_global(); end modulemap[host][module_name] = nil; modulemap[api_instance.host][module_name] = pluginenv; if host ~= api_instance.host and module_has_method(pluginenv, "add_host") then -- Now load the module again onto the host it was originally being loaded on 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; log("error", "Error initializing module '%s' on '%s': %s", module_name, host, err or "nil"); api_instance:set_status("warn", "Error during load (see log)"); else api_instance:set_status("core", "Loaded", false); end return ok and pluginenv, err; end local function do_reload_module(host, name) local mod = get_module(host, name); if not mod then return nil, "module-not-loaded"; end local _mod, err = loader:load_code(name); -- checking for syntax errors if not _mod then log("error", "Unable to load module '%s': %s", name or "nil", err or "nil"); return nil, err; end local saved; if module_has_method(mod, "save") then -- FIXME What goes in 'err' here? local ok, ret, err = call_module_method(mod, "save"); -- luacheck: ignore 211/err if ok then saved = ret; else log("warn", "Error saving module '%s:%s' state: %s", host, name, ret); if not config.get(host, "force_module_reload") then log("warn", "Aborting reload due to error, set force_module_reload to ignore this"); return nil, "save-state-failed"; else log("warn", "Continuing with reload (using the force)"); end end end mod.module.reloading = true; do_unload_module(host, name); local ok, err = do_load_module(host, name, saved or true); if ok then mod = get_module(host, name); if module_has_method(mod, "restore") then local ok, err = call_module_method(mod, "restore", saved or {}) if (not ok) and err then log("warn", "Error restoring module '%s' from '%s': %s", name, host, err); end end end return ok and mod, err; end --- Public API --- -- Load a module and fire module-loaded event function load(host, name) local mod, err = do_load_module(host, name); if mod then (hosts[mod.module.host] or prosody).events.fire_event("module-loaded", { module = name, host = mod.module.host }); end return mod, err; end -- Unload a module and fire module-unloaded function unload(host, name) local ok, err = do_unload_module(host, name); if ok then (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host }); end return ok, err; end function reload(host, name) local mod, err = do_reload_module(host, name); if mod then modulemap[host][name].module.reloading = true; (hosts[host] or prosody).events.fire_event("module-reloaded", { module = name, host = host }); mod.module.reloading = nil; elseif not is_loaded(host, name) then (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host }); end return mod, err; end function get_module(host, name) return modulemap[host] and modulemap[host][name]; end function get_items(key, host) local result = {}; local modules = modulemap[host]; if not key or not host or not modules then return nil; end for _, module in pairs(modules) do local mod = module.module; if mod.items and mod.items[key] then for _, value in ipairs(mod.items[key]) do t_insert(result, value); end end end return result; end function get_modules(host) return modulemap[host]; end function is_loaded(host, name) return modulemap[host] and modulemap[host][name] and true; end function module_has_method(module, method) return type(rawget(module.module, method)) == "function"; end function call_module_method(module, method, ...) local f = rawget(module.module, method); if type(f) == "function" then return xpcall(f, debug_traceback, ...); else return false, "no-such-method"; end end return { get_modules_for_host = get_modules_for_host; load_modules_for_host = load_modules_for_host; load = load; unload = unload; reload = reload; get_module = get_module; get_items = get_items; get_modules = get_modules; is_loaded = is_loaded; module_has_method = module_has_method; call_module_method = call_module_method; loader = loader; }; prosody-13.0.1/core/PaxHeaders/portmanager.lua0000644000000000000000000000011714773555365016352 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.531710295 prosody-13.0.1/core/portmanager.lua0000644000175000017500000002736714773555365020570 0ustar00prosodyprosody00000000000000local config = require "prosody.core.configmanager"; local certmanager = require "prosody.core.certmanager"; local server = require "prosody.net.server"; local socket = require "socket"; 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; local type, tonumber, ipairs = type, tonumber, ipairs; local pairs = pairs; local prosody = prosody; local fire_event = prosody.events.fire_event; local _ENV = nil; -- luacheck: std none --- Config local default_interfaces = { }; local default_local_interfaces = { }; if config.get("*", "use_ipv4") ~= false then table.insert(default_interfaces, "*"); table.insert(default_local_interfaces, "127.0.0.1"); end if socket.tcp6 and config.get("*", "use_ipv6") ~= false then table.insert(default_interfaces, "::"); table.insert(default_local_interfaces, "::1"); end local default_mode = config.get("*", "network_default_read_size") or 4096; --- Private state -- service_name -> { service_info, ... } local services = setmetatable({}, { __index = function (t, k) rawset(t, k, {}); return rawget(t, k); end }); -- service_name, interface (string), port (number) local active_services = multitable.new(); --- Private helpers local function error_to_friendly_message(service_name, port, err) --luacheck: ignore 212/service_name local friendly_message = err; 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 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"; else friendly_message = "this port is in use by another application"; end elseif err:match("permission") then friendly_message = "Prosody does not have sufficient privileges to use this port"; end return friendly_message; end local function get_port_ssl_ctx(port, interface, config_prefix, service_info) local global_ssl_config = config.get("*", "ssl") or {}; local prefix_ssl_config = config.get("*", config_prefix.."ssl") or global_ssl_config; log("debug", "Creating context for direct TLS service %s on port %d", service_info.name, port); local ssl, err, cfg = certmanager.create_context(service_info.name.." port "..port, "server", prefix_ssl_config[interface], prefix_ssl_config[port], prefix_ssl_config, service_info.ssl_config or {}, global_ssl_config[interface], global_ssl_config[port]); return ssl, cfg, err; end --- Public API local function activate(service_name) local service_info = services[service_name][1]; if not service_info then return nil, "Unknown service: "..service_name; end local listener = service_info.listener; local config_prefix = (service_info.config_prefix or service_name).."_"; if config_prefix == "_" then config_prefix = ""; end local bind_interfaces = config.get("*", config_prefix.."interfaces") or config.get("*", config_prefix.."interface") -- COMPAT w/pre-0.9 or (service_info.private and (config.get("*", "local_interfaces") or default_local_interfaces)) or config.get("*", "interfaces") or config.get("*", "interface") -- COMPAT w/pre-0.9 or listener.default_interface -- COMPAT w/pre0.9 or default_interfaces bind_interfaces = set.new(type(bind_interfaces)~="table" and {bind_interfaces} or bind_interfaces); local bind_ports = config.get("*", config_prefix.."ports") or service_info.default_ports or {service_info.default_port or listener.default_port -- COMPAT w/pre-0.9 } bind_ports = set.new(type(bind_ports) ~= "table" and { bind_ports } or bind_ports ); local mode = listener.default_mode or default_mode; local hooked_ports = {}; for interface in bind_interfaces do for port in bind_ports do local port_number = tonumber(port); if not port_number then log("error", "Invalid port number specified for service '%s': %s", service_info.name, port); elseif #active_services:search(nil, interface, port_number) > 0 then log("error", "Multiple services configured to listen on the same port ([%s]:%d): %s, %s", interface, port, active_services:search(nil, interface, port)[1][1].service.name or "", service_name or ""); else local ssl, cfg, err; -- Create SSL context for this service/port if service_info.encryption == "ssl" then ssl, cfg, err = get_port_ssl_ctx(port, interface, config_prefix, service_info); if not ssl then log("error", "Error binding encrypted port for %s: %s", service_info.name, error_to_friendly_message(service_name, port_number, err) or "unknown error"); end end if not err then -- Start listening on interface+port local handler, err = server.listen(interface, port_number, listener, { read_size = mode, tls_ctx = ssl, tls_direct = service_info.encryption == "ssl"; sni_hosts = {}, }); if not handler then log("error", "Failed to open server port %d on %s, %s", port_number, interface, error_to_friendly_message(service_name, port_number, err)); else table.insert(hooked_ports, "["..interface.."]:"..port_number); log("debug", "Added listening service %s to [%s]:%d", service_name, interface, port_number); active_services:add(service_name, interface, port_number, { server = handler; service = service_info; tls_cfg = cfg; }); end end end end end log("info", "Activated service '%s' on %s", service_name, #hooked_ports == 0 and "no ports" or table.concat(hooked_ports, ", ")); return true; end local close; -- forward declaration local function deactivate(service_name, service_info) for name, interface, port, n, active_service --luacheck: ignore 213/name 213/n in active_services:iter(service_name or service_info and service_info.name, nil, nil, nil) do if service_info == nil or active_service.service == service_info then close(interface, port); end end log("info", "Deactivated service '%s'", service_name or service_info.name); end local function register_service(service_name, service_info) table.insert(services[service_name], service_info); if not active_services:get(service_name) and prosody.process_type == "prosody" then log("debug", "No active service for %s, activating...", service_name); local ok, err = activate(service_name); if not ok then log("error", "Failed to activate service '%s': %s", service_name, err or "unknown error"); end end fire_event("service-added", { name = service_name, service = service_info }); return true; end local function unregister_service(service_name, service_info) log("debug", "Unregistering service: %s", service_name); local service_info_list = services[service_name]; for i, service in ipairs(service_info_list) do if service == service_info then table.remove(service_info_list, i); end end deactivate(nil, service_info); if #service_info_list > 0 then -- Other services registered with this name activate(service_name); -- Re-activate with the next available one end fire_event("service-removed", { name = service_name, service = service_info }); end local get_service_at -- forward declaration function close(interface, port) local service, service_server = get_service_at(interface, port); if not service then return false, "port-not-open"; end service_server:close(); active_services:remove(service.name, interface, port); log("debug", "Removed listening service %s from [%s]:%d", service.name, interface, port); return true; end function get_service_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.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 local function get_active_services() return active_services; end local function get_registered_services() return services; end -- Event handlers 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 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"); 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); local ssl, err, cfg = certmanager.create_context(alternate_host or host, "server", prefix_ssl_config, autocert, active_service.tls_cfg); if not ssl then log("error", "Error creating TLS context for SNI host %s: %s", host, err); else log("debug", "Using certificate %s for %s (%s) on %s (%s)", cfg.certificate, service or name, name, alternate_host or host, host) local ok, err = active_service.server:sslctx():set_sni_host( alternate_host or host, cfg.certificate, cfg.key ); if not ok then log("error", "Error creating TLS context for SNI host %s: %s", host, err); end end end end end prosody.events.add_handler("item-added/net-provider", function (event) local item = event.item; register_service(item.name, item); for host in pairs(prosody.hosts) do add_sni_host(host, item.name); end end); prosody.events.add_handler("item-removed/net-provider", function (event) local item = event.item; unregister_service(item.name, item); end); prosody.events.add_handler("host-activated", add_sni_host); 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:sslctx():remove_sni_host(host) end end end); prosody.events.add_handler("config-reloaded", function () for service_name, interface, port, _, active_service in active_services:iter(nil, nil, nil, nil) do if active_service.tls_cfg then local service_info = active_service.service; local config_prefix = (service_info.config_prefix or service_name).."_"; if config_prefix == "_" then config_prefix = ""; end local ssl, cfg, err = get_port_ssl_ctx(port, interface, config_prefix, service_info); if ssl then active_service.server:set_sslctx(ssl); active_service.tls_cfg = cfg; else log("error", "Error reloading certificate for encrypted port for %s: %s", service_info.name, error_to_friendly_message(service_name, port, err) or "unknown error"); end end end for host in pairs(prosody.hosts) do add_sni_host(host, nil); end end, -1); return { activate = activate; deactivate = deactivate; register_service = register_service; 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; }; prosody-13.0.1/core/PaxHeaders/rostermanager.lua0000644000000000000000000000011714773555365016704 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.531710295 prosody-13.0.1/core/rostermanager.lua0000644000175000017500000003364014773555365021111 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: globals prosody.bare_sessions.?.roster local log = require "prosody.util.logger".init("rostermanager"); local new_id = require "prosody.util.id".short; local new_cache = require "prosody.util.cache".new; local pairs = pairs; local tostring = tostring; local type = type; local hosts = prosody.hosts; local bare_sessions = prosody.bare_sessions; 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 local save_roster; -- forward declaration local function add_to_roster(session, jid, item) if session.roster then local old_item = session.roster[jid]; session.roster[jid] = item; if save_roster(session.username, session.host, nil, jid) then return true; else session.roster[jid] = old_item; return nil, "wait", "internal-server-error", "Unable to save roster"; end else return nil, "auth", "not-authorized", "Session's roster not loaded"; end end local function remove_from_roster(session, jid) if session.roster then local old_item = session.roster[jid]; session.roster[jid] = nil; if save_roster(session.username, session.host, nil, jid) then return true; else session.roster[jid] = old_item; return nil, "wait", "internal-server-error", "Unable to save roster"; end else return nil, "auth", "not-authorized", "Session's roster not loaded"; end end local function roster_push(username, host, jid) local roster = jid and hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster; if roster then local item = hosts[host].sessions[username].roster[jid]; local stanza = st.iq({type="set", id=new_id()}); stanza:tag("query", {xmlns = "jabber:iq:roster", ver = tostring(roster[false].version or "1") }); if item then stanza:tag("item", {jid = jid, subscription = item.subscription, name = item.name, ask = item.ask}); for group in pairs(item.groups) do stanza:tag("group"):text(group):up(); end else stanza:tag("item", {jid = jid, subscription = "remove"}); end stanza:up(); -- move out from item stanza:up(); -- move out from stanza -- stanza ready for _, session in pairs(hosts[host].sessions[username].sessions) do if session.interested then session.send(stanza); end end end end local function roster_metadata(roster, err) local metadata = roster[false]; if not metadata then metadata = { broken = err or nil }; roster[false] = metadata; end if roster.pending and type(roster.pending.subscription) ~= "string" then metadata.pending = roster.pending; roster.pending = nil; elseif not metadata.pending then metadata.pending = {}; end return metadata; end local function load_roster(username, host) local jid = username.."@"..host; log("debug", "load_roster: asked for: %s", jid); local user = bare_sessions[jid]; local roster; if user then roster = user.roster; if roster then return roster; end log("debug", "load_roster: loading for new user: %s", jid); else -- Attempt to load roster for non-loaded user log("debug", "load_roster: loading for offline user: %s", jid); end local roster_cache = hosts[host] and hosts[host].roster_cache; if not roster_cache then if hosts[host] then roster_cache = new_cache(1024); hosts[host].roster_cache = roster_cache; end else roster = roster_cache:get(jid); if roster then log("debug", "load_roster: cache hit"); roster_cache:set(jid, roster); if user then user.roster = roster; end return roster; else log("debug", "load_roster: cache miss, loading from storage"); end end local roster_store = storagemanager.open(host, "roster", "keyval"); local data, err = roster_store:get(username); roster = data or {}; if user then user.roster = roster; end local legacy_pending = roster.pending and type(roster.pending.subscription) ~= "string"; roster_metadata(roster, err); if legacy_pending then -- Due to map store use, we need to manually delete this entry log("debug", "Removing legacy 'pending' entry"); if not save_roster(username, host, roster, "pending") then log("warn", "Could not remove legacy 'pending' entry"); end end if roster[jid] then roster[jid] = nil; log("debug", "Roster for %s had a self-contact, removing", jid); if not save_roster(username, host, roster, jid) then log("warn", "Could not remove self-contact from roster for %s", jid); end end if not err then hosts[host].events.fire_event("roster-load", { username = username, host = host, roster = roster }); end if roster_cache and not user then log("debug", "load_roster: caching loaded roster"); roster_cache:set(jid, roster); end return roster, err; end function save_roster(username, host, roster, jid) if not um_user_exists(username, host) then log("debug", "not saving roster for %s@%s: the user doesn't exist", username, host); return nil; end log("debug", "save_roster: saving roster for %s@%s, (%s)", username, host, jid or "all contacts"); if not roster then roster = hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster; --if not roster then -- --roster = load_roster(username, host); -- return true; -- roster unchanged, no reason to save --end end if roster then local metadata = roster_metadata(roster); if metadata.version ~= true then metadata.version = (metadata.version or 0) + 1; end if metadata.broken then return nil, "Not saving broken roster" end if jid == nil then local roster_store = storagemanager.open(host, "roster", "keyval"); return roster_store:set(username, roster); else local roster_store = storagemanager.open(host, "roster", "map"); return roster_store:set_keys(username, { [false] = metadata, [jid] = roster[jid] or roster_store.remove }); end end log("warn", "save_roster: user had no roster to save"); return nil; end local function process_inbound_subscription_approval(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; if item and item.ask then if item.subscription == "none" then item.subscription = "to"; else -- subscription == from item.subscription = "both"; end item.ask = nil; return save_roster(username, host, roster, jid); end end local is_contact_pending_out -- forward declaration local function process_inbound_subscription_cancellation(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; local changed = nil; if is_contact_pending_out(username, host, jid) then item.ask = nil; changed = true; end if item then if item.subscription == "to" then item.subscription = "none"; changed = true; elseif item.subscription == "both" then item.subscription = "from"; changed = true; end end if changed then return save_roster(username, host, roster, jid); end end local is_contact_pending_in -- forward declaration local function process_inbound_unsubscribe(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; local changed = nil; if is_contact_pending_in(username, host, jid) then roster[false].pending[jid] = nil; changed = true; end if item then if item.subscription == "from" then item.subscription = "none"; changed = true; elseif item.subscription == "both" then item.subscription = "to"; changed = true; end end if changed then return save_roster(username, host, roster, jid); end end local function _get_online_roster_subscription(jidA, jidB) local user = bare_sessions[jidA]; local item = user and (user.roster[jidB] or { subscription = "none" }); return item and item.subscription; end local function is_contact_subscribed(username, host, jid) do local selfjid = username.."@"..host; local user_subscription = _get_online_roster_subscription(selfjid, jid); if user_subscription then return (user_subscription == "both" or user_subscription == "from"); end local contact_subscription = _get_online_roster_subscription(jid, selfjid); if contact_subscription then return (contact_subscription == "both" or contact_subscription == "to"); end end local roster, err = load_roster(username, host); local item = roster[jid]; return item and (item.subscription == "from" or item.subscription == "both"), err; end local function is_user_subscribed(username, host, jid) do local selfjid = username.."@"..host; local user_subscription = _get_online_roster_subscription(selfjid, jid); if user_subscription then return (user_subscription == "both" or user_subscription == "to"); end local contact_subscription = _get_online_roster_subscription(jid, selfjid); if contact_subscription then return (contact_subscription == "both" or contact_subscription == "from"); end end local roster, err = load_roster(username, host); local item = roster[jid]; return item and (item.subscription == "to" or item.subscription == "both"), err; end function is_contact_pending_in(username, host, jid) local roster = load_roster(username, host); return roster[false].pending[jid] ~= nil; end local function set_contact_pending_in(username, host, jid, stanza) local roster = load_roster(username, host); local item = roster[jid]; if item and (item.subscription == "from" or item.subscription == "both") then return; -- false end roster[false].pending[jid] = st.is_stanza(stanza) and st.preserialize(stanza) or true; return save_roster(username, host, roster, jid); end function is_contact_pending_out(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; return item and item.ask; end local function is_contact_preapproved(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; return item and (item.approved == "true"); end local function set_contact_pending_out(username, host, jid) -- subscribe local roster = load_roster(username, host); local item = roster[jid]; if item and (item.ask or item.subscription == "to" or item.subscription == "both") then return true; end if not item then item = {subscription = "none", groups = {}}; roster[jid] = item; end item.ask = "subscribe"; log("debug", "set_contact_pending_out: saving roster; set %s@%s.roster[%q].ask=subscribe", username, host, jid); return save_roster(username, host, roster, jid); end local function unsubscribe(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; if not item then return false; end if (item.subscription == "from" or item.subscription == "none") and not item.ask then return true; end item.ask = nil; if item.subscription == "both" then item.subscription = "from"; elseif item.subscription == "to" then item.subscription = "none"; end return save_roster(username, host, roster, jid); end local function subscribed(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; if is_contact_pending_in(username, host, jid) then if not item then -- FIXME should roster item be auto-created? item = {subscription = "none", groups = {}}; roster[jid] = item; end if item.subscription == "none" then item.subscription = "from"; else -- subscription == to item.subscription = "both"; end roster[false].pending[jid] = nil; return save_roster(username, host, roster, jid); elseif not item or item.subscription == "none" or item.subscription == "to" then -- Contact is not subscribed and has not sent a subscription request. -- We store a pre-approval as per RFC6121 3.4 if not item then item = {subscription = "none", groups = {}}; roster[jid] = item; end item.approved = "true"; log("debug", "Storing preapproval for %s", jid); return save_roster(username, host, roster, jid); end end local function unsubscribed(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; local pending = is_contact_pending_in(username, host, jid); if pending then roster[false].pending[jid] = nil; end local is_subscribed; if item then if item.subscription == "from" then item.subscription = "none"; is_subscribed = true; elseif item.subscription == "both" then item.subscription = "to"; is_subscribed = true; end end local success = (pending or is_subscribed) and save_roster(username, host, roster, jid); return success, pending, is_subscribed; end local function process_outbound_subscription_request(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; if item and (item.subscription == "none" or item.subscription == "from") then item.ask = "subscribe"; return save_roster(username, host, roster, jid); end end --[[function process_outbound_subscription_approval(username, host, jid) local roster = load_roster(username, host); local item = roster[jid]; if item and (item.subscription == "none" or item.subscription == "from" then item.ask = "subscribe"; return save_roster(username, host, roster); end end]] return { add_to_roster = add_to_roster; remove_from_roster = remove_from_roster; roster_push = roster_push; load_roster = load_roster; save_roster = save_roster; process_inbound_subscription_approval = process_inbound_subscription_approval; process_inbound_subscription_cancellation = process_inbound_subscription_cancellation; process_inbound_unsubscribe = process_inbound_unsubscribe; is_contact_subscribed = is_contact_subscribed; is_user_subscribed = is_user_subscribed; is_contact_pending_in = is_contact_pending_in; set_contact_pending_in = set_contact_pending_in; is_contact_pending_out = is_contact_pending_out; set_contact_pending_out = set_contact_pending_out; is_contact_preapproved = is_contact_preapproved; unsubscribe = unsubscribe; subscribed = subscribed; unsubscribed = unsubscribed; process_outbound_subscription_request = process_outbound_subscription_request; }; prosody-13.0.1/core/PaxHeaders/s2smanager.lua0000644000000000000000000000011714773555365016075 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.531710295 prosody-13.0.1/core/s2smanager.lua0000644000175000017500000001002714773555365020274 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local hosts = prosody.hosts; local pairs, setmetatable = pairs, setmetatable; local logger_init = require "prosody.util.logger".init; local sessionlib = require "prosody.util.session"; local log = logger_init("s2smanager"); local prosody = _G.prosody; local incoming_s2s = {}; _G.incoming_s2s = incoming_s2s; prosody.incoming_s2s = incoming_s2s; local fire_event = prosody.events.fire_event; local _ENV = nil; -- luacheck: std none local function new_incoming(conn) local host_session = sessionlib.new("s2sin"); sessionlib.set_id(host_session); sessionlib.set_logger(host_session); sessionlib.set_conn(host_session, conn); host_session.direction = "incoming"; host_session.incoming = true; host_session.hosts = {}; incoming_s2s[host_session] = true; return host_session; end local function new_outgoing(from_host, to_host) local host_session = sessionlib.new("s2sout"); sessionlib.set_id(host_session); sessionlib.set_logger(host_session); host_session.to_host = to_host; host_session.from_host = from_host; host_session.host = from_host; host_session.notopen = true; host_session.direction = "outgoing"; host_session.outgoing = true; host_session.hosts = {}; hosts[from_host].s2sout[to_host] = host_session; return host_session; end local resting_session = { -- Resting, not dead destroyed = true; type = "s2s_destroyed"; direction = "destroyed"; open_stream = function (session) session.log("debug", "Attempt to open stream on resting session"); end; close = function (session) session.log("debug", "Attempt to close already-closed session"); end; reset_stream = function (session) session.log("debug", "Attempt to reset stream of already-closed session"); end; filter = function (type, data) return data; end; --luacheck: ignore 212/type }; resting_session.__index = resting_session; local function retire_session(session, reason) local log = session.log or log; --luacheck: ignore 431/log for k in pairs(session) do if k ~= "log" and k ~= "id" and k ~= "conn" then session[k] = nil; end end session.destruction_reason = reason; function session.send(data) log("debug", "Discarding data sent to resting session: %s", data); end function session.data(data) log("debug", "Discarding data received from resting session: %s", data); end session.thread = { run = function (_, data) return session.data(data) end }; session.sends2s = session.send; return setmetatable(session, resting_session); end local function destroy_session(session, reason, bounce_reason) if session.destroyed then return; end local log = session.log or log; log("debug", "Destroying %s session %s->%s%s%s", session.direction, session.from_host, session.to_host, reason and ": " or "", reason or ""); if session.direction == "outgoing" then hosts[session.from_host].s2sout[session.to_host] = nil; session:bounce_sendq(bounce_reason or reason); elseif session.direction == "incoming" then if session.outgoing and hosts[session.to_host].s2sout[session.from_host] == session then hosts[session.to_host].s2sout[session.from_host] = nil; end incoming_s2s[session] = nil; end local event_data = { session = session, reason = reason }; fire_event("s2s-destroyed", event_data); if session.type == "s2sout" then fire_event("s2sout-destroyed", event_data); if hosts[session.from_host] then hosts[session.from_host].events.fire_event("s2sout-destroyed", event_data); end elseif session.type == "s2sin" then fire_event("s2sin-destroyed", event_data); if hosts[session.to_host] then hosts[session.to_host].events.fire_event("s2sin-destroyed", event_data); end end retire_session(session, reason); -- Clean session until it is GC'd return true; end return { incoming_s2s = incoming_s2s; new_incoming = new_incoming; new_outgoing = new_outgoing; retire_session = retire_session; destroy_session = destroy_session; }; prosody-13.0.1/core/PaxHeaders/sessionmanager.lua0000644000000000000000000000011714773555365017051 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.531710295 prosody-13.0.1/core/sessionmanager.lua0000644000175000017500000002612614773555365021257 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: globals prosody.full_sessions prosody.bare_sessions local tostring, setmetatable = tostring, setmetatable; local pairs, next= pairs, next; local prosody, hosts = prosody, prosody.hosts; local full_sessions = prosody.full_sessions; local bare_sessions = prosody.bare_sessions; local logger = require "prosody.util.logger"; local log = logger.init("sessionmanager"); 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; -- luacheck: std none local function new_session(conn) local session = sessionlib.new("c2s"); sessionlib.set_id(session); sessionlib.set_logger(session); sessionlib.set_conn(session, conn); session.conntime = gettime(); local filter = initialize_filters(session); local w = conn.write; function session.rawsend(t) t = filter("bytes/out", tostring(t)); if t then local ret, err = w(conn, t); if not ret then session.log("debug", "Error writing to connection: %s", err); return false, err; end end return true; end session.send = function (t) session.log("debug", "Sending[%s]: %s", session.type, t.top_tag and t:top_tag() or t:match("^[^>]*>?")); if t.name then t = filter("stanzas/out", t); end if t then return session.rawsend(t); end return true; end session.ip = conn:ip(); local conn_name = "c2s"..tostring(session):match("[a-f0-9]+$"); session.log = logger.init(conn_name); return session; end local resting_session = { -- Resting, not dead destroyed = true; type = "c2s_destroyed"; close = function (session) session.log("debug", "Attempt to close already-closed session"); end; filter = function (type, data) return data; end; --luacheck: ignore 212/type }; resting_session.__index = resting_session; local function retire_session(session) local log = session.log or log; --luacheck: ignore 431/log for k in pairs(session) do if k ~= "log" and k ~= "id" then session[k] = nil; end end function session.send(data) log("debug", "Discarding data sent to resting session: %s", data); return false; end function session.rawsend(data) log("debug", "Discarding data sent to resting session: %s", data); return false; end function session.data(data) log("debug", "Discarding data received from resting session: %s", data); end session.thread = { run = function (_, data) return session.data(data) end }; 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 -- Remove session/resource from user's session list if session.full_jid then local host_session = hosts[session.host]; -- Allow plugins to prevent session destruction if host_session.events.fire_event("pre-resource-unbind", {session=session, error=err}) then (session.log or log)("debug", "Resource unbind prevented by module"); return; end (session.log or log)("debug", "Unbinding resource for %s (%s@%s)%s", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)", err and (": "..err) or ""); session.destroyed = true; -- Past this point the session is DOOMED! host_session.sessions[session.username].sessions[session.resource] = nil; full_sessions[session.full_jid] = nil; if not next(host_session.sessions[session.username].sessions) then log("debug", "All resources of %s are now offline", session.username); host_session.sessions[session.username] = nil; bare_sessions[session.username..'@'..session.host] = nil; end host_session.events.fire_event("resource-unbind", {session=session, error=err}); else (session.log or log)("debug", "Destroying unbound session for <%s@%s>%s", session.username or "(unknown)", session.host or "(unknown)", err and (": "..err) or ""); end retire_session(session); end 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 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 -- returns true, nil on success -- returns nil, err_type, err, err_message on failure local function bind_resource(session, resource) if not session.username then return nil, "auth", "not-authorized", "Cannot bind resource before authentication"; end if session.resource then return nil, "cancel", "not-allowed", "Cannot bind multiple resources on a single connection"; end -- We don't support binding multiple resources local event_payload = { session = session, resource = resource }; if hosts[session.host].events.fire_event("pre-resource-bind", event_payload) == false then local err = event_payload.error; if err then return nil, err.type, err.condition, err.text; end return nil, "cancel", "not-allowed"; else -- In case a plugin wants to poke at it resource = event_payload.resource; end resource = resourceprep(resource or "", true); resource = resource ~= "" and resource or generate_identifier(); --FIXME: Randomly-generated resources must be unique per-user, and never conflict with existing if not hosts[session.host].sessions[session.username] then local sessions = { sessions = {} }; hosts[session.host].sessions[session.username] = sessions; bare_sessions[session.username..'@'..session.host] = sessions; else local sessions = hosts[session.host].sessions[session.username].sessions; if sessions[resource] then -- Resource conflict local policy = config_get(session.host, "conflict_resolve"); local increment; if policy == "random" then resource = generate_identifier(); increment = true; elseif policy == "increment" then increment = true; -- TODO ping old resource elseif policy == "kick_new" then return nil, "cancel", "conflict", "Resource already exists"; else -- if policy == "kick_old" then sessions[resource]:close { condition = "conflict"; text = "Replaced by new connection"; }; if not next(sessions) then hosts[session.host].sessions[session.username] = { sessions = sessions }; bare_sessions[session.username.."@"..session.host] = hosts[session.host].sessions[session.username]; end end if increment and sessions[resource] then local count = 1; while sessions[resource.."#"..count] do count = count + 1; end resource = resource.."#"..count; end end end session.resource = resource; session.full_jid = session.username .. '@' .. session.host .. '/' .. resource; hosts[session.host].sessions[session.username].sessions[resource] = session; full_sessions[session.full_jid] = session; if session.type == "c2s_unbound" then session.type = "c2s"; end local err; session.roster, err = rm_load_roster(session.username, session.host); if err then -- FIXME: Why is all this rollback down here, instead of just doing the roster test up above? full_sessions[session.full_jid] = nil; hosts[session.host].sessions[session.username].sessions[resource] = nil; session.full_jid = nil; session.resource = nil; if session.type == "c2s" then session.type = "c2s_unbound"; end if next(bare_sessions[session.username..'@'..session.host].sessions) == nil then bare_sessions[session.username..'@'..session.host] = nil; hosts[session.host].sessions[session.username] = nil; end session.log("error", "Roster loading failed: %s", err); return nil, "cancel", "internal-server-error", "Error loading roster"; end hosts[session.host].events.fire_event("resource-bind", {session=session}); return true; end local function send_to_available_resources(username, host, stanza) local jid = username.."@"..host; local count = 0; local user = bare_sessions[jid]; if user then for _, session in pairs(user.sessions) do if session.presence then session.send(stanza); count = count + 1; end end end return count; end local function send_to_interested_resources(username, host, stanza) local jid = username.."@"..host; local count = 0; local user = bare_sessions[jid]; if user then for _, session in pairs(user.sessions) do if session.interested then session.send(stanza); count = count + 1; end end end return count; 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; send_to_available_resources = send_to_available_resources; send_to_interested_resources = send_to_interested_resources; }; prosody-13.0.1/core/PaxHeaders/stanza_router.lua0000644000000000000000000000011714773555365016733 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.535710311 prosody-13.0.1/core/stanza_router.lua0000644000175000017500000002120614773555365021133 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local log = require "prosody.util.logger".init("stanzarouter") local hosts = _G.prosody.hosts; local tostring = tostring; 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; local core_post_stanza, core_process_stanza, core_route_stanza; local valid_stanzas = { message = true, presence = true, iq = true }; local function handle_unhandled_stanza(host, origin, stanza) --luacheck: ignore 212/host local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns or "jabber:client", origin.type; if xmlns == "jabber:client" and valid_stanzas[name] then -- A normal stanza local st_type = stanza.attr.type; if st_type == "error" or (name == "iq" and st_type == "result") then if st_type == "error" then local err_type, err_condition, err_message = stanza:get_error(); -- luacheck: ignore 211/err_message log("debug", "Discarding unhandled error %s (%s, %s) from %s: %s", name, err_type, err_condition or "unknown condition", origin_type, stanza:top_tag()); else log("debug", "Discarding %s from %s of type: %s", name, origin_type, st_type or ''); end return; end if name == "iq" and (st_type == "get" or st_type == "set") and stanza.tags[1] then xmlns = stanza.tags[1].attr.xmlns or "jabber:client"; end log("debug", "Unhandled %s stanza: %s; xmlns=%s", origin_type, name, xmlns); if origin.send then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end else log("warn", "Unhandled %s stream element or stanza: %s; xmlns=%s: %s", origin_type, name, xmlns, tostring(stanza)); -- we didn't handle it origin:close("unsupported-stanza-type"); end end local iq_types = { set=true, get=true, result=true, error=true }; function core_process_stanza(origin, stanza) (origin.log or log)("debug", "Received[%s]: %s", origin.type, stanza:top_tag()) if origin.type == "c2s" and not stanza.attr.xmlns then local name, st_type = stanza.name, stanza.attr.type; if st_type == "error" and #stanza.tags == 0 then return handle_unhandled_stanza(origin.host, origin, stanza); end if name == "iq" then if not iq_types[st_type] then origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid IQ type")); return; elseif not stanza.attr.id then origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing required 'id' attribute")); return; elseif (st_type == "set" or st_type == "get") and (#stanza.tags ~= 1) then origin.send(st.error_reply(stanza, "modify", "bad-request", "Incorrect number of children for IQ stanza")); return; end end -- TODO also, stanzas should be returned to their original state before the function ends stanza.attr.from = origin.full_jid; end local to, xmlns = stanza.attr.to, stanza.attr.xmlns; local from = stanza.attr.from; local node, host, resource; local from_node, from_host, from_resource; local to_bare, from_bare; if to then if full_sessions[to] or bare_sessions[to] or hosts[to] then host = jid_host(to); else node, host, resource = jid_prepped_split(to); if not host then log("warn", "Received stanza with invalid destination JID: %s", to); if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then origin.send(st.error_reply(stanza, "modify", "jid-malformed", "The destination address is invalid: "..to)); end return; end to_bare = node and (node.."@"..host) or host; -- bare JID if resource then to = to_bare.."/"..resource; else to = to_bare; end stanza.attr.to = to; end end if from and not origin.full_jid then -- We only stamp the 'from' on c2s stanzas, so we still need to check validity from_node, from_host, from_resource = jid_prepped_split(from); if not from_host then log("warn", "Received stanza with invalid source JID: %s", from); if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then origin.send(st.error_reply(stanza, "modify", "jid-malformed", "The source address is invalid: "..from)); end return; end from_bare = from_node and (from_node.."@"..from_host) or from_host; -- bare JID if from_resource then from = from_bare.."/"..from_resource; else from = from_bare; end stanza.attr.from = from; end if (origin.type == "s2sin" or origin.type == "s2sout" or origin.type == "c2s" or origin.type == "component") and xmlns == nil then if (origin.type == "s2sin" or origin.type == "s2sout") and not origin.dummy then local host_status = origin.hosts[from_host]; if not host_status or not host_status.authed then -- remote server trying to impersonate some other server? log("warn", "Received a stanza claiming to be from %s, over a stream authed for %s!", from_host, origin.from_host); origin:close("not-authorized"); return; elseif not hosts[host] then log("warn", "Remote server %s sent us a stanza for %s, closing stream", origin.from_host, host); origin:close("host-unknown"); return; end end core_post_stanza(origin, stanza, origin.full_jid); else local h = hosts[stanza.attr.to or origin.host]; if h then local event; if xmlns == nil then if stanza.name == "iq" and (stanza.attr.type == "set" or stanza.attr.type == "get") and stanza.tags[1] and stanza.tags[1].attr.xmlns then event = "stanza/iq/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name; else event = "stanza/"..stanza.name; end else event = "stanza/"..xmlns..":"..stanza.name; end 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, origin, stanza); end end function core_post_stanza(origin, stanza, preevents) local to = stanza.attr.to; local node, host, resource = jid_split(to); local to_bare = node and (node.."@"..host) or host; -- bare JID local to_type, to_self; if node then if resource then to_type = '/full'; else to_type = '/bare'; if node == origin.username and host == origin.host then stanza.attr.to = nil; to_self = true; end end else if host then to_type = '/host'; else to_type = '/bare'; to_self = true; end end local event_data = {origin=origin, stanza=stanza, to_self=to_self}; if preevents then -- c2s connection local result = hosts[origin.host].events.fire_event("pre-stanza", event_data); if result ~= nil then log("debug", "Stanza rejected by pre-stanza handler: %s", event_data.reason or "unknown reason"); return; end if hosts[origin.host].events.fire_event('pre-'..stanza.name..to_type, event_data) then return; end -- do preprocessing end local h = hosts[to_bare] or hosts[host or origin.host]; if h then if h.events.fire_event(stanza.name..to_type, event_data) then return; end -- do processing if to_self and h.events.fire_event(stanza.name..'/self', event_data) then return; end -- do processing handle_unhandled_stanza(h.host, origin, stanza); else core_route_stanza(origin, stanza); end end function core_route_stanza(origin, stanza) local to_host = jid_host(stanza.attr.to); local from_host = jid_host(stanza.attr.from); -- Auto-detect origin if not specified origin = origin or hosts[from_host]; if not origin then return false; end if hosts[to_host] then -- old stanza routing code removed core_post_stanza(origin, stanza); else local host_session = hosts[from_host]; if not host_session then log("error", "No hosts[from_host] (please report): %s", stanza); else local xmlns = stanza.attr.xmlns; stanza.attr.xmlns = nil; local routed = host_session.events.fire_event("route/remote", { origin = origin, stanza = stanza, from_host = from_host, to_host = to_host }); stanza.attr.xmlns = xmlns; -- reset if not routed then log("debug", "Could not route stanza to remote"); if stanza.attr.type == "error" or (stanza.name == "iq" and stanza.attr.type == "result") then return; end core_route_stanza(host_session, st.error_reply(stanza, "cancel", "not-allowed", "Communication with remote domains is not enabled")); end end end end --luacheck: ignore 122/prosody prosody.core_process_stanza = core_process_stanza; prosody.core_post_stanza = core_post_stanza; prosody.core_route_stanza = core_route_stanza; prosody-13.0.1/core/PaxHeaders/statsmanager.lua0000644000000000000000000000011714773555365016524 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.535710311 prosody-13.0.1/core/statsmanager.lua0000644000175000017500000002110214773555365020717 0ustar00prosodyprosody00000000000000 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 "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); if stats_interval_config and not stats_interval and stats_interval_config ~= "manual" then log("error", "Invalid 'statistics_interval' setting, statistics will be disabled"); end local stats_provider_name; local stats_provider_config = config.get("*", "statistics"); local stats_provider = stats_provider_config; if not stats_provider and stats_interval then stats_provider = "internal"; elseif stats_provider and not stats_interval then stats_interval = 60; end if stats_interval_config == "manual" then stats_interval = nil; end local builtin_providers = { internal = "prosody.util.statistics"; statsd = "prosody.util.statsd"; }; local stats, stats_err = false, nil; if stats_provider then if stats_provider:sub(1,1) == ":" then stats_provider = stats_provider:sub(2); stats_provider_name = "external "..stats_provider; elseif stats_provider then stats_provider_name = "built-in "..stats_provider; stats_provider = builtin_providers[stats_provider]; if not stats_provider then log("error", "Unrecognized statistics provider '%s', statistics will be disabled", stats_provider_config); end end local have_stats_provider, stats_lib = pcall(require, stats_provider); if not have_stats_provider then stats, stats_err = nil, stats_lib; else local stats_config = config.get("*", "statistics_config"); stats, stats_err = stats_lib.new(stats_config); stats_provider_name = stats_lib._NAME or stats_provider_name; end end if stats == nil then log("error", "Error loading statistics provider '%s': %s", stats_provider, stats_err); end local measure, collect, metric, cork, uncork; if stats then function metric(type_, name, unit, description, labels, extra) local registry = stats.metric_registry local f = assert(registry[type_], "unknown metric family type: "..type_); return f(registry, name, unit or "", description or "", labels, extra); end local function new_legacy_metric(stat_type, name, unit, description, fixed_label_key, fixed_label_value, extra) local label_keys = array() local conf = extra or {} if fixed_label_key then label_keys:push(fixed_label_key) end unit = unit or "" local mf = metric(stat_type, "prosody_" .. name, unit, description, label_keys, conf); if fixed_label_key then mf = mf:with_partial_label(fixed_label_value) end return mf:with_labels() end local function unwrap_legacy_extra(extra, type_, name, unit) local description = extra and extra.description or name.." "..type_ unit = extra and extra.unit or unit return description, unit end -- These wrappers provide the pre-OpenMetrics interface of statsmanager -- and moduleapi (module:measure). local legacy_metric_wrappers = { amount = function(name, fixed_label_key, fixed_label_value, extra) local initial = 0 if type(extra) == "number" then initial = extra else initial = extra and extra.initial or initial end local description, unit = unwrap_legacy_extra(extra, "amount", name) local m = new_legacy_metric("gauge", name, unit, description, fixed_label_key, fixed_label_value) m:set(initial or 0) return function(v) m:set(v) end end; counter = function(name, fixed_label_key, fixed_label_value, extra) if type(extra) == "number" then -- previous versions of the API allowed passing an initial -- value here; we do not allow that anymore, it is not a thing -- which makes sense with counters extra = nil end local description, unit = unwrap_legacy_extra(extra, "counter", name) local m = new_legacy_metric("counter", name, unit, description, fixed_label_key, fixed_label_value) m:set(0) return function(v) m:add(v) end end; rate = function(name, fixed_label_key, fixed_label_value, extra) if type(extra) == "number" then -- previous versions of the API allowed passing an initial -- value here; we do not allow that anymore, it is not a thing -- which makes sense with counters extra = nil end local description, unit = unwrap_legacy_extra(extra, "counter", name) local m = new_legacy_metric("counter", name, unit, description, fixed_label_key, fixed_label_value) m:set(0) return function() m:add(1) end end; times = function(name, fixed_label_key, fixed_label_value, extra) local conf = {} if extra and extra.buckets then conf.buckets = extra.buckets else conf.buckets = { 0.001, 0.01, 0.1, 1.0, 10.0, 100.0 } end local description, _ = unwrap_legacy_extra(extra, "times", name) local m = new_legacy_metric("histogram", name, "seconds", description, fixed_label_key, fixed_label_value, conf) return function() return timed(m) end end; sizes = function(name, fixed_label_key, fixed_label_value, extra) local conf = {} if extra and extra.buckets then conf.buckets = extra.buckets else conf.buckets = { 1024, 4096, 32768, 131072, 1048576, 4194304, 33554432, 134217728, 1073741824 } end local description, _ = unwrap_legacy_extra(extra, "sizes", name) local m = new_legacy_metric("histogram", name, "bytes", description, fixed_label_key, fixed_label_value, conf) return function(v) m:sample(v) end end; distribution = function(name, fixed_label_key, fixed_label_value, extra) if type(extra) == "string" then -- compat with previous API extra = { unit = extra } end local description, unit = unwrap_legacy_extra(extra, "distribution", name, "") local m = new_legacy_metric("summary", name, unit, description, fixed_label_key, fixed_label_value) return function(v) m:sample(v) end end; }; -- Argument order switched here to support the legacy statsmanager.measure -- interface. function measure(stat_type, name, extra, fixed_label_key, fixed_label_value) local wrapper = assert(legacy_metric_wrappers[stat_type], "unknown legacy metric type "..stat_type) return wrapper(name, fixed_label_key, fixed_label_value, extra) end if stats.cork then function cork() return stats:cork() end function uncork() return stats:uncork() end else function cork() end function uncork() end end if stats_interval or stats_interval_config == "manual" then local mark_collection_start = measure("times", "stats.collection"); local mark_processing_start = measure("times", "stats.processing"); function collect() local mark_collection_done = mark_collection_start(); fire_event("stats-update"); -- ensure that the backend is uncorked, in case it got stuck at -- some point, to avoid infinite resource use uncork() mark_collection_done(); local manual_result = nil if stats.metric_registry then -- only if supported by the backend, we fire the event which -- provides the current metric values local mark_processing_done = mark_processing_start(); local metric_registry = stats.metric_registry; fire_event("openmetrics-updated", { metric_registry = metric_registry }) mark_processing_done(); manual_result = metric_registry; end return stats_interval, manual_result; end if stats_interval then log("debug", "Statistics enabled using %s provider, collecting every %d seconds", stats_provider_name, stats_interval); timer.add_task(stats_interval, collect); prosody.events.add_handler("server-started", function () collect() end, -1); prosody.events.add_handler("server-stopped", function () collect() end, -1); else log("debug", "Statistics enabled using %s provider, no scheduled collection", stats_provider_name); end else log("debug", "Statistics enabled using %s provider, collection is disabled", stats_provider_name); end else log("debug", "Statistics disabled"); function measure() return measure; end local dummy_mt = {} function dummy_mt.__newindex() end function dummy_mt:__index() return self end function dummy_mt:__call() return self end local dummy = {} setmetatable(dummy, dummy_mt) function metric() return dummy; end function cork() end function uncork() end end local exported_collect = nil; if stats_interval_config == "manual" then exported_collect = collect; end return { collect = exported_collect; measure = measure; cork = cork; uncork = uncork; metric = metric; get_metric_registry = function () return stats and stats.metric_registry or nil end; }; prosody-13.0.1/core/PaxHeaders/storagemanager.lua0000644000000000000000000000011714773555365017032 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.535710311 prosody-13.0.1/core/storagemanager.lua0000644000175000017500000002340214773555365021232 0ustar00prosodyprosody00000000000000 local type, pairs = type, pairs; local setmetatable = setmetatable; local rawset = rawset; 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; local hosts = prosody.hosts; local _ENV = nil; -- luacheck: std none local olddm = {}; -- maintain old datamanager, for backwards compatibility for k,v in pairs(datamanager) do olddm[k] = v; end local null_storage_method = function () return false, "no data storage active"; end local null_storage_driver = setmetatable( { name = "null", open = function (self) return self; end }, { __index = function (self, method) --luacheck: ignore 212 return null_storage_method; end } ); local async_check = config.get("*", "storage_async_check") == true; local stores_available = multitable.new(); local function check_async_wrapper(event) local store = event.store; event.store = setmetatable({}, { __index = function (t, method_name) local original_method = store[method_name]; if type(original_method) ~= "function" then if original_method then rawset(t, method_name, original_method); end return original_method; end local wrapped_method = function (...) if not async.ready() then log("warn", "ASYNC-01: Attempt to access storage outside async context, " .."see https://prosody.im/doc/developers/async - %s", debug.traceback()); end return original_method(...); end rawset(t, method_name, wrapped_method); return wrapped_method; end; }); end local function initialize_host(host) local host_session = hosts[host]; host_session.events.add_handler("item-added/storage-provider", function (event) local item = event.item; stores_available:set(host, item.name, item); end); host_session.events.add_handler("item-removed/storage-provider", function (event) local item = event.item; stores_available:set(host, item.name, nil); end); if async_check then host_session.events.add_handler("store-opened", check_async_wrapper); end end prosody.events.add_handler("host-activated", initialize_host, 101); local function load_driver(host, driver_name) if driver_name == "null" then return null_storage_driver; end local driver = stores_available:get(host, driver_name); if driver then return driver; end local ok, err = modulemanager.load(host, "storage_"..driver_name); if not ok then log("error", "Failed to load storage driver plugin %s on %s: %s", driver_name, host, err); end return stores_available:get(host, driver_name); end local function get_storage_config(host) -- Here used to be some some compat checks return config.get(host, "storage"); end local function get_driver(host, store) local storage = get_storage_config(host); local driver_name; local option_type = type(storage); if option_type == "string" then driver_name = storage; elseif option_type == "table" then driver_name = storage[store]; end if not driver_name then driver_name = config.get(host, "default_storage") or "internal"; end local driver = load_driver(host, driver_name); if not driver then log("warn", "Falling back to null driver for %s storage on %s", store, host); driver_name = "null"; driver = null_storage_driver; end return driver, driver_name; end local map_shim_mt = { __index = { get = function(self, username, key) local ret, err = self.keyval_store:get(username); if ret == nil then return nil, err end return ret[key]; end; set = function(self, username, key, data) local current, err = self.keyval_store:get(username); if current == nil then if err then return nil, err; else current = {}; end end current[key] = data; return self.keyval_store:set(username, current); end; set_keys = function (self, username, keydatas) local current, err = self.keyval_store:get(username); if current == nil then if err then return nil, err; end current = {}; end for k,v in pairs(keydatas) do if v == self.remove then v = nil; end current[k] = v; end return self.keyval_store:set(username, current); end; remove = {}; get_all = function (self, key) if type(key) ~= "string" or key == "" then return nil, "get_all only supports non-empty string keys"; end local ret; for username in self.keyval_store:users() do local key_data = self:get(username, key); if key_data then if not ret then ret = {}; end ret[username] = key_data; end end return ret; end; delete_all = function (self, key) if type(key) ~= "string" or key == "" then return nil, "delete_all only supports non-empty string keys"; end local data = { [key] = self.remove }; local last_err; for username in self.keyval_store:users() do local ok, err = self:set_keys(username, data); if not ok then last_err = err; end end if last_err then return nil, last_err; end return true; end; }; } 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) local keyval_store, err = open(host, store, "keyval"); if keyval_store == nil then return nil, err end return setmetatable({ keyval_store = keyval_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 if err == "unsupported-store" then if typ == "map" then -- Use shim on top of keyval store log("debug", "map storage driver unavailable, using shim on top of keyval store."); ret, err = create_map_shim(host, store); else log("debug", "Storage driver %s does not support store %s (%s), falling back to null driver", driver_name, store, typ or ""); ret, err = null_storage_driver, nil; end end end if ret then local event_data = { host = host, store_name = store, store_type = typ, store = ret }; hosts[host].events.fire_event("store-opened", event_data); ret, err = event_data.store, event_data.store_err; end return ret, err; end local function purge(user, host) local storage = get_storage_config(host); if type(storage) == "table" then -- multiple storage backends in use that we need to purge local purged = {}; for store, driver_name in pairs(storage) do if not purged[driver_name] then local driver = get_driver(host, store); if driver.purge then purged[driver_name] = driver:purge(user); else log("warn", "Storage driver %s does not support removing all user data, " .."you may need to delete it manually", driver_name); end end end end get_driver(host):purge(user); -- and the default driver olddm.purge(user, host); -- COMPAT list stores, like offline messages end up in the old datamanager return true; end function datamanager.load(username, host, datastore) return open(host, datastore):get(username); end function datamanager.store(username, host, datastore, data) return open(host, datastore):set(username, data); end function datamanager.users(host, datastore, typ) local driver = open(host, datastore, typ); if not driver.users then return function() log("warn", "Storage driver %s does not support listing users", driver.name) end end return driver:users(); end function datamanager.stores(username, host, typ) return get_driver(host):stores(username, typ); end function datamanager.purge(username, host) return purge(username, host); end return { initialize_host = initialize_host; load_driver = load_driver; get_driver = get_driver; open = open; purge = purge; olddm = olddm; }; prosody-13.0.1/core/PaxHeaders/usermanager.lua0000644000000000000000000000011714773555365016344 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.539710327 prosody-13.0.1/core/usermanager.lua0000644000175000017500000002742314773555365020553 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local modulemanager = require "prosody.core.modulemanager"; local log = require "prosody.util.logger".init("usermanager"); local type = type; 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; local setmetatable = setmetatable; local default_provider = "internal_hashed"; local debug = debug; local _ENV = nil; -- luacheck: std none local function new_null_provider() local function dummy() return nil, "method not implemented"; end; local function dummy_get_sasl_handler() return sasl_new(nil, {}); end return setmetatable({name = "null", get_sasl_handler = dummy_get_sasl_handler}, { __index = function(self, method) return dummy; end --luacheck: ignore 212 }); end local fallback_authz_provider = { -- luacheck: ignore 212 get_jids_with_role = function (role) end; get_user_role = function (user) end; set_user_role = function (user, role_name) end; 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() }; local function initialize_host(host) local host_session = hosts[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 fallback_authz_provider; if host_session.type ~= "local" then return; end host_session.events.add_handler("item-added/auth-provider", function (event) local provider = event.item; local auth_provider = config.get(host, "authentication") or default_provider; if config.get(host, "anonymous_login") then log("error", "Deprecated config option 'anonymous_login'. Use authentication = 'anonymous' instead."); auth_provider = "anonymous"; end -- COMPAT 0.7 if provider.name == auth_provider then host_session.users = setmetatable(provider, provider_mt); end if host_session.users ~= nil and host_session.users.name ~= nil then log("debug", "Host '%s' now set to use user provider '%s'", host, host_session.users.name); end end); host_session.events.add_handler("item-removed/auth-provider", function (event) local provider = event.item; if host_session.users == provider then host_session.users = new_null_provider(); end end); host_session.users = new_null_provider(); -- Start with the default usermanager provider local auth_provider = config.get(host, "authentication") or default_provider; if config.get(host, "anonymous_login") then auth_provider = "anonymous"; end -- COMPAT 0.7 if auth_provider ~= "null" then modulemanager.load(host, "auth_"..auth_provider); end end; prosody.events.add_handler("host-activated", initialize_host, 100); local function test_password(username, host, password) return hosts[host].users.test_password(username, password); end local function get_password(username, host) return hosts[host].users.get_password(username); end local function set_password(username, password, host, resource) local ok, err = hosts[host].users.set_password(username, password); if ok then log("info", "Account password changed: %s@%s", username, host); prosody.events.fire_event("user-password-changed", { username = username, host = host, resource = resource }); end 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); end local function create_user(username, password, host) local ok, err = hosts[host].users.create_user(username, password); if ok then log("info", "User account created: %s@%s", username, host); end return ok, err; end local function delete_user(username, host) local ok, err = hosts[host].users.delete_user(username); if not ok then return nil, err; end log("info", "User account deleted: %s@%s", username, host); prosody.events.fire_event("user-deleted", { username = username, host = 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 log("info", "User account enabled: %s@%s", username, host); 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 log("info", "User account disabled: %s@%s", username, host); 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 local function get_sasl_handler(host, session) return hosts[host].users.get_sasl_handler(session); end local function get_provider(host) return hosts[host].users; end local function get_user_role(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_role(user); end local function set_user_role(user, host, role_name) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end local role, err = hosts[host].authz.set_user_role(user, role_name); if role then log("info", "Account %s@%s role changed to %s", user, host, role_name); prosody.events.fire_event("user-role-changed", { username = user, host = host, role = role; }); end return role, err; end 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 if password then local pw_ok, pw_err = set_password(username, password, host); if not pw_ok then return nil, "Failed to set password: "..pw_err; end local enable_ok, enable_err = enable_user(username, host); if not enable_ok and enable_err ~= "method not implemented" then return enable_ok, "Failed to enable account: "..enable_err; end end return true; end local function user_can_assume_role(user, host, role_name) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end return hosts[host].authz.user_can_assume_role(user, role_name); end local function 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 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_name = role_name, 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) 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"); local role = get_jid_role(jid, host); return role and legacy_admin_roles[role.name] 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 local function get_role_by_name(role_name, host) if host and not hosts[host] then return false; end if type(role_name) ~= "string" then return false; end return hosts[host].authz.get_role_by_name(role_name); end local function get_all_roles(host) if host and not hosts[host] then return false; end return hosts[host].authz.get_all_roles(); end return { new_null_provider = new_null_provider; initialize_host = initialize_host; 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_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; }; prosody-13.0.1/PaxHeaders/doc0000644000000000000000000000013114773555365013064 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.539710327 30 ctime=1743706869.543710343 prosody-13.0.1/doc/0000755000175000017500000000000014773555365015344 5ustar00prosodyprosody00000000000000prosody-13.0.1/doc/PaxHeaders/coding_style.md0000644000000000000000000000011714773555365016152 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.539710327 prosody-13.0.1/doc/coding_style.md0000644000175000017500000004650114773555365020357 0ustar00prosodyprosody00000000000000 # Prosody Coding Style Guide This style guides lists the coding conventions used in the [Prosody](https://prosody.im/) project. It is based heavily on the [style guide used by the LuaRocks project](https://github.com/luarocks/lua-style-guide). ## Indentation and formatting * Prosody code is indented with tabs at the start of the line, a single tab per logical indent level: ```lua for i, pkg in ipairs(packages) do for name, version in pairs(pkg) do if name == searched then print(version); end end end ``` Tab width is configurable in editors, so never assume a particular width. Specifically this means you should not mix tabs and spaces, or use tabs for alignment of items at different indentation levels. * Use LF (Unix) line endings. ## Comments * Comments are encouraged where necessary to explain non-obvious code. * In general comments should be used to explain 'why', not 'how' ### Comment tags A comment may be prefixed with one of the following tags: * **FIXME**: Indicates a serious problem with the code that should be addressed * **TODO**: Indicates an open task, feature request or code restructuring that is primarily of interest to developers (otherwise it should be in the issue tracker). * **COMPAT**: Must be used on all code that is present only for backwards-compatibility, and may be removed one day. For example code that is added to support old or buggy third-party software or dependencies. **Example:** ```lua -- TODO: implement method local function something() -- FIXME: check conditions end ``` ## Variable names * Variable names with larger scope should be more descriptive than those with smaller scope. One-letter variable names should be avoided except for very small scopes (less than ten lines) or for iterators. * `i` should be used only as a counter variable in for loops (either numeric for or `ipairs`). * Prefer more descriptive names than `k` and `v` when iterating with `pairs`, unless you are writing a function that operates on generic tables. * Use `_` for ignored variables (e.g. in for loops:) ```lua for _, item in ipairs(items) do do_something_with_item(item); end ``` * Generally all identifiers (variables and function names) should use `snake_case`, i.e. lowercase words joined by `_`. ```lua -- bad local OBJEcttsssss = {} local thisIsMyObject = {} local c = function() -- ...stuff... end -- good local this_is_my_object = {}; local function do_that_thing() -- ...stuff... end ``` > **Rationale:** The standard library uses lowercase APIs, with `joinedlowercase` names, but this does not scale too well for more complex APIs. `snake_case` tends to look good enough and not too out-of-place along side the standard APIs. ```lua for _, name in pairs(names) do -- ...stuff... end ``` * Prefer using `is_` when naming boolean functions: ```lua -- bad local function evil(alignment) return alignment < 100 end -- good local function is_evil(alignment) return alignment < 100; end ``` * `UPPER_CASE` is to be used sparingly, with "constants" only. > **Rationale:** "Sparingly", since Lua does not have real constants. This notation is most useful in libraries that bind C libraries, when bringing over constants from C. * Do not use uppercase names starting with `_`, they are reserved by Lua. ## Tables * When creating a table, prefer populating its fields all at once, if possible: ```lua local player = { name = "Jack", class = "Rogue" }; ``` * Items should be separated by commas. If there are many items, put each key/value on a separate line and use a semi-colon after each item (including the last one): ```lua local player = { name = "Jack"; class = "Rogue"; } ``` > **Rationale:** This makes the structure of your tables more evident at a glance. Trailing semi-colons make it quicker to add new fields and produces shorter diffs. * Use plain `key` syntax whenever possible, use `["key"]` syntax when using names that can't be represented as identifiers and avoid mixing representations in a declaration: ```lua local mytable = { ["1394-E"] = val1; ["UTF-8"] = val2; ["and"] = val2; } ``` ## Strings * Use `"double quotes"` for strings; use `'single quotes'` when writing strings that contain double quotes. ```lua local name = "Prosody"; local sentence = 'The name of the program is "Prosody"'; ``` > **Rationale:** Double quotes are used as string delimiters in a larger number of programming languages. Single quotes are useful for avoiding escaping when using double quotes in literals. ## Line lengths * There are no hard or soft limits on line lengths. Line lengths are naturally limited by using one statement per line. If that still produces lines that are too long (e.g. an expression that produces a line over 256-characters long, for example), this means the expression is too complex and would do better split into subexpressions with reasonable names. > **Rationale:** No one works on VT100 terminals anymore. If line lengths are a proxy for code complexity, we should address code complexity instead of using line breaks to fit mind-bending statements over multiple lines. ## Function declaration syntax * Prefer function syntax over variable syntax. This helps differentiate between named and anonymous functions. ```lua -- bad local nope = function(name, options) -- ...stuff... end -- good local function yup(name, options) -- ...stuff... end ``` * Perform validation early and return as early as possible. ```lua -- bad local function is_good_name(name, options, arg) local is_good = #name > 3 is_good = is_good and #name < 30 -- ...stuff... return is_good end -- good local function is_good_name(name, options, args) if #name < 3 or #name > 30 then return false; end -- ...stuff... return true; end ``` ## Function calls * Even though Lua allows it, generally you should not omit parentheses for functions that take a unique string literal argument. ```lua -- bad local data = get_data"KRP"..tostring(area_number) -- good local data = get_data("KRP"..tostring(area_number)); local data = get_data("KRP")..tostring(area_number); ``` > **Rationale:** It is not obvious at a glace what the precedence rules are when omitting the parentheses in a function call. Can you quickly tell which of the two "good" examples in equivalent to the "bad" one? (It's the second one). * You should not omit parenthesis for functions that take a unique table argument on a single line. You may do so for table arguments that span several lines. ```lua local an_instance = a_module.new { a_parameter = 42; another_parameter = "yay"; } ``` > **Rationale:** The use as in `a_module.new` above occurs alone in a statement, so there are no precedence issues. ## Table attributes * Use dot notation when accessing known properties. ```lua local luke = { jedi = true; age = 28; } -- bad local is_jedi = luke["jedi"] -- good local is_jedi = luke.jedi; ``` * Use subscript notation `[]` when accessing properties with a variable or if using a table as a list. ```lua local vehicles = load_vehicles_from_disk("vehicles.dat") if vehicles["Porsche"] then porsche_handler(vehicles["Porsche"]); vehicles["Porsche"] = nil; end for name, cars in pairs(vehicles) do regular_handler(cars); end ``` > **Rationale:** Using dot notation makes it clearer that the given key is meant to be used as a record/object field. ## Functions in tables * When declaring modules and classes, declare functions external to the table definition: ```lua local my_module = {}; function my_module.a_function(x) -- code end ``` * When declaring metatables, declare function internal to the table definition. ```lua local version_mt = { __eq = function(a, b) -- code end; __lt = function(a, b) -- code end; } ``` > **Rationale:** Metatables contain special behavior that affect the tables they're assigned (and are used implicitly at the call site), so it's good to be able to get a view of the complete behavior of the metatable at a glance. This is not as important for objects and modules, which usually have way more code, and which don't fit in a single screen anyway, so nesting them inside the table does not gain much: when scrolling a longer file, it is more evident that `check_version` is a method of `Api` if it says `function Api:check_version()` than if it says `check_version = function()` under some indentation level. ## Variable declaration * Always use `local` to declare variables. ```lua -- bad superpower = get_superpower() -- good local superpower = get_superpower(); ``` > **Rationale:** Not doing so will result in global variables to avoid polluting the global namespace. ## Variable scope * Assign variables with the smallest possible scope. ```lua -- bad local function good() local name = get_name() test() print("doing stuff..") --...other stuff... if name == "test" then return false end return name end -- good local bad = function() test(); print("doing stuff.."); --...other stuff... local name = get_name(); if name == "test" then return false; end return name; end ``` > **Rationale:** Lua has proper lexical scoping. Declaring the function later means that its scope is smaller, so this makes it easier to check for the effects of a variable. ## Conditional expressions * False and nil are falsy in conditional expressions. Use shortcuts when you can, unless you need to know the difference between false and nil. ```lua -- bad if name ~= nil then -- ...stuff... end -- good if name then -- ...stuff... end ``` * Avoid designing APIs which depend on the difference between `nil` and `false`. * Use the `and`/`or` idiom for the pseudo-ternary operator when it results in more straightforward code. When nesting expressions, use parentheses to make it easier to scan visually: ```lua local function default_name(name) -- return the default "Waldo" if name is nil return name or "Waldo"; end local function brew_coffee(machine) return (machine and machine.is_loaded) and "coffee brewing" or "fill your water"; end ``` Note that the `x and y or z` as a substitute for `x ? y : z` does not work if `y` may be `nil` or `false` so avoid it altogether for returning booleans or values which may be nil. ## Blocks * Use single-line blocks only for `then return`, `then break` and `function return` (a.k.a "lambda") constructs: ```lua -- good if test then break end -- good if not ok then return nil, "this failed for this reason: " .. reason end -- good use_callback(x, function(k) return k.last end); -- good if test then return false end -- bad if test < 1 and do_complicated_function(test) == false or seven == 8 and nine == 10 then do_other_complicated_function() end -- good if test < 1 and do_complicated_function(test) == false or seven == 8 and nine == 10 then do_other_complicated_function(); return false; end ``` * Separate statements onto multiple lines. Use semicolons as statement terminators. ```lua -- bad local whatever = "sure" a = 1 b = 2 -- good local whatever = "sure"; a = 1; b = 2; ``` ## Spacing * Use a space after `--`. ```lua --bad -- good ``` * Always put a space after commas and between operators and assignment signs: ```lua -- bad local x = y*9 local numbers={1,2,3} numbers={1 , 2 , 3} numbers={1 ,2 ,3} local strings = { "hello" , "Lua" , "world" } dog.set( "attr",{ age="1 year", breed="Bernese Mountain Dog" }) -- good local x = y * 9; local numbers = {1, 2, 3}; local strings = { "hello"; "Lua"; "world"; } dog.set("attr", { age = "1 year"; breed = "Bernese Mountain Dog"; }); ``` * Indent tables and functions according to the start of the line, not the construct: ```lua -- bad local my_table = { "hello", "world", } using_a_callback(x, function(...) print("hello") end) -- good local my_table = { "hello"; "world"; } using_a_callback(x, function(...) print("hello"); end) ``` > **Rationale:** This keep indentation levels aligned at predictable places. You don't need to realign the entire block if something in the first line changes (such as replacing `x` with `xy` in the `using_a_callback` example above). * The concatenation operator gets a pass for avoiding spaces: ```lua -- okay local message = "Hello, "..user.."! This is your day # "..day.." in our platform!"; ``` > **Rationale:** Being at the baseline, the dots already provide some visual spacing. * No spaces after the name of a function in a declaration or in its arguments: ```lua -- bad local function hello ( name, language ) -- code end -- good local function hello(name, language) -- code end ``` * Add blank lines between functions: ```lua -- bad local function foo() -- code end local function bar() -- code end -- good local function foo() -- code end local function bar() -- code end ``` * Avoid aligning variable declarations: ```lua -- bad local a = 1 local long_identifier = 2 -- good local a = 1; local long_identifier = 2; ``` > **Rationale:** This produces extra diffs which add noise to `hg annotate`. * Alignment is occasionally useful when logical correspondence is to be highlighted: ```lua -- okay sys_command(form, UI_FORM_UPDATE_NODE, "a", FORM_NODE_HIDDEN, false); sys_command(form, UI_FORM_UPDATE_NODE, "sample", FORM_NODE_VISIBLE, false); ``` ## Typing * In non-performance critical code, it can be useful to add type-checking assertions for function arguments: ```lua function manif.load_manifest(repo_url, lua_version) assert(type(repo_url) == "string"); assert(type(lua_version) == "string" or not lua_version); -- ... end ``` * Use the standard functions for type conversion, avoid relying on coercion: ```lua -- bad local total_score = review_score .. "" -- good local total_score = tostring(review_score); ``` ## Errors * Functions that can fail for reasons that are expected (e.g. I/O) should return `nil` and a (string) error message on error, possibly followed by other return values such as an error code. * On errors such as API misuse, an error should be thrown, either with `error()` or `assert()`. ## Modules Follow [these guidelines](http://hisham.hm/2014/01/02/how-to-write-lua-modules-in-a-post-module-world/) for writing modules. In short: * Always require a module into a local variable named after the last component of the module’s full name. ```lua local bar = require("foo.bar"); -- requiring the module bar.say("hello"); -- using the module ``` * Don’t rename modules arbitrarily: ```lua -- bad local skt = require("socket") ``` > **Rationale:** Code is much harder to read if we have to keep going back to the top to check how you chose to call a module. * Start a module by declaring its table using the same all-lowercase local name that will be used to require it. You may use an LDoc comment to identify the whole module path. ```lua --- @module foo.bar local bar = {}; ``` * Try to use names that won't clash with your local variables. For instance, don't name your module something like “size”. * Use `local function` to declare _local_ functions only: that is, functions that won’t be accessible from outside the module. That is, `local function helper_foo()` means that `helper_foo` is really local. * Public functions are declared in the module table, with dot syntax: ```lua function bar.say(greeting) print(greeting); end ``` > **Rationale:** Visibility rules are made explicit through syntax. * Do not set any globals in your module and always return a table in the end. * If you would like your module to be used as a function, you may set the `__call` metamethod on the module table instead. > **Rationale:** Modules should return tables in order to be amenable to have their contents inspected via the Lua interactive interpreter or other tools. * Requiring a module should cause no side-effect other than loading other modules and returning the module table. * A module should not have state. If a module needs configuration, turn it into a factory. For example, do not make something like this: ```lua -- bad local mp = require "MessagePack" mp.set_integer("unsigned") ``` and do something like this instead: ```lua -- good local messagepack = require("messagepack"); local mpack = messagepack.new({integer = "unsigned"}); ``` * The invocation of require may omit parentheses around the module name: ```lua local bla = require "bla"; ``` ## Metatables, classes and objects If creating a new type of object that has a metatable and methods, the metatable and methods table should be separate, and the metatable name should end with `_mt`. ```lua local mytype_methods = {}; local mytype_mt = { __index = mytype_methods }; function mytype_methods:add_new_thing(thing) end local function new() return setmetatable({}, mytype_mt); end return { new = new }; ``` * Use the method notation when invoking methods: ``` -- bad my_object.my_method(my_object) -- good my_object:my_method(); ``` > **Rationale:** This makes it explicit that the intent is to use the function as a method. * Do not rely on the `__gc` metamethod to release resources other than memory. If your object manage resources such as files, add a `close` method to their APIs and do not auto-close via `__gc`. Auto-closing via `__gc` would entice users of your module to not close resources as soon as possible. (Note that the standard `io` library does not follow this recommendation, and users often forget that not closing files immediately can lead to "too many open files" errors when the program runs for a while.) > **Rationale:** The garbage collector performs automatic *memory* management, dealing with memory only. There is no guarantees as to when the garbage collector will be invoked, and memory pressure does not correlate to pressure on other resources. ## File structure * Lua files should be named in all lowercase. * Tests should be in a top-level `spec` directory. Prosody uses [Busted](http://olivinelabs.com/busted/) for testing. ## Static checking All code should pass [luacheck](https://github.com/mpeterv/luacheck) using the `.luacheckrc` provided in the Prosody repository, and using minimal inline exceptions. * luacheck warnings of class 211, 212, 213 (unused variable, argument or loop variable) may be ignored, if the unused variable was added explicitly: for example, sometimes it is useful, for code understandability, to spell out what the keys and values in a table are, even if you're only using one of them. Another example is a function that needs to follow a given signature for API reasons (e.g. a callback that follows a given format) but doesn't use some of its arguments; it's better to spell out in the argument what the API the function implements is, instead of adding `_` variables. ``` local foo, bar = some_function(); --luacheck: ignore 212/foo print(bar); ``` * luacheck warning 542 (empty if branch) can also be ignored, when a sequence of `if`/`elseif`/`else` blocks implements a "switch/case"-style list of cases, and one of the cases is meant to mean "pass". For example: ```lua if warning >= 600 and warning <= 699 then print("no whitespace warnings"); elseif warning == 542 then --luacheck: ignore 542 -- pass else print("got a warning: "..warning); end ``` > **Rationale:** This avoids writing negated conditions in the final fallback case, and it's easy to add another case to the construct without having to edit the fallback. prosody-13.0.1/doc/PaxHeaders/doap.xml0000644000000000000000000000011714773555365014612 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.539710327 prosody-13.0.1/doc/doap.xml0000644000175000017500000010613414773555365017016 0ustar00prosodyprosody00000000000000 Prosody IM Lightweight XMPP server Prosody is a server for Jabber/XMPP written in Lua. It aims to be easy to use and light on resources. For developers, it aims to give a flexible system on which to rapidly develop added functionality or rapidly prototype new protocols. 2008-08-22 Lua C Linux macOS FreeBSD OpenBSD NetBSD Matthew Wild MattJ https://matthewwild.co.uk/ Waqas Hussain waqas Kim Alvefur Zash https://www.zash.se/ 2.13.1 0.4.0 partial no support for multiple items (reported tag) 0.4.0 0.7.0 removed 2.0 0.1.0 mod_lastactivity and mod_uptime complete 0.7.0 0.10.0 removed mod_privacy 2.5.0 0.10.0 complete 0.1 0.1.0 complete libexpat 1.34.5 0.3.0 partial 1.2 0.1.0 complete mod_private, indirectly supported via XEP-0049 1.2 0.1.0 complete mod_private 1.3.0 0.8.0 complete mod_adhoc 1.2 0.1.0 complete mod_vcard and mod_vcard_legacy 1.0 0.10.0 complete used by XEP-0313, util.rsm 1.24.1 0.9.0 partial mod_pubsub 1.8.2 0.7.0 complete mod_proxy65 1.3.0 complete 0.1.0 2.4 complete 2.5 0.1.0 partial mod_legacyauth, lacks digest method 1.9 complete via XEP-0163 1.1.1 0.1.0 complete 1.1.4 complete via XEP-0163, also mod_vcard_legacy 1.2 0.1.0 13.0.0 removed mod_time 1.4 0.1.0 0.12.0 removed Gone from offline messages in 0.10.0, gone from MUC in 0.12 1.1 0.1.0 complete 1.0 0.9.0 complete util.jid.(un)escape, missing rejection of \20 at start or end per xep version 1.1. Missing PRECIS for version 1.1.1. 1.2.2 complete via XEP-0163 1.3 complete via XEP-0163 1.6 0.4.0 complete 1.6.0 0.8.0 complete 1.3.0 complete via XEP-0163 1.0.2 0.11.0 partial 1.11.2 0.2.0 complete mod_bosh 0.10.0 removed Gone with XEP-0016 1.0.1 0.9.0 complete 1.3.1 0.7.0 partial mod_admin_adhoc, missing some commands 2.1 0.6.0 0.10.0 removed Compression considered insecure 1.1.1 0.11.0 complete via XEP-0398 1.4.0 13.0.0 complete mod_http_altconnect 1.1.1 0.10.0 complete 1.0.1 0.1.0 complete 1.2.2 0.5.0 complete 1.0 complete 1.1 complete via XEP-0163, also mod_vcard_legacy 1.2 0.4.0 partial 1.2 0.9.0 complete 1.1 complete 1.4.0 0.12.0 mod_mam archives receipts 1.0 complete 0.9.10 0.14 complete via XEP-0163 1.3 complete 0.10.0 mod_blocklist 0.3 complete via XEP-0163 0.3 complete via XEP-0163 0.3 complete via XEP-0163 0.3 complete via XEP-0163 1.6.2 complete 0.12.0 mod_smacks 2.0.1 0.1.0 complete 2.0 complete 0.1.0 mod_time 2.0 0.1.0 complete 1.0.2 0.12.0 complete stanza size limits, bandwidth limits, stanza-too-big error condition 1.4 partial 0.2.0 What's that about restartlogic in 1.3? 1.0 complete required level 1.0.0 complete 0.12.0 mod_external_services 1.1.1 0.1.0 partial 1.0 0.11.0 complete mod_pep 1.1.1 0.11.0 complete mod_pep 1.1 0.7.0 partial Used in migrator tools and mod_storage_xep0227 1.3 0.4.0 complete implied by rfc6121 1.2 0.12.0 complete mod_csi_simple 1.0.1 complete 0.10.0 1.0.0 0.11.0 complete mod_csi_simple 1.0.1 complete 0.12.0 0.12.0 complete 0.11.0 mod_vcard4, mod_vcard_legacy 1.0 0.11.0 complete Used by XEP-0280, XEP-0313 0.1 complete Core Server 0.1 complete 0.6.0 Moved into mod_muc_unique in 0.11 1.1.2 complete 0.10.0 mod_mam, mod_muc_mam 0.2.0 complete 0.12.0 muc/hats 0.2 0.9.0 complete refers to inclusion of delay stamp in presence 1.0.0 0.10.0 partial Used in mod_mam and mod_muc 1.0.0 0.10.0 complete Used in mod_carbons, mod_mam, and mod_muc 1.0.0 0.11.0 complete mod_csi+mod_csi_simple 0.6.0 0.11.6 complete triggers buffer flush in mod_csi_simple since 0.11.6; recognised by mod_carbons and mod_mam since 0.12 0.4.1 13.0.0 complete mod_cloud_notify 0.7.0 complete 0.10.0 Used in context of XEP-0313 by mod_mam and mod_muc_mam 1.1.0 complete 0.12.0 mod_http_file_share 1.1.0 complete 0.2.0 c2s_direct_tls_ports (formerly legacy_ssl_ports) for c2s and direct_tls_s2s_ports for s2s 0.3.3 complete 0.12.0 0.4.0 0.11.0 complete Used in context of XEP-0352 0.8.3 complete via XEP-0163, XEP-0222 1.0.0 0.11.0 complete mod_vcard_legacy 0.5.0 0.12.0 partial 1.2.0 0.12.0 complete mod_bookmarks 1.1.0 0.11.0 complete Server Optimization 1.1.0 0.12.0 complete mod_bookmarks 1.0.0 0.12.0 complete mod_muc 0.2.0 partial 0.4.2 13.0.0 complete 0.2.0 complete Broken out of XEP-0313 0.2.0 0.12.0 complete 0.2.0 13.0.0 prosody-13.0.1/doc/PaxHeaders/hgrc-email.ini0000644000000000000000000000011714773555365015656 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.539710327 prosody-13.0.1/doc/hgrc-email.ini0000644000175000017500000000044514773555365020060 0ustar00prosodyprosody00000000000000# 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 = prosody-13.0.1/doc/PaxHeaders/hgrc.ini0000644000000000000000000000011714773555365014571 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.539710327 prosody-13.0.1/doc/hgrc.ini0000644000175000017500000000242414773555365016772 0ustar00prosodyprosody00000000000000[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 = prosody-13.0.1/doc/PaxHeaders/names.txt0000644000000000000000000000011714773555365015011 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.539710327 prosody-13.0.1/doc/names.txt0000644000175000017500000000225114773555365017210 0ustar00prosodyprosody00000000000000lxmppd - ... dia - Greek, 'through', pronounced "dee-ah", root of "dialogue" metaphor - An imaginative comparison between two actions/objects etc which is not literally applicable. minstrel - Itinerant medieval musician/singer/story teller/poet. parody - Imitation of a poem or another poet's style for comic/satiric effect. poesy - Archaic word for poetry. Xinshi - Chinese poetic term which literally means 'new poetry'. polylogue - Many conversations Thorns thought of: poe - Derived from "poetry" poezie - Romanian for "poesy" and "poem" Elain - Just a cool name Elane - A variation Eclaire - Idem (French) Adel - Random Younha - Read as "yuna" Quezacotl - Mayan gods -> google for correct form and pronunciation Carbuncle - FF8 Guardian Force ^^ Protos - Mars satellite mins - Derived from minstrel diapoe - gr. dia + poesy/poetry xinshi - I like it for a name just like that loom - The first application I run on the first day of using a computer Lory - Another name I happen to like Loki - Nordic god of mischief, IIRC Luna - Probably taken but I think worth mentioning Coreo - Random thought Miria - Also random Lora - Idem Kraken - :P Nebula - . prosody-13.0.1/doc/PaxHeaders/net.server.lua0000644000000000000000000000011714773555365015743 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.543710343 prosody-13.0.1/doc/net.server.lua0000644000175000017500000001431414773555365020145 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2014,2016 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. --luacheck: ignore --[[ This file is a template for writing a net.server compatible backend. ]] --[[ Read patterns (also called modes) can be one of: - "*a": Read as much as possible - "*l": Read until end of line ]] --- Handle API local handle_mt = {}; local handle_methods = {}; handle_mt.__index = handle_methods; function handle_methods:set_mode(new_pattern) end function handle_methods:setlistener(listeners) end function handle_methods:setoption(option, value) end function handle_methods:ip() end function handle_methods:starttls(sslctx) end function handle_methods:write(data) end function handle_methods:close() end function handle_methods:pause() end function handle_methods:resume() end --[[ Returns - socket: the socket object underlying this handle ]] function handle_methods:socket() end --[[ Returns - boolean: if an ssl context has been set on this handle ]] function handle_methods:ssl() end --- Listeners API local listeners = {} --[[ connect Called when a client socket has established a connection with it's peer ]] function listeners.onconnect(handle) end --[[ incoming Called when data is received If reading data failed this will be called with `nil, "error message"` ]] function listeners.onincoming(handle, buff, err) end --[[ status Known statuses: - "ssl-handshake-complete" ]] function listeners.onstatus(handle, status) end --[[ disconnect Called when the peer has closed the connection ]] function listeners.ondisconnect(handle) end --[[ drain Called when the handle's write buffer is empty ]] function listeners.ondrain(handle) end --[[ readtimeout Called when a socket inactivity timeout occurs ]] function listeners.onreadtimeout(handle) end --[[ detach: Called when other listeners are going to be removed Allows for clean-up ]] function listeners.ondetach(handle) end --- Top level functions --[[ Returns the syscall level event mechanism in use. Returns: - backend: e.g. "select", "epoll" ]] local function get_backend() end --[[ Starts the event loop. Returns: - "quitting" ]] local function loop() end --[[ Stop a running loop() ]] local function setquitting(quit) end --[[ Links to two handles together, so anything written to one is piped to the other Arguments: - sender, receiver: handles to link - buffersize: maximum #bytes until sender will be locked ]] local function link(sender, receiver, buffersize) end --[[ Binds and listens on the given address and port If `sslctx` is given, the connecting clients will have to negotiate an SSL session Arguments: - address: address to bind to, may be "*" to bind all addresses. will be resolved if it is a string. - port: port to bind (as number) - listeners: a table of listeners - pattern: the read pattern - sslctx: is a valid luasec constructor Returns: - handle - nil, "an error message": on failure (e.g. out of file descriptors) ]] local function addserver(address, port, listeners, pattern, sslctx) end --[[ Binds and listens on the given address and port Mostly the same as addserver but with all optional arguments in a table Arguments: - address: address to bind to, may be "*" to bind all addresses. will be resolved if it is a string. - port: port to bind (as number) - listeners: a table of listeners - config: table of extra settings - read_size: the amount of bytes to read or a read pattern - tls_ctx: is a valid luasec constructor - tls_direct: boolean true for direct TLS, false (or nil) for starttls Returns: - handle - nil, "an error message": on failure (e.g. out of file descriptors) ]] local function listen(address, port, listeners, config) end --[[ Wraps a lua-socket socket client socket in a handle. The socket must be already connected to the remote end. If `sslctx` is given, a SSL session will be negotiated before listeners are called. Arguments: - socket: the lua-socket object to wrap - ip: returned by `handle:ip()` - port: - listeners: a table of listeners - pattern: the read pattern - sslctx: is a valid luasec constructor - typ: the socket type, one of: - "tcp" - "tcp6" - "udp" Returns: - handle, socket - nil, "an error message": on failure (e.g. ) ]] local function wrapclient(socket, ip, serverport, listeners, pattern, sslctx) end --[[ Connects to the given address and port If `sslctx` is given, a SSL session will be negotiated before listeners are called. Arguments: - address: address to connect to. will be resolved if it is a string. - port: port to connect to (as number) - listeners: a table of listeners - pattern: the read pattern - sslctx: is a valid luasec constructor - typ: the socket type, one of: - "tcp" - "tcp6" - "udp" Returns: - handle - nil, "an error message": on failure (e.g. out of file descriptors) ]] local function addclient(address, port, listeners, pattern, sslctx, typ) end --[[ Close all handles ]] local function closeall() end --[[ The callback should be called after `delay` seconds. The callback should be called with the time at the point of firing. If the callback returns a number, it should be called again after that many seconds. Arguments: - delay: number of seconds to wait - callback: function to call. ]] local function add_task(delay, callback) end --[[ Adds a handler for when a signal is fired. Optional to implement callback does not take any arguments Arguments: - signal_id: the signal id (as number) to listen for - handler: callback ]] local function hook_signal(signal_id, handler) end --[[ Adds a low-level FD watcher Arguments: - fd_number: A non-negative integer representing a file descriptor or object with a :getfd() method returning one - on_readable: Optional callback for when the FD is readable - on_writable: Optional callback for when the FD is writable Returns: - net.server handle ]] local function watchfd(fd_number, on_readable, on_writable) end return { get_backend = get_backend; loop = loop; setquitting = setquitting; link = link; addserver = addserver; wrapclient = wrapclient; addclient = addclient; closeall = closeall; hook_signal = hook_signal; watchfd = watchfd; listen = listen; } prosody-13.0.1/doc/PaxHeaders/roster_format.txt0000644000000000000000000000011714773555365016574 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.543710343 prosody-13.0.1/doc/roster_format.txt0000644000175000017500000000113114773555365020767 0ustar00prosodyprosody00000000000000 This file documents the structure of the roster object. table roster { [string bare_jid] = roster_item } table roster_item { string subscription = "none" | "to" | "from" | "both" string name = Opaque string set by client. (optional) set groups = a set of opaque strings set by the client boolean ask = nil | "subscribe" - a value of true indicates subscription is pending } The roster is available as hosts[host].sessions[username].roster and a copy is made to session.roster for all sessions. All modifications to a roster should be done through the rostermanager. prosody-13.0.1/doc/PaxHeaders/session.txt0000644000000000000000000000011714773555365015371 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.543710343 prosody-13.0.1/doc/session.txt0000644000175000017500000000320414773555365017567 0ustar00prosodyprosody00000000000000 Structure of a session: session { -- properties -- conn -- the tcp connection notopen -- true if stream has not been initiated, removed after receiving type -- the connection type. Valid values include: -- "c2s_unauthed" - connection has not been authenticated yet -- "c2s" - from a local client to the server username -- the node part of the client's jid (not defined before auth) host -- the host part of the client's jid (not defined before stream initiation) resource -- the resource part of the client's full jid (not defined before resource binding) full_jid -- convenience for the above 3 as string in username@host/resource form (not defined before resource binding) priority -- the resource priority, default: 0 presence -- the last non-directed presence with no type attribute. initially nil. reset to nil on unavailable presence. interested -- true if the resource requested the roster. Interested resources receive roster updates. Initially nil. roster -- the user's roster. Loaded as soon as the resource is bound (session becomes a connected resource). -- methods -- send(x) -- converts x to a string, and writes it to the connection close(x) -- Disconnect the user and clean up the session, best call sessionmanager.destroy_session() instead of this in most cases } if session.full_jid (also session.roster and session.resource) then this is a "connected resource" if session.presence then this is an "available resource" (all available resources are connected resources) if session.interested then this is an "interested resource" (all interested resources are connected resources) prosody-13.0.1/doc/PaxHeaders/stanza.txt0000644000000000000000000000011714773555365015206 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.543710343 prosody-13.0.1/doc/stanza.txt0000644000175000017500000000104614773555365017406 0ustar00prosodyprosody00000000000000 Structure of a stanza: stanza { --- properties --- tags -- array of tags --- static methods --- iq(attrs) -- --- read-only methods --- reply -- return new stanza with attributes of current stanza child_with_name(string name) -- return the first child of the current tag with the matching name --- write methods --- tag(name, sttrs) -- create a new child of the current tag, and set the child as current up() -- move to the parent of the current tag text(string) -- append a new text node to the current tag } prosody-13.0.1/doc/PaxHeaders/stanza_routing.txt0000644000000000000000000000011714773555365016755 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.543710343 prosody-13.0.1/doc/stanza_routing.txt0000644000175000017500000000152514773555365021157 0ustar00prosodyprosody00000000000000No 'to' attribute: IQ: Pass to appropriate handler Presence: Broadcast to contacts - if initial presence, also send out presence probes - if probe would be to local user, generate presence stanza for them Message: Route as if it is addressed to the bare JID of the sender To a local host: IQ: Pass to appropriate handler Presence: - Message: Deliver to admin? To local contact: Bare JID: IQ: Pass to appropriate handler Presence: Broadcast to all resources Message: Route to 'best' resource Full JID: IQ: Send to resource Presence: Send to resource Message: Send to resource Full JID but resource not connected: IQ: Return service-unavailable Message: Handle same as if to bare JID Presence: Drop (unless type=subscribe[ed]) To remote contact: Initiate s2s connection if necessary Send stanza across prosody-13.0.1/PaxHeaders/fallbacks0000644000000000000000000000013114773555365014241 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.543710343 30 ctime=1743706869.547710359 prosody-13.0.1/fallbacks/0000755000175000017500000000000014773555365016521 5ustar00prosodyprosody00000000000000prosody-13.0.1/fallbacks/PaxHeaders/bit.lua0000644000000000000000000000011714773555365015603 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.543710343 prosody-13.0.1/fallbacks/bit.lua0000644000175000017500000002360514773555365020010 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local type = type; local tonumber = tonumber; local setmetatable = setmetatable; local error = error; local tostring = tostring; local print = print; local xor_map = {[0]=0;[1]=1;[2]=2;[3]=3;[4]=4;[5]=5;[6]=6;[7]=7;[8]=8;[9]=9;[10]=10;[11]=11;[12]=12;[13]=13;[14]=14;[15]=15;[16]=1;[17]=0;[18]=3;[19]=2;[20]=5;[21]=4;[22]=7;[23]=6;[24]=9;[25]=8;[26]=11;[27]=10;[28]=13;[29]=12;[30]=15;[31]=14;[32]=2;[33]=3;[34]=0;[35]=1;[36]=6;[37]=7;[38]=4;[39]=5;[40]=10;[41]=11;[42]=8;[43]=9;[44]=14;[45]=15;[46]=12;[47]=13;[48]=3;[49]=2;[50]=1;[51]=0;[52]=7;[53]=6;[54]=5;[55]=4;[56]=11;[57]=10;[58]=9;[59]=8;[60]=15;[61]=14;[62]=13;[63]=12;[64]=4;[65]=5;[66]=6;[67]=7;[68]=0;[69]=1;[70]=2;[71]=3;[72]=12;[73]=13;[74]=14;[75]=15;[76]=8;[77]=9;[78]=10;[79]=11;[80]=5;[81]=4;[82]=7;[83]=6;[84]=1;[85]=0;[86]=3;[87]=2;[88]=13;[89]=12;[90]=15;[91]=14;[92]=9;[93]=8;[94]=11;[95]=10;[96]=6;[97]=7;[98]=4;[99]=5;[100]=2;[101]=3;[102]=0;[103]=1;[104]=14;[105]=15;[106]=12;[107]=13;[108]=10;[109]=11;[110]=8;[111]=9;[112]=7;[113]=6;[114]=5;[115]=4;[116]=3;[117]=2;[118]=1;[119]=0;[120]=15;[121]=14;[122]=13;[123]=12;[124]=11;[125]=10;[126]=9;[127]=8;[128]=8;[129]=9;[130]=10;[131]=11;[132]=12;[133]=13;[134]=14;[135]=15;[136]=0;[137]=1;[138]=2;[139]=3;[140]=4;[141]=5;[142]=6;[143]=7;[144]=9;[145]=8;[146]=11;[147]=10;[148]=13;[149]=12;[150]=15;[151]=14;[152]=1;[153]=0;[154]=3;[155]=2;[156]=5;[157]=4;[158]=7;[159]=6;[160]=10;[161]=11;[162]=8;[163]=9;[164]=14;[165]=15;[166]=12;[167]=13;[168]=2;[169]=3;[170]=0;[171]=1;[172]=6;[173]=7;[174]=4;[175]=5;[176]=11;[177]=10;[178]=9;[179]=8;[180]=15;[181]=14;[182]=13;[183]=12;[184]=3;[185]=2;[186]=1;[187]=0;[188]=7;[189]=6;[190]=5;[191]=4;[192]=12;[193]=13;[194]=14;[195]=15;[196]=8;[197]=9;[198]=10;[199]=11;[200]=4;[201]=5;[202]=6;[203]=7;[204]=0;[205]=1;[206]=2;[207]=3;[208]=13;[209]=12;[210]=15;[211]=14;[212]=9;[213]=8;[214]=11;[215]=10;[216]=5;[217]=4;[218]=7;[219]=6;[220]=1;[221]=0;[222]=3;[223]=2;[224]=14;[225]=15;[226]=12;[227]=13;[228]=10;[229]=11;[230]=8;[231]=9;[232]=6;[233]=7;[234]=4;[235]=5;[236]=2;[237]=3;[238]=0;[239]=1;[240]=15;[241]=14;[242]=13;[243]=12;[244]=11;[245]=10;[246]=9;[247]=8;[248]=7;[249]=6;[250]=5;[251]=4;[252]=3;[253]=2;[254]=1;[255]=0;}; local or_map = {[0]=0;[1]=1;[2]=2;[3]=3;[4]=4;[5]=5;[6]=6;[7]=7;[8]=8;[9]=9;[10]=10;[11]=11;[12]=12;[13]=13;[14]=14;[15]=15;[16]=1;[17]=1;[18]=3;[19]=3;[20]=5;[21]=5;[22]=7;[23]=7;[24]=9;[25]=9;[26]=11;[27]=11;[28]=13;[29]=13;[30]=15;[31]=15;[32]=2;[33]=3;[34]=2;[35]=3;[36]=6;[37]=7;[38]=6;[39]=7;[40]=10;[41]=11;[42]=10;[43]=11;[44]=14;[45]=15;[46]=14;[47]=15;[48]=3;[49]=3;[50]=3;[51]=3;[52]=7;[53]=7;[54]=7;[55]=7;[56]=11;[57]=11;[58]=11;[59]=11;[60]=15;[61]=15;[62]=15;[63]=15;[64]=4;[65]=5;[66]=6;[67]=7;[68]=4;[69]=5;[70]=6;[71]=7;[72]=12;[73]=13;[74]=14;[75]=15;[76]=12;[77]=13;[78]=14;[79]=15;[80]=5;[81]=5;[82]=7;[83]=7;[84]=5;[85]=5;[86]=7;[87]=7;[88]=13;[89]=13;[90]=15;[91]=15;[92]=13;[93]=13;[94]=15;[95]=15;[96]=6;[97]=7;[98]=6;[99]=7;[100]=6;[101]=7;[102]=6;[103]=7;[104]=14;[105]=15;[106]=14;[107]=15;[108]=14;[109]=15;[110]=14;[111]=15;[112]=7;[113]=7;[114]=7;[115]=7;[116]=7;[117]=7;[118]=7;[119]=7;[120]=15;[121]=15;[122]=15;[123]=15;[124]=15;[125]=15;[126]=15;[127]=15;[128]=8;[129]=9;[130]=10;[131]=11;[132]=12;[133]=13;[134]=14;[135]=15;[136]=8;[137]=9;[138]=10;[139]=11;[140]=12;[141]=13;[142]=14;[143]=15;[144]=9;[145]=9;[146]=11;[147]=11;[148]=13;[149]=13;[150]=15;[151]=15;[152]=9;[153]=9;[154]=11;[155]=11;[156]=13;[157]=13;[158]=15;[159]=15;[160]=10;[161]=11;[162]=10;[163]=11;[164]=14;[165]=15;[166]=14;[167]=15;[168]=10;[169]=11;[170]=10;[171]=11;[172]=14;[173]=15;[174]=14;[175]=15;[176]=11;[177]=11;[178]=11;[179]=11;[180]=15;[181]=15;[182]=15;[183]=15;[184]=11;[185]=11;[186]=11;[187]=11;[188]=15;[189]=15;[190]=15;[191]=15;[192]=12;[193]=13;[194]=14;[195]=15;[196]=12;[197]=13;[198]=14;[199]=15;[200]=12;[201]=13;[202]=14;[203]=15;[204]=12;[205]=13;[206]=14;[207]=15;[208]=13;[209]=13;[210]=15;[211]=15;[212]=13;[213]=13;[214]=15;[215]=15;[216]=13;[217]=13;[218]=15;[219]=15;[220]=13;[221]=13;[222]=15;[223]=15;[224]=14;[225]=15;[226]=14;[227]=15;[228]=14;[229]=15;[230]=14;[231]=15;[232]=14;[233]=15;[234]=14;[235]=15;[236]=14;[237]=15;[238]=14;[239]=15;[240]=15;[241]=15;[242]=15;[243]=15;[244]=15;[245]=15;[246]=15;[247]=15;[248]=15;[249]=15;[250]=15;[251]=15;[252]=15;[253]=15;[254]=15;[255]=15;}; local and_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=0;[9]=0;[10]=0;[11]=0;[12]=0;[13]=0;[14]=0;[15]=0;[16]=0;[17]=1;[18]=0;[19]=1;[20]=0;[21]=1;[22]=0;[23]=1;[24]=0;[25]=1;[26]=0;[27]=1;[28]=0;[29]=1;[30]=0;[31]=1;[32]=0;[33]=0;[34]=2;[35]=2;[36]=0;[37]=0;[38]=2;[39]=2;[40]=0;[41]=0;[42]=2;[43]=2;[44]=0;[45]=0;[46]=2;[47]=2;[48]=0;[49]=1;[50]=2;[51]=3;[52]=0;[53]=1;[54]=2;[55]=3;[56]=0;[57]=1;[58]=2;[59]=3;[60]=0;[61]=1;[62]=2;[63]=3;[64]=0;[65]=0;[66]=0;[67]=0;[68]=4;[69]=4;[70]=4;[71]=4;[72]=0;[73]=0;[74]=0;[75]=0;[76]=4;[77]=4;[78]=4;[79]=4;[80]=0;[81]=1;[82]=0;[83]=1;[84]=4;[85]=5;[86]=4;[87]=5;[88]=0;[89]=1;[90]=0;[91]=1;[92]=4;[93]=5;[94]=4;[95]=5;[96]=0;[97]=0;[98]=2;[99]=2;[100]=4;[101]=4;[102]=6;[103]=6;[104]=0;[105]=0;[106]=2;[107]=2;[108]=4;[109]=4;[110]=6;[111]=6;[112]=0;[113]=1;[114]=2;[115]=3;[116]=4;[117]=5;[118]=6;[119]=7;[120]=0;[121]=1;[122]=2;[123]=3;[124]=4;[125]=5;[126]=6;[127]=7;[128]=0;[129]=0;[130]=0;[131]=0;[132]=0;[133]=0;[134]=0;[135]=0;[136]=8;[137]=8;[138]=8;[139]=8;[140]=8;[141]=8;[142]=8;[143]=8;[144]=0;[145]=1;[146]=0;[147]=1;[148]=0;[149]=1;[150]=0;[151]=1;[152]=8;[153]=9;[154]=8;[155]=9;[156]=8;[157]=9;[158]=8;[159]=9;[160]=0;[161]=0;[162]=2;[163]=2;[164]=0;[165]=0;[166]=2;[167]=2;[168]=8;[169]=8;[170]=10;[171]=10;[172]=8;[173]=8;[174]=10;[175]=10;[176]=0;[177]=1;[178]=2;[179]=3;[180]=0;[181]=1;[182]=2;[183]=3;[184]=8;[185]=9;[186]=10;[187]=11;[188]=8;[189]=9;[190]=10;[191]=11;[192]=0;[193]=0;[194]=0;[195]=0;[196]=4;[197]=4;[198]=4;[199]=4;[200]=8;[201]=8;[202]=8;[203]=8;[204]=12;[205]=12;[206]=12;[207]=12;[208]=0;[209]=1;[210]=0;[211]=1;[212]=4;[213]=5;[214]=4;[215]=5;[216]=8;[217]=9;[218]=8;[219]=9;[220]=12;[221]=13;[222]=12;[223]=13;[224]=0;[225]=0;[226]=2;[227]=2;[228]=4;[229]=4;[230]=6;[231]=6;[232]=8;[233]=8;[234]=10;[235]=10;[236]=12;[237]=12;[238]=14;[239]=14;[240]=0;[241]=1;[242]=2;[243]=3;[244]=4;[245]=5;[246]=6;[247]=7;[248]=8;[249]=9;[250]=10;[251]=11;[252]=12;[253]=13;[254]=14;[255]=15;} local not_map = {[0]=15;[1]=14;[2]=13;[3]=12;[4]=11;[5]=10;[6]=9;[7]=8;[8]=7;[9]=6;[10]=5;[11]=4;[12]=3;[13]=2;[14]=1;[15]=0;}; local rshift1_map = {[0]=0;[1]=0;[2]=1;[3]=1;[4]=2;[5]=2;[6]=3;[7]=3;[8]=4;[9]=4;[10]=5;[11]=5;[12]=6;[13]=6;[14]=7;[15]=7;}; local rshift1carry_map = {[0]=0;[1]=8;[2]=0;[3]=8;[4]=0;[5]=8;[6]=0;[7]=8;[8]=0;[9]=8;[10]=0;[11]=8;[12]=0;[13]=8;[14]=0;[15]=8;}; local lshift1_map = {[0]=0;[1]=2;[2]=4;[3]=6;[4]=8;[5]=10;[6]=12;[7]=14;[8]=0;[9]=2;[10]=4;[11]=6;[12]=8;[13]=10;[14]=12;[15]=14;}; local lshift1carry_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=1;[9]=1;[10]=1;[11]=1;[12]=1;[13]=1;[14]=1;[15]=1;}; local arshift1carry_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=8;[9]=8;[10]=8;[11]=8;[12]=8;[13]=8;[14]=8;[15]=8;}; module "bit" local bit_mt = {__tostring = function(t) return ("%x%x%x%x%x%x%x%x"):format(t[1],t[2],t[3],t[4],t[5],t[6],t[7],t[8]); end}; local function do_bop(a, b, op) return setmetatable({ op[a[1]*16+b[1]]; op[a[2]*16+b[2]]; op[a[3]*16+b[3]]; op[a[4]*16+b[4]]; op[a[5]*16+b[5]]; op[a[6]*16+b[6]]; op[a[7]*16+b[7]]; op[a[8]*16+b[8]]; }, bit_mt); end local function do_uop(a, op) return setmetatable({ op[a[1]]; op[a[2]]; op[a[3]]; op[a[4]]; op[a[5]]; op[a[6]]; op[a[7]]; op[a[8]]; }, bit_mt); end function bxor(a, b) return do_bop(a, b, xor_map); end function bor(a, b) return do_bop(a, b, or_map); end function band(a, b) return do_bop(a, b, and_map); end function bnot(a) return do_uop(a, not_map); end local function _rshift1(t) local carry = 0; for i=1,8 do local t_i = rshift1_map[t[i]] + carry; carry = rshift1carry_map[t[i]]; t[i] = t_i; end end function rshift(a, i) local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]}; for _ = 1, i do _rshift1(t); end return setmetatable(t, bit_mt); end local function _arshift1(t) local carry = arshift1carry_map[t[1]]; for i=1,8 do local t_i = rshift1_map[t[i]] + carry; carry = rshift1carry_map[t[i]]; t[i] = t_i; end end function arshift(a, i) local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]}; for _ = 1, i do _arshift1(t); end return setmetatable(t, bit_mt); end local function _lshift1(t) local carry = 0; for i=8,1,-1 do local t_i = lshift1_map[t[i]] + carry; carry = lshift1carry_map[t[i]]; t[i] = t_i; end end function lshift(a, i) local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]}; for _ = 1, i do _lshift1(t); end return setmetatable(t, bit_mt); end local function _cast(a) if type(a) == "number" then a = ("%x"):format(a); elseif type(a) == "table" then return a; elseif type(a) ~= "string" then error("string expected, got "..type(a), 2); end local t = {0,0,0,0,0,0,0,0}; a = "00000000"..a; a = a:sub(-8); for i = 1,8 do t[i] = tonumber(a:sub(i,i), 16) or error("Number format error", 2); end return setmetatable(t, bit_mt); end local function wrap1(f) return function(a, ...) if type(a) ~= "table" then a = _cast(a); end a = f(a, ...); a = tonumber(tostring(a), 16); if a > 0x7fffffff then a = a - 1 - 0xffffffff; end return a; end; end local function wrap2(f) return function(a, b, ...) if type(a) ~= "table" then a = _cast(a); end if type(b) ~= "table" then b = _cast(b); end a = f(a, b, ...); a = tonumber(tostring(a), 16); if a > 0x7fffffff then a = a - 1 - 0xffffffff; end return a; end; end bxor = wrap2(bxor); bor = wrap2(bor); band = wrap2(band); bnot = wrap1(bnot); lshift = wrap1(lshift); rshift = wrap1(rshift); arshift = wrap1(arshift); cast = wrap1(_cast); bits = 32; return _M; prosody-13.0.1/fallbacks/PaxHeaders/lxp.lua0000644000000000000000000000011714773555365015630 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.547710359 prosody-13.0.1/fallbacks/lxp.lua0000644000175000017500000000706314773555365020035 0ustar00prosodyprosody00000000000000 local coroutine = coroutine; local tonumber = tonumber; local string = string; local setmetatable, getmetatable = setmetatable, getmetatable; local pairs = pairs; local deadroutine = coroutine.create(function() end); coroutine.resume(deadroutine); module("lxp") local entity_map = setmetatable({ ["amp"] = "&"; ["gt"] = ">"; ["lt"] = "<"; ["apos"] = "'"; ["quot"] = "\""; }, {__index = function(_, s) if s:sub(1,1) == "#" then if s:sub(2,2) == "x" then return string.char(tonumber(s:sub(3), 16)); else return string.char(tonumber(s:sub(2))); end end end }); local function xml_unescape(str) return (str:gsub("&(.-);", entity_map)); end local function parse_tag(s) local name,sattr=(s):gmatch("([^%s]+)(.*)")(); local attr = {}; for a,b in (sattr):gmatch("([^=%s]+)=['\"]([^'\"]*)['\"]") do attr[a] = xml_unescape(b); end return name, attr; end local function parser(data, handlers, ns_separator) local function read_until(str) local pos = data:find(str, nil, true); while not pos do data = data..coroutine.yield(); pos = data:find(str, nil, true); end local r = data:sub(1, pos); data = data:sub(pos+1); return r; end local function read_before(str) local pos = data:find(str, nil, true); while not pos do data = data..coroutine.yield(); pos = data:find(str, nil, true); end local r = data:sub(1, pos-1); data = data:sub(pos); return r; end local function peek() while #data == 0 do data = coroutine.yield(); end return data:sub(1,1); end local ns = { xml = "http://www.w3.org/XML/1998/namespace" }; ns.__index = ns; local function apply_ns(name, dodefault) local prefix,n = name:match("^([^:]*):(.*)$"); if prefix and ns[prefix] then return ns[prefix]..ns_separator..n; end if dodefault and ns[""] then return ns[""]..ns_separator..name; end return name; end local function push(tag, attr) ns = setmetatable({}, ns); for k,v in pairs(attr) do local xmlns = k == "xmlns" and "" or k:match("^xmlns:(.*)$"); if xmlns then ns[xmlns] = v; attr[k] = nil; end end local newattr, n = {}, 0; for k,v in pairs(attr) do n = n+1; k = apply_ns(k); newattr[n] = k; newattr[k] = v; end tag = apply_ns(tag, true); ns[0] = tag; ns.__index = ns; return tag, newattr; end local function pop() local tag = ns[0]; ns = getmetatable(ns); return tag; end while true do if peek() == "<" then local elem = read_until(">"):sub(2,-2); if elem:sub(1,1) == "!" or elem:sub(1,1) == "?" then -- neglect comments and processing-instructions elseif elem:sub(1,1) == "/" then -- end tag elem = elem:sub(2); local name = pop(); handlers:EndElement(name); -- TODO check for start-end tag name match elseif elem:sub(-1,-1) == "/" then -- empty tag elem = elem:sub(1,-2); local name,attr = parse_tag(elem); name,attr = push(name,attr); handlers:StartElement(name,attr); name = pop(); handlers:EndElement(name); else -- start tag local name,attr = parse_tag(elem); name,attr = push(name,attr); handlers:StartElement(name,attr); end else local text = read_before("<"); handlers:CharacterData(xml_unescape(text)); end end end function new(handlers, ns_separator) local co = coroutine.create(parser); return { parse = function(self, data) if not data then co = deadroutine; return true; -- eof end local success, result = coroutine.resume(co, data, handlers, ns_separator); if result then co = deadroutine; return nil, result; -- error end return true; -- success end; }; end return _M; prosody-13.0.1/PaxHeaders/loader.lua0000644000000000000000000000011714773555365014351 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.547710359 prosody-13.0.1/loader.lua0000644000175000017500000000213114773555365016545 0ustar00prosodyprosody00000000000000-- 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; }) prosody-13.0.1/PaxHeaders/makefile0000644000000000000000000000011714773555365014100 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.547710359 prosody-13.0.1/makefile0000644000175000017500000001107414773555365016302 0ustar00prosodyprosody00000000000000 include config.unix BIN = $(DESTDIR)$(PREFIX)/bin CONFIG = $(DESTDIR)$(SYSCONFDIR) MODULES = $(DESTDIR)$(LIBDIR)/prosody/modules SOURCE = $(DESTDIR)$(LIBDIR)/prosody DATA = $(DESTDIR)$(DATADIR) MAN = $(DESTDIR)$(PREFIX)/share/man INSTALLEDSOURCE = $(LIBDIR)/prosody INSTALLEDCONFIG = $(SYSCONFDIR) INSTALLEDMODULES = $(LIBDIR)/prosody/modules INSTALLEDDATA = $(DATADIR) INSTALL=install -p INSTALL_DATA=$(INSTALL) -m644 INSTALL_EXEC=$(INSTALL) -m755 MKDIR=install -d MKDIR_PRIVATE=$(MKDIR) -m750 LUACHECK=luacheck BUSTED=busted .PHONY: all test clean install all: prosody.install prosodyctl.install prosody.cfg.lua.install prosody.version $(MAKE) -C util-src install .if $(EXCERTS) == "yes" $(MAKE) -C certs localhost.crt example.com.crt .endif install-etc: prosody.cfg.lua.install $(MKDIR) $(CONFIG) $(MKDIR) $(CONFIG)/certs test -f $(CONFIG)/prosody.cfg.lua || $(INSTALL_DATA) prosody.cfg.lua.install $(CONFIG)/prosody.cfg.lua .if $(EXCERTS) == "yes" $(INSTALL_DATA) certs/localhost.crt certs/localhost.key $(CONFIG)/certs $(INSTALL_DATA) certs/example.com.crt certs/example.com.key $(CONFIG)/certs .endif install-bin: prosody.install prosodyctl.install $(MKDIR) $(BIN) $(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 $(INSTALL_DATA) core/*.lua $(SOURCE)/core install-net: $(MKDIR) $(SOURCE) $(MKDIR) $(SOURCE)/net $(INSTALL_DATA) net/*.lua $(SOURCE)/net $(MKDIR) $(SOURCE)/net/http $(SOURCE)/net/resolvers $(SOURCE)/net/websocket $(INSTALL_DATA) net/http/*.lua $(SOURCE)/net/http $(INSTALL_DATA) net/resolvers/*.lua $(SOURCE)/net/resolvers $(INSTALL_DATA) net/websocket/*.lua $(SOURCE)/net/websocket install-util: util/encodings.so util/encodings.so util/pposix.so util/signal.so $(MKDIR) $(SOURCE) $(MKDIR) $(SOURCE)/util $(INSTALL_DATA) util/*.lua $(SOURCE)/util $(MAKE) install -C util-src $(INSTALL_DATA) util/*.so $(SOURCE)/util $(MKDIR) $(SOURCE)/util/sasl $(INSTALL_DATA) util/sasl/*.lua $(SOURCE)/util/sasl $(MKDIR) $(SOURCE)/util/human $(INSTALL_DATA) util/human/*.lua $(SOURCE)/util/human $(MKDIR) $(SOURCE)/util/prosodyctl $(INSTALL_DATA) util/prosodyctl/*.lua $(SOURCE)/util/prosodyctl install-plugins: $(MKDIR) $(MODULES) $(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 $(INSTALL_DATA) man/prosodyctl.man $(MAN)/man1/prosodyctl.1 install-meta: -test -f prosody.version && $(INSTALL_DATA) prosody.version $(SOURCE)/prosody.version 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-loader clean: rm -f prosody.install rm -f prosodyctl.install rm -f prosody.cfg.lua.install rm -f prosody.version $(MAKE) clean -C util-src lint: $(LUACHECK) -q $$(HGPLAIN= hg files -I '**.lua') prosody prosodyctl @echo $$(sed -n '/^\tlocal exclude_files/,/^}/p;' .luacheckrc | sed '1d;$d' | wc -l) files ignored shellcheck configure test: $(BUSTED) --lua=$(RUNWITH) prosody.install: prosody sed "1s| lua$$| $(RUNWITH)|; \ s|^CFG_SOURCEDIR=.*;$$|CFG_SOURCEDIR='$(INSTALLEDSOURCE)';|; \ s|^CFG_CONFIGDIR=.*;$$|CFG_CONFIGDIR='$(INSTALLEDCONFIG)';|; \ s|^CFG_DATADIR=.*;$$|CFG_DATADIR='$(INSTALLEDDATA)';|; \ s|^CFG_PLUGINDIR=.*;$$|CFG_PLUGINDIR='$(INSTALLEDMODULES)/';|;" < prosody > $@ prosodyctl.install: prosodyctl sed "1s| lua$$| $(RUNWITH)|; \ s|^CFG_SOURCEDIR=.*;$$|CFG_SOURCEDIR='$(INSTALLEDSOURCE)';|; \ s|^CFG_CONFIGDIR=.*;$$|CFG_CONFIGDIR='$(INSTALLEDCONFIG)';|; \ s|^CFG_DATADIR=.*;$$|CFG_DATADIR='$(INSTALLEDDATA)';|; \ s|^CFG_PLUGINDIR=.*;$$|CFG_PLUGINDIR='$(INSTALLEDMODULES)/';|;" < prosodyctl > $@ prosody.cfg.lua.install: prosody.cfg.lua.dist sed 's|certs/|$(INSTALLEDCONFIG)/certs/|' prosody.cfg.lua.dist > $@ prosody.version: if [ -f prosody.release ]; then \ cp prosody.release $@; \ elif [ -f .hg_archival.txt ]; then \ sed -n 's/^node: \(............\).*/\1/p' .hg_archival.txt > $@; \ elif [ -f .hg/dirstate ]; then \ hexdump -n6 -e'6/1 "%02x"' .hg/dirstate > $@; \ else \ echo unknown > $@; \ fi prosody-13.0.1/PaxHeaders/man0000644000000000000000000000013114773555365013072 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.547710359 30 ctime=1743706869.547710359 prosody-13.0.1/man/0000755000175000017500000000000014773555365015352 5ustar00prosodyprosody00000000000000prosody-13.0.1/man/PaxHeaders/Makefile0000644000000000000000000000011714773555365014613 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.547710359 prosody-13.0.1/man/Makefile0000644000175000017500000000010214773555365017003 0ustar00prosodyprosody00000000000000all: prosodyctl.man %.man: %.markdown pandoc -s -t man -o $@ $^ prosody-13.0.1/man/PaxHeaders/prosodyctl.man0000644000000000000000000000011714773555365016052 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.547710359 prosody-13.0.1/man/prosodyctl.man0000644000175000017500000001137414773555365020257 0ustar00prosodyprosody00000000000000.\" Automatically generated by Pandoc 2.17.0.1 .\" .TH "PROSODYCTL" "1" "2022-02-02" "" "" .hy .SH NAME .PP prosodyctl - Manage a Prosody XMPP server .SH SYNOPSIS .IP .nf \f[C] prosodyctl command [--help] \f[R] .fi .SH DESCRIPTION .PP prosodyctl is the control tool for the Prosody XMPP server. It may be used to control the server daemon and manage users. .PP prosodyctl needs to be executed with sufficient privileges to perform its commands. This typically means executing prosodyctl as the root user. If a user named \[lq]prosody\[rq] is found then prosodyctl will change to that user before executing its commands. .SH COMMANDS .SS User Management .PP In the following commands users are identified by a Jabber ID, jid, of the usual form: user\[at]domain. .TP adduser jid Adds a user with Jabber ID, jid, to the server. You will be prompted to enter the user\[cq]s password. .TP passwd jid Changes the password of an existing user with Jabber ID, jid. You will be prompted to enter the user\[cq]s new password. .TP deluser jid Deletes an existing user with Jabber ID, jid, from the server. .SS Daemon Management .PP Although prosodyctl has commands to manage the prosody daemon it is recommended that you utilize your distributions daemon management features if you attained Prosody through a package. .PP To perform daemon control commands prosodyctl needs a pidfile value specified in \f[C]/etc/prosody/prosody.cfg.lua\f[R]. Failure to do so will cause prosodyctl to complain. .TP start Starts the prosody server daemon. If run as root prosodyctl will attempt to change to a user named \[lq]prosody\[rq] before executing. This operation will block for up to five seconds to wait for the server to execute. .TP stop Stops the prosody server daemon. This operation will block for up to five seconds to wait for the server to stop executing. .TP restart Restarts the prosody server daemon. Equivalent to running prosodyctl stop followed by prosodyctl start. .TP reload Signals the prosody server daemon to reload configuration and reopen log files. .TP status Prints the current execution status of the prosody server daemon. .SS Certificates .PP prosodyctl can create self-signed certificates, certificate requests and private keys for use with Prosody. Commands are of the form \f[C]prosodyctl cert subcommand\f[R]. Commands take a list of hosts to be included in the certificate. .TP \f[B]\f[CB]request hosts\f[B]\f[R] Create a certificate request (CSR) file for submission to a certificate authority. Multiple hosts can be given, sub-domains are automatically included. .TP \f[B]\f[CB]generate hosts\f[B]\f[R] Generate a self-signed certificate. .TP \f[B]\f[CB]key host [size]\f[B]\f[R] Generate a private key of `size' bits (defaults to 2048). Invoked automatically by `request' and `generate' if needed. .TP \f[B]\f[CB]config hosts\f[B]\f[R] Produce a config file for the list of hosts. Invoked automatically by `request' and `generate' if needed. .TP \f[B]\f[CB]import hosts paths\f[B]\f[R] Copy certificates for hosts into the certificate path and reload prosody. .SS Debugging .PP prosodyctl can also show some information about the environment, dependencies and such to aid in debugging. .TP \f[B]\f[CB]about\f[B]\f[R] Shows environment, various paths used by Prosody and installed dependencies. .TP \f[B]\f[CB]check [what]\f[B]\f[R] Performs various sanity checks on the configuration, DNS setup and configured TLS certificates. \f[C]what\f[R] can be one of \f[C]config\f[R], \f[C]dns\f[R] \f[C]certs\f[R], \f[C]disabled\f[R] and \f[C]connectivity\f[R] to run only that check. .SS Ejabberd Compatibility .PP ejabberd is another XMPP server which provides a comparable control tool, ejabberdctl, to control its server\[cq]s operations. prosodyctl implements some commands which are compatible with ejabberdctl. For details of how these commands work you should see ejabberdctl(8). .IP .nf \f[C] register user server password unregister user server \f[R] .fi .SH OPTIONS .TP \f[B]\f[CB]--config filename\f[B]\f[R] Use the specified config file instead of the default. .TP \f[B]\f[CB]--root\f[B]\f[R] Don\[cq]t drop root privileges (e.g.\ when invoked with sudo). .TP \f[B]\f[CB]--help\f[B]\f[R] Display help text for the specified command. .TP \f[B]\f[CB]--verbose\f[B]\f[R] Increase log level to show debug messages. .TP \f[B]\f[CB]--quiet\f[B]\f[R] Reduce log level to only show errors. .TP \f[B]\f[CB]--silent\f[B]\f[R] Disable logging completely, leaving only command output. .SH FILES .TP \f[B]\f[CB]/etc/prosody/prosody.cfg.lua\f[B]\f[R] The main prosody configuration file. prosodyctl reads this to determine the process ID file of the prosody server daemon and to determine if a host has been configured. .SH ONLINE .PP More information may be found online at: .SH AUTHORS Dwayne Bent ; Kim Alvefur. prosody-13.0.1/man/PaxHeaders/prosodyctl.markdown0000644000000000000000000000011714773555365017121 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.547710359 prosody-13.0.1/man/prosodyctl.markdown0000644000175000017500000001057114773555365021324 0ustar00prosodyprosody00000000000000--- author: - Dwayne Bent - Kim Alvefur date: 2022-02-02 section: 1 title: PROSODYCTL --- # NAME prosodyctl - Manage a Prosody XMPP server # SYNOPSIS prosodyctl command [--help] # DESCRIPTION prosodyctl is the control tool for the Prosody XMPP server. It may be used to control the server daemon and manage users. prosodyctl needs to be executed with sufficient privileges to perform its commands. This typically means executing prosodyctl as the root user. If a user named "prosody" is found then prosodyctl will change to that user before executing its commands. # COMMANDS ## User Management In the following commands users are identified by a Jabber ID, jid, of the usual form: user@domain. adduser jid : Adds a user with Jabber ID, jid, to the server. You will be prompted to enter the user's password. passwd jid : Changes the password of an existing user with Jabber ID, jid. You will be prompted to enter the user's new password. deluser jid : Deletes an existing user with Jabber ID, jid, from the server. ## Daemon Management Although prosodyctl has commands to manage the prosody daemon it is recommended that you utilize your distributions daemon management features if you attained Prosody through a package. To perform daemon control commands prosodyctl needs a pidfile value specified in `/etc/prosody/prosody.cfg.lua`. Failure to do so will cause prosodyctl to complain. start : Starts the prosody server daemon. If run as root prosodyctl will attempt to change to a user named "prosody" before executing. This operation will block for up to five seconds to wait for the server to execute. stop : Stops the prosody server daemon. This operation will block for up to five seconds to wait for the server to stop executing. restart : Restarts the prosody server daemon. Equivalent to running prosodyctl stop followed by prosodyctl start. reload : Signals the prosody server daemon to reload configuration and reopen log files. status : Prints the current execution status of the prosody server daemon. ## Certificates prosodyctl can create self-signed certificates, certificate requests and private keys for use with Prosody. Commands are of the form `prosodyctl cert subcommand`. Commands take a list of hosts to be included in the certificate. `request hosts` : Create a certificate request (CSR) file for submission to a certificate authority. Multiple hosts can be given, sub-domains are automatically included. `generate hosts` : Generate a self-signed certificate. `key host [size]` : Generate a private key of 'size' bits (defaults to 2048). Invoked automatically by 'request' and 'generate' if needed. `config hosts` : Produce a config file for the list of hosts. Invoked automatically by 'request' and 'generate' if needed. `import hosts paths` : Copy certificates for hosts into the certificate path and reload prosody. ## Debugging prosodyctl can also show some information about the environment, dependencies and such to aid in debugging. `about` : Shows environment, various paths used by Prosody and installed dependencies. `check [what]` : Performs various sanity checks on the configuration, DNS setup and configured TLS certificates. `what` can be one of `config`, `dns` `certs`, `disabled` and `connectivity` to run only that check. ## Ejabberd Compatibility ejabberd is another XMPP server which provides a comparable control tool, ejabberdctl, to control its server's operations. prosodyctl implements some commands which are compatible with ejabberdctl. For details of how these commands work you should see ejabberdctl(8). register user server password unregister user server # OPTIONS `--config filename` : Use the specified config file instead of the default. `--root` : Don't drop root privileges (e.g. when invoked with sudo). `--help` : Display help text for the specified command. `--verbose` : Increase log level to show debug messages. `--quiet` : Reduce log level to only show errors. `--silent` : Disable logging completely, leaving only command output. # FILES `/etc/prosody/prosody.cfg.lua` : The main prosody configuration file. prosodyctl reads this to determine the process ID file of the prosody server daemon and to determine if a host has been configured. # ONLINE More information may be found online at: prosody-13.0.1/PaxHeaders/net0000644000000000000000000000013114773555365013105 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.551710374 30 ctime=1743706869.575710471 prosody-13.0.1/net/0000755000175000017500000000000014773555365015365 5ustar00prosodyprosody00000000000000prosody-13.0.1/net/PaxHeaders/adns.lua0000644000000000000000000000011714773555365014616 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.551710374 prosody-13.0.1/net/adns.lua0000644000175000017500000001066414773555365017024 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local server = require "prosody.net.server"; local new_resolver = require "prosody.net.dns".resolver; local promise = require "prosody.util.promise"; 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 local coroutine, pcall = coroutine, pcall; local setmetatable = setmetatable; local function dummy_send(sock, data, i, j) return (j-i)+1; end -- luacheck: ignore 212 local _ENV = nil; -- luacheck: std none local async_resolver_methods = {}; local async_resolver_mt = { __index = async_resolver_methods }; local query_methods = {}; local query_mt = { __index = query_methods }; local function new_async_socket(sock, resolver) local peername = ""; local listener = {}; local handler = {}; function listener.onincoming(conn, data) -- luacheck: ignore 212/conn if data then resolver:feed(handler, data); end end function listener.ondisconnect(conn, err) if err then log("warn", "DNS socket for %s disconnected: %s", peername, err); local servers = resolver.server; if resolver.socketset[conn] == resolver.best_server and resolver.best_server == #servers then log("warn", "Exhausted all %d configured DNS servers, next lookup will try %s again", #servers, servers[1]); end resolver:servfail(conn); -- Let the magic commence end end do local err; handler, err = server.wrapclient(sock, "dns", 53, listener); if not handler then return nil, err; end end if handler.set then -- server_epoll: only watch for incoming data -- avoids sending empty packet on first 'onwritable' event handler:set(true, false); end handler.settimeout = function () end handler.setsockname = function (_, ...) return sock:setsockname(...); end handler.setpeername = function (_, ...) peername = (...); local ret, err = sock:setpeername(...); _:set_send(dummy_send); return ret, err; end handler.connect = function (_, ...) return sock:connect(...) end --handler.send = function (_, data) _:write(data); return _.sendbuffer and _.sendbuffer(); end handler.send = function (_, data) log("debug", "Sending DNS query to %s", peername); return sock:send(data); end return handler; end local function measure(_qclass, _qtype) return measure; end function async_resolver_methods:lookup(handler, qname, qtype, qclass) local resolver = self._resolver; local m = measure(qclass or "IN", qtype or "A"); return coroutine.wrap(function (peek) if peek then log("debug", "Records for %s already cached, using those...", qname); m(); handler(peek); return; end log("debug", "Records for %s not in cache, sending query (%s)...", qname, coroutine.running()); local ok, err = resolver:query(qname, qtype, qclass); if ok then coroutine.yield(setmetatable({ resolver, qclass or "IN", qtype or "A", qname, coroutine.running()}, query_mt)); -- Wait for reply log("debug", "Reply for %s (%s)", qname, coroutine.running()); end if ok then m(); ok, err = pcall(handler, resolver:peek(qname, qtype, qclass)); else log("error", "Error sending DNS query: %s", err); ok, err = pcall(handler, nil, err); end if not ok then log("error", "Error in DNS response handler: %s", err); end end)(resolver:peek(qname, qtype, qclass)); end function async_resolver_methods:lookup_promise(qname, qtype, qclass) return promise.new(function (resolve, reject) local function handler(answer) if not answer then return reject(); end resolve(answer); end self:lookup(handler, qname, qtype, qclass); end); end function query_methods:cancel(call_handler, reason) -- luacheck: ignore 212/reason log("warn", "Cancelling DNS lookup for %s", self[4]); self[1].cancel(self[2], self[3], self[4], self[5], call_handler); end local function new_async_resolver() local resolver = new_resolver(); resolver:socket_wrapper_set(new_async_socket); return setmetatable({ _resolver = resolver}, async_resolver_mt); end return { lookup = function (...) return new_async_resolver():lookup(...); end; resolver = new_async_resolver; new_async_socket = new_async_socket; instrument = function(measure_) measure = measure_; end; }; prosody-13.0.1/net/PaxHeaders/connect.lua0000644000000000000000000000011714773555365015322 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.551710374 prosody-13.0.1/net/connect.lua0000644000175000017500000000745014773555365017527 0ustar00prosodyprosody00000000000000local 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"; -- FIXME RFC 6724 -- FIXME Error propagation from resolvers doesn't work -- FIXME #1428 Reuse DNS resolver object between service and basic resolver -- FIXME #1429 Close DNS resolver object when done local pending_connection_methods = {}; local pending_connection_mt = { __name = "pending_connection"; __index = pending_connection_methods; __tostring = function (p) return ""; end; }; function pending_connection_methods:log(level, message, ...) log(level, "[pending connection %s] "..message, self.id, ...); end -- pending_connections_map[conn] = pending_connection local pending_connections_map = {}; local pending_connection_listeners = {}; local function attempt_connection(p) p:log("debug", "Checking for targets..."); 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 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 p:log("debug", "Next target to try is %s:%d", ip, port); local conn, err = server.addclient(ip, port, pending_connection_listeners, p.options.pattern or "*a", extra and extra.sslctx or p.options.sslctx, conn_type, extra); if not conn then log("debug", "Connection attempt failed immediately: %s", err); p.last_error = err or "unknown reason"; return attempt_connection(p); end 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 function pending_connection_listeners.onconnect(conn) local p = pending_connections_map[conn]; if not p then log("warn", "Successful connection, but unexpected! Closing."); conn:close(); 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); end function pending_connection_listeners.ondisconnect(conn, reason) local p = pending_connections_map[conn]; if not p then 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); 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) local p = setmetatable({ id = new_id(); target_resolver = target_resolver; listeners = assert(listeners); options = options or {}; data = data; conns = {}; }, pending_connection_mt); p:log("debug", "Starting connection process"); attempt_connection(p); end return { connect = connect; }; prosody-13.0.1/net/PaxHeaders/cqueues.lua0000644000000000000000000000011714773555365015343 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.551710374 prosody-13.0.1/net/cqueues.lua0000644000175000017500000000220314773555365017537 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- This module allows you to use cqueues with a net.server mainloop -- local server = require "prosody.net.server"; local cqueues = require "cqueues"; local timer = require "prosody.util.timer"; assert(cqueues.VERSION >= 20150113, "cqueues newer than 20150113 required") -- Create a single top level cqueue local cq; if server.cq then -- server provides cqueues object cq = server.cq; elseif server.watchfd then cq = cqueues.new(); local timeout = timer.add_task(cq:timeout() or 0, function () -- FIXME It should be enough to reschedule this timeout instead of replacing it, but this does not work. See https://issues.prosody.im/1572 assert(cq:loop(0)); return cq:timeout(); end); server.watchfd(cq:pollfd(), function () assert(cq:loop(0)); local t = cq:timeout(); if t then timer.stop(timeout); timeout = timer.add_task(cq:timeout(), function () assert(cq:loop(0)); return cq:timeout(); end); end end); else error "NYI" end return { cq = cq; } prosody-13.0.1/net/PaxHeaders/dns.lua0000644000000000000000000000011714773555365014455 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.555710391 prosody-13.0.1/net/dns.lua0000644000175000017500000011043614773555365016661 0ustar00prosodyprosody00000000000000-- Prosody IM -- This file is included with Prosody IM. It has modifications, -- which are hereby placed in the public domain. -- todo: quick (default) header generation -- todo: nxdomain, error handling -- todo: cache results of encodeName -- 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, "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 "prosody.util.logger".init("dns"); local _, windows = pcall(require, "prosody.util.windows"); local is_windows = (_ and windows) or os.getenv("WINDIR"); local coroutine, io, math, string, table = coroutine, io, math, string, table; local ipairs, next, pairs, print, setmetatable, tostring, assert, error, select, type = ipairs, next, pairs, print, setmetatable, tostring, assert, error, select, type; local ztact = { -- public domain 20080404 lua@ztact.com get = function(parent, ...) local len = select('#', ...); for i=1,len do parent = parent[select(i, ...)]; if parent == nil then break; end end return parent; end; set = function(parent, ...) local len = select('#', ...); local key, value = select(len-1, ...); local cutpoint, cutkey; for i=1,len-2 do local key = select (i, ...) local child = parent[key] if value == nil then if child == nil then return; elseif next(child, next(child)) then cutpoint = nil; cutkey = nil; elseif cutpoint == nil then cutpoint = parent; cutkey = key; end elseif child == nil then child = {}; parent[key] = child; end parent = child end if value == nil and cutpoint then cutpoint[cutkey] = nil; else parent[key] = value; return value; end end; }; local get, set = ztact.get, ztact.set; local default_timeout = 5; local default_jitter = 1; local default_retry_jitter = 2; -------------------------------------------------- module dns local _ENV = nil; -- luacheck: std none local dns = {}; -- dns type & class codes ------------------------------ dns type & class codes local append = table.insert local function highbyte(i) -- - - - - - - - - - - - - - - - - - - highbyte return (i-(i%0x100))/0x100; end local function augment (t, prefix) -- - - - - - - - - - - - - - - - - augment local a = {}; for i,s in pairs(t) do a[i] = s; a[s] = s; a[string.lower(s)] = s; end setmetatable(a, { __index = function (_, i) if type(i) == "number" then return ("%s%d"):format(prefix, i); elseif type(i) == "string" then return i:upper(); end end; }) return a; end local function encode (t) -- - - - - - - - - - - - - - - - - - - - - encode local code = {}; for i,s in pairs(t) do local word = string.char(highbyte(i), i%0x100); code[i] = word; code[s] = word; code[string.lower(s)] = word; end return code; end dns.types = { [1] = "A", -- a host address,[RFC1035],, [2] = "NS", -- an authoritative name server,[RFC1035],, [3] = "MD", -- a mail destination (OBSOLETE - use MX),[RFC1035],, [4] = "MF", -- a mail forwarder (OBSOLETE - use MX),[RFC1035],, [5] = "CNAME", -- the canonical name for an alias,[RFC1035],, [6] = "SOA", -- marks the start of a zone of authority,[RFC1035],, [7] = "MB", -- a mailbox domain name (EXPERIMENTAL),[RFC1035],, [8] = "MG", -- a mail group member (EXPERIMENTAL),[RFC1035],, [9] = "MR", -- a mail rename domain name (EXPERIMENTAL),[RFC1035],, [10] = "NULL", -- a null RR (EXPERIMENTAL),[RFC1035],, [11] = "WKS", -- a well known service description,[RFC1035],, [12] = "PTR", -- a domain name pointer,[RFC1035],, [13] = "HINFO", -- host information,[RFC1035],, [14] = "MINFO", -- mailbox or mail list information,[RFC1035],, [15] = "MX", -- mail exchange,[RFC1035],, [16] = "TXT", -- text strings,[RFC1035],, [17] = "RP", -- for Responsible Person,[RFC1183],, [18] = "AFSDB", -- for AFS Data Base location,[RFC1183][RFC5864],, [19] = "X25", -- for X.25 PSDN address,[RFC1183],, [20] = "ISDN", -- for ISDN address,[RFC1183],, [21] = "RT", -- for Route Through,[RFC1183],, [22] = "NSAP", -- "for NSAP address, NSAP style A record",[RFC1706],, [23] = "NSAP-PTR", -- "for domain name pointer, NSAP style",[RFC1348][RFC1637][RFC1706],, [24] = "SIG", -- for security signature,[RFC4034][RFC3755][RFC2535][RFC2536][RFC2537][RFC2931][RFC3110][RFC3008],, [25] = "KEY", -- for security key,[RFC4034][RFC3755][RFC2535][RFC2536][RFC2537][RFC2539][RFC3008][RFC3110],, [26] = "PX", -- X.400 mail mapping information,[RFC2163],, [27] = "GPOS", -- Geographical Position,[RFC1712],, [28] = "AAAA", -- IP6 Address,[RFC3596],, [29] = "LOC", -- Location Information,[RFC1876],, [30] = "NXT", -- Next Domain (OBSOLETE),[RFC3755][RFC2535],, [31] = "EID", -- Endpoint Identifier,[Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt],,1995-06 [32] = "NIMLOC", -- Nimrod Locator,[1][Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt],,1995-06 [33] = "SRV", -- Server Selection,[1][RFC2782],, [34] = "ATMA", -- ATM Address,"[ ATM Forum Technical Committee, ""ATM Name System, V2.0"", Doc ID: AF-DANS-0152.000, July 2000. Available from and held in escrow by IANA.]",, [35] = "NAPTR", -- Naming Authority Pointer,[RFC2915][RFC2168][RFC3403],, [36] = "KX", -- Key Exchanger,[RFC2230],, [37] = "CERT", -- CERT,[RFC4398],, [38] = "A6", -- A6 (OBSOLETE - use AAAA),[RFC3226][RFC2874][RFC6563],, [39] = "DNAME", -- DNAME,[RFC6672],, [40] = "SINK", -- SINK,[Donald_E_Eastlake][http://tools.ietf.org/html/draft-eastlake-kitchen-sink],,1997-11 [41] = "OPT", -- OPT,[RFC6891][RFC3225],, [42] = "APL", -- APL,[RFC3123],, [43] = "DS", -- Delegation Signer,[RFC4034][RFC3658],, [44] = "SSHFP", -- SSH Key Fingerprint,[RFC4255],, [45] = "IPSECKEY", -- IPSECKEY,[RFC4025],, [46] = "RRSIG", -- RRSIG,[RFC4034][RFC3755],, [47] = "NSEC", -- NSEC,[RFC4034][RFC3755],, [48] = "DNSKEY", -- DNSKEY,[RFC4034][RFC3755],, [49] = "DHCID", -- DHCID,[RFC4701],, [50] = "NSEC3", -- NSEC3,[RFC5155],, [51] = "NSEC3PARAM", -- NSEC3PARAM,[RFC5155],, [52] = "TLSA", -- TLSA,[RFC6698],, [53] = "SMIMEA", -- S/MIME cert association,[RFC8162],SMIMEA/smimea-completed-template,2015-12-01 -- [54] = "Unassigned", -- ,,, [55] = "HIP", -- Host Identity Protocol,[RFC8005],, [56] = "NINFO", -- NINFO,[Jim_Reid],NINFO/ninfo-completed-template,2008-01-21 [57] = "RKEY", -- RKEY,[Jim_Reid],RKEY/rkey-completed-template,2008-01-21 [58] = "TALINK", -- Trust Anchor LINK,[Wouter_Wijngaards],TALINK/talink-completed-template,2010-02-17 [59] = "CDS", -- Child DS,[RFC7344],CDS/cds-completed-template,2011-06-06 [60] = "CDNSKEY", -- DNSKEY(s) the Child wants reflected in DS,[RFC7344],,2014-06-16 [61] = "OPENPGPKEY", -- OpenPGP Key,[RFC7929],OPENPGPKEY/openpgpkey-completed-template,2014-08-12 [62] = "CSYNC", -- Child-To-Parent Synchronization,[RFC7477],,2015-01-27 -- [63 .. 98] = "Unassigned", -- ,,, [99] = "SPF", -- ,[RFC7208],, [100] = "UINFO", -- ,[IANA-Reserved],, [101] = "UID", -- ,[IANA-Reserved],, [102] = "GID", -- ,[IANA-Reserved],, [103] = "UNSPEC", -- ,[IANA-Reserved],, [104] = "NID", -- ,[RFC6742],ILNP/nid-completed-template, [105] = "L32", -- ,[RFC6742],ILNP/l32-completed-template, [106] = "L64", -- ,[RFC6742],ILNP/l64-completed-template, [107] = "LP", -- ,[RFC6742],ILNP/lp-completed-template, [108] = "EUI48", -- an EUI-48 address,[RFC7043],EUI48/eui48-completed-template,2013-03-27 [109] = "EUI64", -- an EUI-64 address,[RFC7043],EUI64/eui64-completed-template,2013-03-27 -- [110 .. 248] = "Unassigned", -- ,,, [249] = "TKEY", -- Transaction Key,[RFC2930],, [250] = "TSIG", -- Transaction Signature,[RFC2845],, [251] = "IXFR", -- incremental transfer,[RFC1995],, [252] = "AXFR", -- transfer of an entire zone,[RFC1035][RFC5936],, [253] = "MAILB", -- "mailbox-related RRs (MB, MG or MR)",[RFC1035],, [254] = "MAILA", -- mail agent RRs (OBSOLETE - see MX),[RFC1035],, [255] = "*", -- A request for all records the server/cache has available,[RFC1035][RFC6895],, [256] = "URI", -- URI,[RFC7553],URI/uri-completed-template,2011-02-22 [257] = "CAA", -- Certification Authority Restriction,[RFC6844],CAA/caa-completed-template,2011-04-07 [258] = "AVC", -- Application Visibility and Control,[Wolfgang_Riedel],AVC/avc-completed-template,2016-02-26 [259] = "DOA", -- Digital Object Architecture,[draft-durand-doa-over-dns],DOA/doa-completed-template,2017-08-30 -- [260 .. 32767] = "Unassigned", -- ,,, [32768] = "TA", -- DNSSEC Trust Authorities,"[Sam_Weiler][http://cameo.library.cmu.edu/][ Deploying DNSSEC Without a Signed Root. Technical Report 1999-19, Information Networking Institute, Carnegie Mellon University, April 2004.]",,2005-12-13 [32769] = "DLV", -- DNSSEC Lookaside Validation,[RFC4431],, -- [32770 .. 65279] = "Unassigned", -- ,,, -- [65280 .. 65534] = "Private use", -- ,,, -- [65535] = "Reserved", -- ,,, } dns.classes = { 'IN', 'CS', 'CH', 'HS', [255] = '*' }; dns.type = augment (dns.types, "TYPE"); dns.class = augment (dns.classes, "CLASS"); dns.typecode = encode (dns.types); dns.classcode = encode (dns.classes); local function standardize(qname, qtype, qclass) -- - - - - - - standardize if string.byte(qname, -1) ~= 0x2E then qname = qname..'.'; end qname = string.lower(qname); return qname, dns.type[qtype or 'A'], dns.class[qclass or 'IN']; end local function prune(rrs, time, soft) -- - - - - - - - - - - - - - - prune time = time or socket.gettime(); for i,rr in ipairs(rrs) do if rr.tod then if rr.tod < time then rrs[rr[rr.type:lower()]] = nil; table.remove(rrs, i); return prune(rrs, time, soft); -- Re-iterate end elseif soft == 'soft' then -- What is this? I forget! assert(rr.ttl == 0); rrs[rr[rr.type:lower()]] = nil; table.remove(rrs, i); end end end -- metatables & co. ------------------------------------------ metatables & co. local resolver = {}; resolver.__index = resolver; resolver.timeout = default_timeout; local function default_rr_tostring(rr) local rr_val = rr.type and rr[rr.type:lower()]; if type(rr_val) ~= "string" then return ""; end return rr_val; end local special_tostrings = { LOC = resolver.LOC_tostring; MX = function (rr) return string.format('%2i %s', rr.pref, rr.mx); end; SRV = function (rr) local s = rr.srv; return string.format('%5d %5d %5d %s', s.priority, s.weight, s.port, s.target); end; }; local rr_metatable = {}; -- - - - - - - - - - - - - - - - - - - rr_metatable function rr_metatable.__tostring(rr) local rr_string = (special_tostrings[rr.type] or default_rr_tostring)(rr); return string.format('%2s %-5s %6i %-28s %s', rr.class, rr.type, rr.ttl, rr.name, rr_string); end local rrs_metatable = {}; -- - - - - - - - - - - - - - - - - - rrs_metatable function rrs_metatable.__tostring(rrs) local t = {}; for _, rr in ipairs(rrs) do append(t, tostring(rr)..'\n'); end return table.concat(t); end local cache_metatable = {}; -- - - - - - - - - - - - - - - - cache_metatable function cache_metatable.__tostring(cache) local time = socket.gettime(); local t = {}; for class,types in pairs(cache) do for type,names in pairs(types) do for name,rrs in pairs(names) do prune(rrs, time); append(t, tostring(rrs)); end end end return table.concat(t); end -- packet layer -------------------------------------------------- packet layer function dns.random(...) -- - - - - - - - - - - - - - - - - - - dns.random math.randomseed(math.floor(10000*socket.gettime()) % 0x80000000); dns.random = math.random; return dns.random(...); end local function encodeHeader(o) -- - - - - - - - - - - - - - - encodeHeader o = o or {}; o.id = o.id or dns.random(0, 0xffff); -- 16b (random) id o.rd = o.rd or 1; -- 1b 1 recursion desired o.tc = o.tc or 0; -- 1b 1 truncated response o.aa = o.aa or 0; -- 1b 1 authoritative response o.opcode = o.opcode or 0; -- 4b 0 query -- 1 inverse query -- 2 server status request -- 3-15 reserved o.qr = o.qr or 0; -- 1b 0 query, 1 response o.rcode = o.rcode or 0; -- 4b 0 no error -- 1 format error -- 2 server failure -- 3 name error -- 4 not implemented -- 5 refused -- 6-15 reserved o.z = o.z or 0; -- 3b 0 reserved o.ra = o.ra or 0; -- 1b 1 recursion available o.qdcount = o.qdcount or 1; -- 16b number of question RRs o.ancount = o.ancount or 0; -- 16b number of answers RRs o.nscount = o.nscount or 0; -- 16b number of nameservers RRs o.arcount = o.arcount or 0; -- 16b number of additional RRs -- string.char() rounds, so prevent roundup with -0.4999 local header = string.char( highbyte(o.id), o.id %0x100, o.rd + 2*o.tc + 4*o.aa + 8*o.opcode + 128*o.qr, o.rcode + 16*o.z + 128*o.ra, highbyte(o.qdcount), o.qdcount %0x100, highbyte(o.ancount), o.ancount %0x100, highbyte(o.nscount), o.nscount %0x100, highbyte(o.arcount), o.arcount %0x100 ); return header, o.id; end local function encodeName(name) -- - - - - - - - - - - - - - - - encodeName local t = {}; for part in string.gmatch(name, '[^.]+') do append(t, string.char(string.len(part))); append(t, part); end append(t, string.char(0)); return table.concat(t); end local function encodeQuestion(qname, qtype, qclass) -- - - - encodeQuestion qname = encodeName(qname); qtype = dns.typecode[qtype or 'a']; qclass = dns.classcode[qclass or 'in']; return qname..qtype..qclass; end function resolver:byte(len) -- - - - - - - - - - - - - - - - - - - - - byte len = len or 1; local offset = self.offset; local last = offset + len - 1; if last > #self.packet then error(string.format('out of bounds: %i>%i', last, #self.packet)); end self.offset = offset + len; return string.byte(self.packet, offset, last); end function resolver:word() -- - - - - - - - - - - - - - - - - - - - - - word local b1, b2 = self:byte(2); return 0x100*b1 + b2; end function resolver:dword () -- - - - - - - - - - - - - - - - - - - - - dword local b1, b2, b3, b4 = self:byte(4); --print('dword', b1, b2, b3, b4); return 0x1000000*b1 + 0x10000*b2 + 0x100*b3 + b4; end function resolver:sub(len) -- - - - - - - - - - - - - - - - - - - - - - sub len = len or 1; local s = string.sub(self.packet, self.offset, self.offset + len - 1); self.offset = self.offset + len; return s; end function resolver:header(force) -- - - - - - - - - - - - - - - - - - header local id = self:word(); --print(string.format(':header id %x', id)); if not self.active[id] and not force then return nil; end local h = { id = id }; local b1, b2 = self:byte(2); h.rd = b1 %2; h.tc = b1 /2%2; h.aa = b1 /4%2; h.opcode = b1 /8%16; h.qr = b1 /128; h.rcode = b2 %16; h.z = b2 /16%8; h.ra = b2 /128; h.qdcount = self:word(); h.ancount = self:word(); h.nscount = self:word(); h.arcount = self:word(); for k,v in pairs(h) do h[k] = v-v%1; end return h; end function resolver:name() -- - - - - - - - - - - - - - - - - - - - - - name local remember, pointers = nil, 0; local len = self:byte(); local n = {}; if len == 0 then return "." end -- Root label while len > 0 do if len >= 0xc0 then -- name is "compressed" pointers = pointers + 1; if pointers >= 20 then error('dns error: 20 pointers'); end; local offset = ((len-0xc0)*0x100) + self:byte(); remember = remember or self.offset; self.offset = offset + 1; -- +1 for lua else -- name is not compressed append(n, self:sub(len)..'.'); end len = self:byte(); end self.offset = remember or self.offset; return table.concat(n); end function resolver:question() -- - - - - - - - - - - - - - - - - - question local q = {}; q.name = self:name(); q.type = dns.type[self:word()]; q.class = dns.class[self:word()]; return q; end function resolver:A(rr) -- - - - - - - - - - - - - - - - - - - - - - - - A local b1, b2, b3, b4 = self:byte(4); rr.a = string.format('%i.%i.%i.%i', b1, b2, b3, b4); end if have_util_net and util_net.ntop then function resolver:A(rr) rr.a = util_net.ntop(self:sub(4)); end end function resolver:AAAA(rr) local addr = {}; for _ = 1, rr.rdlength, 2 do local b1, b2 = self:byte(2); table.insert(addr, ("%02x%02x"):format(b1, b2)); end addr = table.concat(addr, ":"):gsub("%f[%x]0+(%x)","%1"); local zeros = {}; for item in addr:gmatch(":[0:]+:[0:]+:") do table.insert(zeros, item) end if #zeros == 0 then rr.aaaa = addr; return elseif #zeros > 1 then table.sort(zeros, function(a, b) return #a > #b end); end rr.aaaa = addr:gsub(zeros[1], "::", 1):gsub("^0::", "::"):gsub("::0$", "::"); end if have_util_net and util_net.ntop then function resolver:AAAA(rr) rr.aaaa = util_net.ntop(self:sub(16)); end end function resolver:CNAME(rr) -- - - - - - - - - - - - - - - - - - - - CNAME rr.cname = self:name(); end function resolver:MX(rr) -- - - - - - - - - - - - - - - - - - - - - - - MX rr.pref = self:word(); rr.mx = self:name(); end function resolver:LOC_nibble_power() -- - - - - - - - - - LOC_nibble_power local b = self:byte(); --print('nibbles', ((b-(b%0x10))/0x10), (b%0x10)); return ((b-(b%0x10))/0x10) * (10^(b%0x10)); end function resolver:LOC(rr) -- - - - - - - - - - - - - - - - - - - - - - LOC rr.version = self:byte(); if rr.version == 0 then rr.loc = rr.loc or {}; rr.loc.size = self:LOC_nibble_power(); rr.loc.horiz_pre = self:LOC_nibble_power(); rr.loc.vert_pre = self:LOC_nibble_power(); rr.loc.latitude = self:dword(); rr.loc.longitude = self:dword(); rr.loc.altitude = self:dword(); end end local function LOC_tostring_degrees(f, pos, neg) -- - - - - - - - - - - - - f = f - 0x80000000; if f < 0 then pos = neg; f = -f; end local deg, min, msec; msec = f%60000; f = (f-msec)/60000; min = f%60; deg = (f-min)/60; return string.format('%3d %2d %2.3f %s', deg, min, msec/1000, pos); end function resolver.LOC_tostring(rr) -- - - - - - - - - - - - - LOC_tostring local t = {}; --[[ for k,name in pairs { 'size', 'horiz_pre', 'vert_pre', 'latitude', 'longitude', 'altitude' } do append(t, string.format('%4s%-10s: %12.0f\n', '', name, rr.loc[name])); end --]] append(t, string.format( '%s %s %.2fm %.2fm %.2fm %.2fm', LOC_tostring_degrees (rr.loc.latitude, 'N', 'S'), LOC_tostring_degrees (rr.loc.longitude, 'E', 'W'), (rr.loc.altitude - 10000000) / 100, rr.loc.size / 100, rr.loc.horiz_pre / 100, rr.loc.vert_pre / 100 )); return table.concat(t); end function resolver:NS(rr) -- - - - - - - - - - - - - - - - - - - - - - - NS rr.ns = self:name(); end function resolver:SOA(rr) -- - - - - - - - - - - - - - - - - - - - - - SOA end function resolver:SRV(rr) -- - - - - - - - - - - - - - - - - - - - - - SRV rr.srv = {}; rr.srv.priority = self:word(); rr.srv.weight = self:word(); rr.srv.port = self:word(); rr.srv.target = self:name(); end function resolver:PTR(rr) rr.ptr = self:name(); end function resolver:TXT(rr) -- - - - - - - - - - - - - - - - - - - - - - TXT rr.txt = self:sub (self:byte()); end function resolver:rr() -- - - - - - - - - - - - - - - - - - - - - - - - rr local rr = {}; setmetatable(rr, rr_metatable); rr.name = self:name(self); rr.type = dns.type[self:word()] or rr.type; rr.class = dns.class[self:word()] or rr.class; rr.ttl = 0x10000*self:word() + self:word(); rr.rdlength = self:word(); rr.tod = self.time + math.max(rr.ttl, 1); local remember = self.offset; local rr_parser = self[dns.type[rr.type]]; if rr_parser then rr_parser(self, rr); end self.offset = remember; rr.rdata = self:sub(rr.rdlength); return rr; end function resolver:rrs (count) -- - - - - - - - - - - - - - - - - - - - - rrs local rrs = {}; for _ = 1, count do append(rrs, self:rr()); end return rrs; end function resolver:decode(packet, force) -- - - - - - - - - - - - - - decode self.packet, self.offset = packet, 1; local header = self:header(force); if not header then return nil; end local response = { header = header }; response.question = {}; local offset = self.offset; for _ = 1, response.header.qdcount do append(response.question, self:question()); end response.question.raw = string.sub(self.packet, offset, self.offset - 1); if not force then if not self.active[response.header.id] or not self.active[response.header.id][response.question.raw] then self.active[response.header.id] = nil; return nil; end end response.answer = self:rrs(response.header.ancount); response.authority = self:rrs(response.header.nscount); response.additional = self:rrs(response.header.arcount); return response; end -- socket layer -------------------------------------------------- socket layer resolver.delays = { 1, 2, 3, 5 }; resolver.jitter = have_timer and default_jitter or nil; resolver.retry_jitter = have_timer and default_retry_jitter or nil; function resolver:addnameserver(address) -- - - - - - - - - - addnameserver self.server = self.server or {}; append(self.server, address); end function resolver:setnameserver(address) -- - - - - - - - - - setnameserver self.server = {}; self:addnameserver(address); end function resolver:adddefaultnameservers() -- - - - - adddefaultnameservers if is_windows then if windows and windows.get_nameservers then for _, server in ipairs(windows.get_nameservers()) do self:addnameserver(server); end end if not self.server or #self.server == 0 then -- TODO log warning about no nameservers, adding opendns servers as fallback self:addnameserver("208.67.222.222"); self:addnameserver("208.67.220.220"); end else -- posix local resolv_conf = io.open("/etc/resolv.conf"); if resolv_conf then for line in resolv_conf:lines() do line = line:gsub("#.*$", "") :match('^%s*nameserver%s+([%x:%.]*%%?%S*)%s*$'); if line then local ip = new_ip(line); if ip then self:addnameserver(ip.addr); end end end resolv_conf:close(); end if not self.server or #self.server == 0 then -- TODO log warning about no nameservers, adding localhost as the default nameserver self:addnameserver("127.0.0.1"); end end end function resolver:getsocket(servernum) -- - - - - - - - - - - - - getsocket self.socket = self.socket or {}; self.socketset = self.socketset or {}; local sock = self.socket[servernum]; if sock then return sock; end local ok, err; local peer = self.server[servernum]; if peer:find(":") then sock, err = socket.udp6(); else sock, err = (socket.udp4 or socket.udp)(); end if sock and self.socket_wrapper then sock, err = self.socket_wrapper(sock, self); end if not sock then return nil, err; end sock:settimeout(0); -- todo: attempt to use a random port, fallback to 0 self.socket[servernum] = sock; self.socketset[sock] = servernum; -- set{sock,peer}name can fail, eg because of local routing table -- if so, try the next server ok, err = sock:setsockname('*', 0); if not ok then return self:servfail(sock, err); end ok, err = sock:setpeername(peer, 53); if not ok then return self:servfail(sock, err); end return sock; end function resolver:voidsocket(sock) if self.socket[sock] then self.socketset[self.socket[sock]] = nil; self.socket[sock] = nil; elseif self.socketset[sock] then self.socket[self.socketset[sock]] = nil; self.socketset[sock] = nil; end sock:close(); end function resolver:socket_wrapper_set(func) -- - - - - - - socket_wrapper_set self.socket_wrapper = func; end function resolver:closeall () -- - - - - - - - - - - - - - - - - - closeall for i,sock in ipairs(self.socket) do self.socket[i] = nil; self.socketset[sock] = nil; sock:close(); end end function resolver:remember(rr, type) -- - - - - - - - - - - - - - remember --print ('remember', type, rr.class, rr.type, rr.name) local qname, qtype, qclass = standardize(rr.name, rr.type, rr.class); if type ~= '*' then type = qtype; local all = get(self.cache, qclass, '*', qname); --print('remember all', all); if all then append(all, rr); end end self.cache = self.cache or setmetatable({}, cache_metatable); local rrs = get(self.cache, qclass, type, qname) or set(self.cache, qclass, type, qname, setmetatable({}, rrs_metatable)); if rr[qtype:lower()] and not rrs[rr[qtype:lower()]] then rrs[rr[qtype:lower()]] = true; append(rrs, rr); end if type == 'MX' then self.unsorted[rrs] = true; end end local function comp_mx(a, b) -- - - - - - - - - - - - - - - - - - - comp_mx return (a.pref == b.pref) and (a.mx < b.mx) or (a.pref < b.pref); end function resolver:peek (qname, qtype, qclass, n) -- - - - - - - - - - - - peek qname, qtype, qclass = standardize(qname, qtype, qclass); local rrs = get(self.cache, qclass, qtype, qname); if not rrs then if n then if n <= 0 then return end else n = 3 end rrs = get(self.cache, qclass, "CNAME", qname); if not (rrs and rrs[1]) then return end return self:peek(rrs[1].cname, qtype, qclass, n - 1); end if prune(rrs, socket.gettime()) and qtype == '*' or not next(rrs) then set(self.cache, qclass, qtype, qname, nil); return nil; end if self.unsorted[rrs] then table.sort (rrs, comp_mx); self.unsorted[rrs] = nil; end return rrs; end function resolver:purge(soft) -- - - - - - - - - - - - - - - - - - - purge if soft == 'soft' then self.time = socket.gettime(); for class,types in pairs(self.cache or {}) do for type,names in pairs(types) do for name,rrs in pairs(names) do prune(rrs, self.time, 'soft') end end end else self.cache = setmetatable({}, cache_metatable); end end function resolver:query(qname, qtype, qclass) -- - - - - - - - - - -- query qname, qtype, qclass = standardize(qname, qtype, qclass) local co = coroutine.running(); local q = get(self.wanted, qclass, qtype, qname); if co and q then -- We are already waiting for a reply to an identical query. set(self.wanted, qclass, qtype, qname, co, true); return true; end if not self.server then self:adddefaultnameservers(); end local question = encodeQuestion(qname, qtype, qclass); local peek = self:peek (qname, qtype, qclass); if peek then return peek; end local header, id = encodeHeader(); --print ('query id', id, qclass, qtype, qname) local o = { packet = header..question, server = self.best_server, delay = 1, retry = socket.gettime() + self.delays[1]; qclass = qclass; qtype = qtype; qname = qname; }; -- remember the query self.active[id] = self.active[id] or {}; self.active[id][question] = o; local conn, err = self:getsocket(o.server) if not conn then return nil, err; end if self.jitter then timer.add_task(math.random()*self.jitter, function () conn:send(o.packet); end); else conn:send(o.packet); end -- remember which coroutine wants the answer if co then set(self.wanted, qclass, qtype, qname, co, true); end if have_timer and self.timeout then local num_servers = #self.server; local i = 1; timer.add_task(self.timeout, function () if get(self.wanted, qclass, qtype, qname, co) then log("debug", "DNS request timeout %d/%d", i, num_servers) i = i + 1; self:servfail(self.socket[o.server]); -- end end -- Still outstanding? (i.e. retried) if get(self.wanted, qclass, qtype, qname, co) then return self.timeout; -- Then wait end end) end return true; end function resolver:servfail(sock, err) -- Resend all queries for this server local num = self.socketset[sock] -- Socket is dead now sock = self:voidsocket(sock); -- Find all requests to the down server, and retry on the next server self.time = socket.gettime(); log("debug", "servfail %d (of %d)", num, #self.server); for id,queries in pairs(self.active) do for question,o in pairs(queries) do if o.server == num then -- This request was to the broken server o.server = o.server + 1 -- Use next server if o.server > #self.server then o.server = 1; end o.retries = (o.retries or 0) + 1; local retried; if o.retries < #self.server then sock, err = self:getsocket(o.server); if sock then retried = true; if self.retry_jitter then local delay = self.delays[((o.retries-1)%#self.delays)+1] + (math.random()*self.retry_jitter); log("debug", "retry %d in %0.2fs", o.retries, delay); timer.add_task(delay, function () sock:send(o.packet); end); else log("debug", "retry %d (immediate)", o.retries); sock:send(o.packet); end end end if not retried then log("debug", 'tried all servers, giving up'); self:cancel(o.qclass, o.qtype, o.qname); queries[question] = nil; end end end if next(queries) == nil then self.active[id] = nil; end end if num == self.best_server then self.best_server = self.best_server + 1; if self.best_server > #self.server then -- Exhausted all servers, try first again self.best_server = 1; end end return sock, err; end function resolver:settimeout(seconds) self.timeout = seconds; end function resolver:receive(rset) -- - - - - - - - - - - - - - - - - receive --print('receive'); print(self.socket); self.time = socket.gettime(); rset = rset or self.socket; local response; for _, sock in pairs(rset) do if self.socketset[sock] then local packet = sock:receive(); if packet then response = self:decode(packet); if response and self.active[response.header.id] and self.active[response.header.id][response.question.raw] then --print('received response'); --self.print(response); for _, rr in pairs(response.answer) do if rr.name:sub(-#response.question[1].name, -1) == response.question[1].name then self:remember(rr, response.question[1].type) end end -- retire the query local queries = self.active[response.header.id]; queries[response.question.raw] = nil; if not next(queries) then self.active[response.header.id] = nil; end if not next(self.active) then self:closeall(); end -- was the query on the wanted list? local q = response.question[1]; local cos = get(self.wanted, q.class, q.type, q.name); if cos then for co in pairs(cos) do if coroutine.status(co) == "suspended" then coroutine.resume(co); end end set(self.wanted, q.class, q.type, q.name, nil); end end end end end return response; end function resolver:feed(sock, packet, force) --print('receive'); print(self.socket); self.time = socket.gettime(); local response = self:decode(packet, force); if response and self.active[response.header.id] and self.active[response.header.id][response.question.raw] then --print('received response'); --self.print(response); for _, rr in pairs(response.answer) do self:remember(rr, rr.type); end for _, rr in pairs(response.additional) do self:remember(rr, rr.type); end -- retire the query local queries = self.active[response.header.id]; queries[response.question.raw] = nil; if not next(queries) then self.active[response.header.id] = nil; end if not next(self.active) then self:closeall(); end -- was the query on the wanted list? local q = response.question[1]; if q then local cos = get(self.wanted, q.class, q.type, q.name); if cos then for co in pairs(cos) do if coroutine.status(co) == "suspended" then coroutine.resume(co); end end set(self.wanted, q.class, q.type, q.name, nil); end end end return response; end function resolver:cancel(qclass, qtype, qname) local cos = get(self.wanted, qclass, qtype, qname); if cos then for co in pairs(cos) do if coroutine.status(co) == "suspended" then coroutine.resume(co); end end set(self.wanted, qclass, qtype, qname, nil); end end function resolver:pulse() -- - - - - - - - - - - - - - - - - - - - - pulse --print(':pulse'); while self:receive() do end if not next(self.active) then return nil; end self.time = socket.gettime(); for id,queries in pairs(self.active) do for question,o in pairs(queries) do if self.time >= o.retry then o.server = o.server + 1; if o.server > #self.server then o.server = 1; o.delay = o.delay + 1; end if o.delay > #self.delays then --print('timeout'); queries[question] = nil; if not next(queries) then self.active[id] = nil; end if not next(self.active) then return nil; end else --print('retry', o.server, o.delay); local _a = self.socket[o.server]; if _a then _a:send(o.packet); end o.retry = self.time + self.delays[o.delay]; end end end end if next(self.active) then return true; end return nil; end function resolver:lookup(qname, qtype, qclass) -- - - - - - - - - - lookup self:query (qname, qtype, qclass) while self:pulse() do local recvt = {} for i, s in ipairs(self.socket) do recvt[i] = s end socket.select(recvt, nil, 4) end --print(self.cache); return self:peek(qname, qtype, qclass); end function resolver:lookupex(handler, qname, qtype, qclass) -- - - - - - - - - - lookup return self:peek(qname, qtype, qclass) or self:query(qname, qtype, qclass); end function resolver:tohostname(ip) return dns.lookup(ip:gsub("(%d+)%.(%d+)%.(%d+)%.(%d+)", "%4.%3.%2.%1.in-addr.arpa."), "PTR"); end --print ---------------------------------------------------------------- print local hints = { -- - - - - - - - - - - - - - - - - - - - - - - - - - - hints qr = { [0]='query', 'response' }, opcode = { [0]='query', 'inverse query', 'server status request' }, aa = { [0]='non-authoritative', 'authoritative' }, tc = { [0]='complete', 'truncated' }, rd = { [0]='recursion not desired', 'recursion desired' }, ra = { [0]='recursion not available', 'recursion available' }, z = { [0]='(reserved)' }, rcode = { [0]='no error', 'format error', 'server failure', 'name error', 'not implemented' }, type = dns.type, class = dns.class }; local function hint(p, s) -- - - - - - - - - - - - - - - - - - - - - - hint return (hints[s] and hints[s][p[s]]) or ''; end function resolver.print(response) -- - - - - - - - - - - - - resolver.print for _, s in pairs { 'id', 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z', 'rcode', 'qdcount', 'ancount', 'nscount', 'arcount' } do print( string.format('%-30s', 'header.'..s), response.header[s], hint(response.header, s) ); end for i,question in ipairs(response.question) do print(string.format ('question[%i].name ', i), question.name); print(string.format ('question[%i].type ', i), question.type); print(string.format ('question[%i].class ', i), question.class); end local common = { name=1, type=1, class=1, ttl=1, rdlength=1, rdata=1 }; local tmp; for _, s in pairs({'answer', 'authority', 'additional'}) do for i,rr in pairs(response[s]) do for _, t in pairs({ 'name', 'type', 'class', 'ttl', 'rdlength' }) do tmp = string.format('%s[%i].%s', s, i, t); print(string.format('%-30s', tmp), rr[t], hint(rr, t)); end for j,t in pairs(rr) do if not common[j] then tmp = string.format('%s[%i].%s', s, i, j); print(string.format('%-30s %s', tostring(tmp), tostring(t))); end end end end end -- module api ------------------------------------------------------ module api function dns.resolver () -- - - - - - - - - - - - - - - - - - - - - resolver local r = { active = {}, cache = {}, unsorted = {}, wanted = {}, best_server = 1 }; setmetatable (r, resolver); setmetatable (r.cache, cache_metatable); setmetatable (r.unsorted, { __mode = 'kv' }); return r; end local _resolver = dns.resolver(); dns._resolver = _resolver; _resolver.jitter, _resolver.retry_jitter = false, false; function dns.lookup(...) -- - - - - - - - - - - - - - - - - - - - - lookup return _resolver:lookup(...); end function dns.tohostname(...) return _resolver:tohostname(...); end function dns.purge(...) -- - - - - - - - - - - - - - - - - - - - - - purge return _resolver:purge(...); end function dns.peek(...) -- - - - - - - - - - - - - - - - - - - - - - - peek return _resolver:peek(...); end function dns.query(...) -- - - - - - - - - - - - - - - - - - - - - - query return _resolver:query(...); end function dns.feed(...) -- - - - - - - - - - - - - - - - - - - - - - - feed return _resolver:feed(...); end function dns.cancel(...) -- - - - - - - - - - - - - - - - - - - - - - cancel return _resolver:cancel(...); end function dns.settimeout(...) return _resolver:settimeout(...); end function dns.cache() return _resolver.cache; end function dns.socket_wrapper_set(...) -- - - - - - - - - socket_wrapper_set return _resolver:socket_wrapper_set(...); end return dns; prosody-13.0.1/net/PaxHeaders/http0000644000000000000000000000013114773555365014064 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.555710391 30 ctime=1743706869.559710407 prosody-13.0.1/net/http/0000755000175000017500000000000014773555365016344 5ustar00prosodyprosody00000000000000prosody-13.0.1/net/http/PaxHeaders/codes.lua0000644000000000000000000000011714773555365015745 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.555710391 prosody-13.0.1/net/http/codes.lua0000644000175000017500000000706614773555365020155 0ustar00prosodyprosody00000000000000 local response_codes = { -- Source: http://www.iana.org/assignments/http-status-codes [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"; -- 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"; -- 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"; -- 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"; -- RFC9110, Section 15.5.20 [422] = "Unprocessable Content"; -- RFC9110, Section 15.5.21 [423] = "Locked"; [424] = "Failed Dependency"; [425] = "Too Early"; [426] = "Upgrade Required"; -- RFC9110, Section 15.5.22 -- [427] = "Unassigned"; [428] = "Precondition Required"; [429] = "Too Many Requests"; -- [430] = "Unassigned"; [431] = "Request Header Fields Too Large"; -- [432-450] = "Unassigned"; [451] = "Unavailable For Legal Reasons"; -- [452-499] = "Unassigned"; [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"; -- (OBSOLETED) [511] = "Network Authentication Required"; -- [512-599] = "Unassigned"; }; for k,v in pairs(response_codes) do response_codes[k] = ("%03d %s"):format(k, v); end return setmetatable(response_codes, { __index = function(_, k) return k.." Unassigned"; end }) prosody-13.0.1/net/http/PaxHeaders/errors.lua0000644000000000000000000000011714773555365016164 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.555710391 prosody-13.0.1/net/http/errors.lua0000644000175000017500000001030114773555365020356 0ustar00prosodyprosody00000000000000-- This module returns a table that is suitable for use as a util.error registry, -- and a function to return a util.error object given callback 'code' and 'body' -- parameters. 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. -- Instead of using the code, use the supplied body text to get one of -- the more detailed errors below. [0] = { code = 0, type = "cancel", condition = "internal-server-error"; text = "Connection or internal error"; }; -- These are net.http built-in errors, they are returned in -- the body parameter when code == 0 ["cancelled"] = { code = 0, type = "cancel", condition = "remote-server-timeout"; text = "Request cancelled"; }; ["connection-closed"] = { code = 0, type = "wait", condition = "remote-server-timeout"; text = "Connection closed"; }; ["certificate-chain-invalid"] = { code = 0, type = "cancel", condition = "remote-server-timeout"; text = "Server certificate not trusted"; }; ["certificate-verify-failed"] = { code = 0, type = "cancel", condition = "remote-server-timeout"; text = "Server certificate invalid"; }; ["connection failed"] = { code = 0, type = "cancel", condition = "remote-server-not-found"; text = "Connection failed"; }; ["invalid-url"] = { code = 0, type = "modify", condition = "bad-request"; text = "Invalid URL"; }; ["unable to resolve service"] = { code = 0, type = "cancel", condition = "remote-server-not-found"; text = "DNS resolution failed"; }; -- This doesn't attempt to map every single HTTP code (not all have sane mappings), -- but all the common ones should be covered. XEP-0086 was used as reference for -- most of these. [400] = { type = "modify", condition = "bad-request" }; [401] = { type = "auth", condition = "not-authorized" }; [402] = { type = "auth", condition = "payment-required" }; [403] = { type = "auth", condition = "forbidden" }; [404] = { type = "cancel", condition = "item-not-found" }; [405] = { type = "cancel", condition = "not-allowed" }; [406] = { type = "modify", condition = "not-acceptable" }; [407] = { type = "auth", condition = "registration-required" }; [408] = { type = "wait", condition = "remote-server-timeout" }; [409] = { type = "cancel", condition = "conflict" }; [410] = { type = "cancel", condition = "gone" }; [411] = { type = "modify", condition = "bad-request" }; [412] = { type = "cancel", condition = "conflict" }; [413] = { type = "modify", condition = "resource-constraint" }; [414] = { type = "modify", condition = "resource-constraint" }; [415] = { type = "cancel", condition = "feature-not-implemented" }; [416] = { type = "modify", condition = "bad-request" }; [422] = { type = "modify", condition = "bad-request" }; [423] = { type = "wait", condition = "resource-constraint" }; [429] = { type = "wait", condition = "resource-constraint" }; [431] = { type = "modify", condition = "resource-constraint" }; [451] = { type = "auth", condition = "forbidden" }; [500] = { type = "wait", condition = "internal-server-error" }; [501] = { type = "cancel", condition = "feature-not-implemented" }; [502] = { type = "wait", condition = "remote-server-timeout" }; [503] = { type = "cancel", condition = "service-unavailable" }; [504] = { type = "wait", condition = "remote-server-timeout" }; [507] = { type = "wait", condition = "resource-constraint" }; [511] = { type = "auth", condition = "not-authorized" }; }; for k, v in pairs(codes) do if error_templates[k] then error_templates[k].code = k; error_templates[k].text = v; else error_templates[k] = { type = "cancel", condition = "undefined-condition", text = v, code = k }; end end setmetatable(error_templates, { __index = function(_, k) if type(k) ~= "number" then return nil; end return { type = "cancel"; condition = "undefined-condition"; text = codes[k] or (k.." Unassigned"); code = k; }; end }); local function new(code, body, context) if code == 0 then return util_error.new(body, context, error_templates); else return util_error.new(code, context, error_templates); end end return { registry = error_templates; new = new; }; prosody-13.0.1/net/http/PaxHeaders/files.lua0000644000000000000000000000011714773555365015752 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.555710391 prosody-13.0.1/net/http/files.lua0000644000175000017500000001102314773555365020146 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local server = require"prosody.net.http.server"; local lfs = require "lfs"; 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; local stat = lfs.attributes; local build_path = require"socket.url".build_path; local path_sep = package.config:sub(1,1); local forbidden_chars_pattern = "[/%z]"; if package.config:sub(1,1) == "\\" then forbidden_chars_pattern = "[/%z\001-\031\127\"*:<>?|]" end 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 = {}; local c = 0; for component in path:gmatch("([^/]+)") do component = urldecode(component); if component:find(forbidden_chars_pattern) then return nil; elseif component == ".." then if c <= 0 then return nil; end out[c] = nil; c = c - 1; elseif component ~= "." then c = c + 1; out[c] = component; end end if path:sub(-1,-1) == "/" then out[c+1] = ""; end return "/"..table.concat(out, "/"); end local function serve(opts) if type(opts) ~= "table" then -- assume path string opts = { path = opts }; end local mime_map = opts.mime_map or { html = "text/html" }; local cache = new_cache(opts.cache_size or 256); local cache_max_file_size = tonumber(opts.cache_max_file_size) or 1024 -- luacheck: ignore 431 local base_path = assert(opts.path, "invalid argument to net.http.files.path(), missing required 'path'"); local dir_indices = opts.index_files or { "index.html", "index.htm" }; local directory_index = opts.directory_index; local function serve_file(event, path) local request, response = event.request, event.response; local sanitized_path = sanitize_path(path); if path and not sanitized_path then return 400; end path = sanitized_path; local orig_path = sanitize_path(request.path); local full_path = base_path .. (path or ""):gsub("/", path_sep); local attr = stat(full_path:match("^.*[^\\/]")); -- Strip trailing path separator because Windows if not attr then return 404; end local request_headers, response_headers = request.headers, response.headers; local last_modified = os_date('!%a, %d %b %Y %H:%M:%S GMT', attr.modification); response_headers.last_modified = last_modified; local etag = ('"%x-%x-%x"'):format(attr.change or 0, attr.size or 0, attr.modification or 0); response_headers.etag = etag; local if_none_match = request_headers.if_none_match local if_modified_since = request_headers.if_modified_since; if etag == if_none_match or (not if_none_match and last_modified == if_modified_since) then return 304; end local data = cache:get(orig_path); if data and data.etag == etag then response_headers.content_type = data.content_type; data = data.data; cache:set(orig_path, data); elseif attr.mode == "directory" and path then if full_path:sub(-1) ~= "/" then local dir_path = { is_absolute = true, is_directory = true }; for dir in orig_path:gmatch("[^/]+") do dir_path[#dir_path+1]=dir; end response_headers.location = build_path(dir_path); return 301; end for i=1,#dir_indices do if stat(full_path..dir_indices[i], "mode") == "file" then return serve_file(event, path..dir_indices[i]); end end if directory_index then data = server._events.fire_event("directory-index", { path = request.path, full_path = full_path }); end if not data then return 403; end cache:set(orig_path, { data = data, content_type = mime_map.html; etag = etag; }); response_headers.content_type = mime_map.html; else local f, err = open(full_path, "rb"); if not f then log("debug", "Could not open %s. Error was %s", full_path, err); return 403; end local ext = full_path:match("%.([^./]+)$"); local content_type = ext and mime_map[ext]; response_headers.content_type = content_type; if attr.size > cache_max_file_size then response_headers.content_length = ("%d"):format(attr.size); log("debug", "%d > cache_max_file_size", attr.size); return response:send_file(f); else data = f:read("*a"); f:close(); end cache:set(orig_path, { data = data; content_type = content_type; etag = etag }); end return response:send(data); end return serve_file; end return { serve = serve; } prosody-13.0.1/net/http/PaxHeaders/parser.lua0000644000000000000000000000011714773555365016144 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.559710407 prosody-13.0.1/net/http/parser.lua0000644000175000017500000001662414773555365020354 0ustar00prosodyprosody00000000000000local tonumber = tonumber; local assert = assert; local url_parse = require "socket.url".parse; local urldecode = require "prosody.util.http".urldecode; local dbuffer = require "prosody.util.dbuffer"; local function preprocess_path(path) path = urldecode((path:gsub("//+", "/"))); if path:sub(1,1) ~= "/" then path = "/"..path; end local level = 0; for component in path:gmatch("([^/]+)/") do if component == ".." then level = level - 1; elseif component ~= "." then level = level + 1; end if level < 0 then return nil; end end return path; end local httpstream = {}; function httpstream.new(success_cb, error_cb, parser_type, options_cb) local client = true; if not parser_type or parser_type == "server" then client = false; else assert(parser_type == "client", "Invalid parser type"); end local bodylimit = tonumber(options_cb and options_cb().body_size_limit) or 10*1024*1024; -- https://stackoverflow.com/a/686243 -- Individual headers can be up to 16k? What madness? local headlimit = tonumber(options_cb and options_cb().head_size_limit) or 10*1024; local buflimit = tonumber(options_cb and options_cb().buffer_size_limit) or bodylimit * 2; local buffer = dbuffer.new(buflimit); local chunked; local state = nil; local packet; local len; local have_body; local error; return { feed = function(_, data) if error then return nil, "parse has failed"; end if not data then -- EOF if state and client and not len then -- reading client body until EOF buffer:collapse(); packet.body = buffer:read_chunk() or ""; packet.partial = nil; success_cb(packet); state = nil; elseif buffer:length() ~= 0 then -- unexpected EOF error = true; return error_cb("unexpected-eof"); end return; end if not buffer:write(data) then error = true; return error_cb("max-buffer-size-exceeded"); end while buffer:length() > 0 do if state == nil then -- read request local index = buffer:sub(1, headlimit):find("\r\n\r\n", nil, true); if not index then if buffer:length() > headlimit then return error_cb("header-too-large"); end -- not enough data return; end -- FIXME was reason_phrase meant to be passed on somewhere? local method, path, httpversion, status_code, reason_phrase; -- luacheck: ignore reason_phrase local first_line; local headers = {}; for line in buffer:read(index+3):gmatch("([^\r\n]+)\r\n") do -- parse request if first_line then local key, val = line:match("^([^%s:]+): *(.*)$"); if not key then error = true; return error_cb("invalid-header-line"); end -- TODO handle multi-line and invalid headers key = key:lower(); headers[key] = headers[key] and headers[key]..","..val or val; else first_line = line; if client then httpversion, status_code, reason_phrase = line:match("^HTTP/(1%.[01]) (%d%d%d) (.*)$"); status_code = tonumber(status_code); if not status_code then error = true; return error_cb("invalid-status-line"); end have_body = not ( (options_cb and options_cb().method == "HEAD") or (status_code == 204 or status_code == 304 or status_code == 301) or (status_code >= 100 and status_code < 200) ); else method, path, httpversion = line:match("^(%w+) (%S+) HTTP/(1%.[01])$"); if not method then error = true; return error_cb("invalid-status-line"); end end end end if not first_line then error = true; return error_cb("invalid-status-line"); end chunked = have_body and headers["transfer-encoding"] == "chunked"; len = tonumber(headers["content-length"]); -- TODO check for invalid len if client then -- FIXME handle '100 Continue' response (by skipping it) if not have_body then len = 0; end packet = { code = status_code; httpversion = httpversion; headers = headers; body = false; body_length = len; chunked = chunked; partial = true; -- COMPAT the properties below are deprecated responseversion = httpversion; responseheaders = headers; }; else local parsed_url; if path:byte() == 47 then -- starts with / local _path, _query = path:match("([^?]*).?(.*)"); if _query == "" then _query = nil; end parsed_url = { path = _path, query = _query }; else parsed_url = url_parse(path); if not(parsed_url and parsed_url.path) then error = true; return error_cb("invalid-url"); end end path = preprocess_path(parsed_url.path); headers.host = parsed_url.host or headers.host; len = len or 0; packet = { method = method; url = parsed_url; path = path; httpversion = httpversion; headers = headers; body = false; body_sink = nil; chunked = chunked; partial = true; }; end if not len or len > bodylimit then -- Early notification, for redirection success_cb(packet); if not packet.body_sink and (len and len > bodylimit) then error = true; return error_cb("content-length-limit-exceeded"); end end if chunked and not packet.body_sink then success_cb(packet); if not packet.body_sink then packet.body_buffer = dbuffer.new(buflimit); end end state = true; end if state then -- read body if chunked then local chunk_header = buffer:sub(1, 512); -- XXX How large do chunk headers grow? local chunk_size, chunk_start = chunk_header:match("^(%x+)[^\r\n]*\r\n()"); if not chunk_size then return; end chunk_size = chunk_size and tonumber(chunk_size, 16); if not chunk_size then error = true; return error_cb("invalid-chunk-size"); end if chunk_size == 0 and chunk_header:find("\r\n\r\n", chunk_start-2, true) then local body_buffer = packet.body_buffer; if body_buffer then packet.body_buffer = nil; body_buffer:collapse(); packet.body = body_buffer:read_chunk() or ""; end buffer:collapse(); local buf = buffer:read_chunk(); buf = buf:gsub("^.-\r\n\r\n", ""); -- This ensure extensions and trailers are stripped buffer:write(buf); state, chunked = nil, nil; packet.partial = nil; success_cb(packet); elseif buffer:length() - chunk_start - 1 >= chunk_size then -- we have a chunk buffer:discard(chunk_start - 1); (packet.body_sink or packet.body_buffer):write(buffer:read(chunk_size)); buffer:discard(2); -- CRLF else -- Partial chunk remaining break; end elseif packet.body_sink then local chunk = buffer:read_chunk(len); while chunk and (not len or len > 0) do if packet.body_sink:write(chunk) then if len then len = len - #chunk; end chunk = buffer:read_chunk(len); else error = true; return error_cb("body-sink-write-failure"); end end if len == 0 then state = nil; packet.partial = nil; success_cb(packet); end elseif not len or buffer:length() >= len then -- or not len assert(not chunked) packet.body = len and buffer:read(len) or buffer:read_chunk() or ""; state = nil; packet.partial = nil; success_cb(packet); else break; end else break; end end end; }; end return httpstream; prosody-13.0.1/net/http/PaxHeaders/server.lua0000644000000000000000000000011714773555365016156 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.559710407 prosody-13.0.1/net/http/server.lua0000644000175000017500000003226014773555365020360 0ustar00prosodyprosody00000000000000 local t_insert, t_concat = table.insert, table.concat; 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 "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 "prosody.util.async"; local id = require"prosody.util.id"; local _M = {}; local sessions = {}; local incomplete = {}; local listener = {}; local hosts = {}; local default_host; local options = {}; local function is_wildcard_event(event) return event:sub(-2, -1) == "/*"; end local function is_wildcard_match(wildcard_event, event) return wildcard_event:sub(1, -2) == event:sub(1, #wildcard_event-1); end local _handlers = events._handlers; local recent_wildcard_events = cache.new(10000, function (key, value) -- luacheck: ignore 212/value rawset(_handlers, key, nil); end); local event_map = events._event_map; setmetatable(events._handlers, { -- Called when firing an event that doesn't exist (but may match a wildcard handler) __index = function (handlers, curr_event) if is_wildcard_event(curr_event) then return; end -- Wildcard events cannot be fired -- Find all handlers that could match this event, sort them -- and then put the array into handlers[curr_event] (and return it) local matching_handlers_set = {}; local handlers_array = {}; for event, handlers_set in pairs(event_map) do if event == curr_event or is_wildcard_event(event) and is_wildcard_match(event, curr_event) then for handler, priority in pairs(handlers_set) do matching_handlers_set[handler] = { (select(2, event:gsub("/", "%1"))), is_wildcard_event(event) and 0 or 1, priority }; table.insert(handlers_array, handler); end end end if #handlers_array > 0 then table.sort(handlers_array, function(b, a) local a_score, b_score = matching_handlers_set[a], matching_handlers_set[b]; for i = 1, #a_score do if a_score[i] ~= b_score[i] then -- If equal, compare next score value return a_score[i] < b_score[i]; end end return false; end); else handlers_array = false; end rawset(handlers, curr_event, handlers_array); if not event_map[curr_event] then -- Only wildcard handlers match, if any recent_wildcard_events:set(curr_event, true); end return handlers_array; end; __newindex = function (handlers, curr_event, handlers_array) if handlers_array == nil and is_wildcard_event(curr_event) then -- Invalidate the indexes of all matching events for event in pairs(handlers) do if is_wildcard_match(curr_event, event) then handlers[event] = nil; end end end rawset(handlers, curr_event, handlers_array); end; }); local handle_request; events.add_handler("http-error", function (error) return "Error processing request: "..codes[error.code]..". Check your error log for more information."; end, -1); local runner_callbacks = {}; function runner_callbacks:ready() self.data.conn:resume(); end function runner_callbacks:waiting() self.data.conn:pause(); end function runner_callbacks:error(err) log("error", "Traceback[httpserver]: %s", 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 local function noop() end function listener.onconnect(conn) local session = { conn = conn }; local secure = conn:ssl() and true or nil; local ip = conn:ip(); session.thread = async.runner(function (request) local wait, done; if request.partial == true then -- Have the header for a request, we want to receive the rest -- when we've decided where the data should go. wait, done = noop, noop; else -- Got the entire request -- Hold off on receiving more incoming requests until this one has been handled. wait, done = async.waiter(); end handle_request(conn, request, done); wait(); 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); end local function error_cb(err) log("debug", "error_cb: %s", err or ""); -- FIXME don't close immediately, wait until we process current stuff -- FIXME if err, send off a bad-request response conn:close(); end local function options_cb() return options; end session.parser = parser_new(success_cb, error_cb, "server", options_cb); sessions[conn] = session; end function listener.ondisconnect(conn) local open_response = conn._http_open_response; if open_response and open_response.on_destroy then open_response.finished = true; open_response:on_destroy(); end incomplete[conn] = nil; sessions[conn] = nil; end function listener.ondetach(conn) sessions[conn] = nil; incomplete[conn] = nil; end function listener.onincoming(conn, data) sessions[conn].parser:feed(data); end function listener.ondrain(conn) local response = incomplete[conn]; if response and response._send_more then response._send_more(); end end local headerfix = setmetatable({}, { __index = function(t, k) local v = "\r\n"..k:gsub("_", "-"):gsub("%f[%w].", s_upper)..": "; t[k] = v; return v; end }); local function handle_result(request, response, result) if result == nil then result = 404; end if result == true then return; end local body; local result_type = type(result); if result_type == "number" then response.status_code = result; if result >= 400 then body = events.fire_event("http-error", { request = request, response = response, code = result }); end elseif result_type == "string" then body = result; 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 result:next(function (ret) handle_result(request, response, ret); end, function (err) response.status_code = 500; handle_result(request, response, err or 500); end); return true; elseif result_type == "table" then for k, v in pairs(result) do if k ~= "headers" then response[k] = v; else for header_name, header_value in pairs(v) do response.headers[header_name] = header_value; end end end end return response:send(body); end function _M.hijack_response(response, listener) -- luacheck: ignore error("TODO"); end function handle_request(conn, request, finish_cb) --log("debug", "handler: %s", request.path); local headers = {}; for k,v in pairs(request.headers) do headers[k:gsub("-", "_")] = v; end 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 "" local httpversion = request.httpversion local persistent = conn_header:find(",keep-alive,", 1, true) or (httpversion == "1.1" and not conn_header:find(",close,", 1, true)); local response_conn_header; if persistent then response_conn_header = "Keep-Alive"; else response_conn_header = httpversion == "1.1" and "close" or nil end 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; x_request_id = request.id }; persistent = persistent; conn = conn; send = _M.send_response; write_headers = _M.write_headers; send_file = _M.send_file; done = _M.finish_response; finish_cb = finish_cb; }; conn._http_open_response = response; local host = request.headers.host; if host then host = host:gsub(":%d+$",""); end -- Some sanity checking local err_code, err; if not request.path then err_code, err = 400, "Invalid path"; end if err then response.status_code = err_code; response:send(events.fire_event("http-error", { code = err_code, message = err, response = response })); return; end local global_event = request.method.." "..request.path:match("[^?]*"); local payload = { request = request, response = response }; local result = events.fire_event(global_event, payload); if result == nil and is_head_request then local global_head_event = "GET "..request.path:match("[^?]*"); result = events.fire_event(global_head_event, payload); end if result == nil then if not hosts[host] then if hosts[default_host] then host = default_host; elseif host then err_code, err = 404, "Unknown host: "..host; else err_code, err = 400, "Missing or invalid 'Host' header"; end end if err then response.status_code = err_code; response:send(events.fire_event("http-error", { code = err_code, message = err, response = response })); return; end local host_event = request.method.." "..host..request.path:match("[^?]*"); result = events.fire_event(host_event, payload); if result == nil and is_head_request then local host_head_event = "GET "..host..request.path:match("[^?]*"); result = events.fire_event(host_head_event, payload); end end return handle_result(request, response, result); 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 t_insert(output, headerfix[k]..v); end t_insert(output, "\r\n\r\n"); return output; end _M.prepare_header = prepare_header; function _M.write_headers(response) if response.finished then return; end local output = prepare_header(response); response.conn:write(t_concat(output)); end function _M.send_head_response(response) if response.finished then return; end _M.write_headers(response); response:done(); end function _M.send_response(response, body) if response.finished then return; end body = body or response.body or ""; -- Per RFC 7230, informational (1xx) and 204 (no content) should have no c-l header if response.status_code > 199 and response.status_code ~= 204 then response.headers.content_length = ("%d"):format(#body); end if response.is_head_request then return _M.send_head_response(response) end local output = prepare_header(response); t_insert(output, body); response.conn:write(t_concat(output)); response:done(); end function _M.send_file(response, f) if response.is_head_request then if f.close then f:close(); end return _M.send_head_response(response); end if response.finished then return; end local chunked = not response.headers.content_length; if chunked then response.headers.transfer_encoding = "chunked"; end incomplete[response.conn] = response; response._send_more = function () if response.finished then incomplete[response.conn] = nil; return; end local chunk = f:read(blocksize); if chunk then if chunked then chunk = ("%x\r\n%s\r\n"):format(#chunk, chunk); end -- io.write("."); io.flush(); 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"); return response:done(); end end _M.write_headers(response); return true; end function _M.finish_response(response) if response.finished then return; end response.finished = true; response.conn._http_open_response = nil; if response.on_destroy then response:on_destroy(); response.on_destroy = nil; end response:finish_cb(); if not response.persistent then response.conn:close(); end end function _M.add_handler(event, handler, priority) events.add_handler(event, handler, priority); end function _M.remove_handler(event, handler) events.remove_handler(event, handler); end function _M.listen_on(port, interface, ssl) return addserver(interface or "*", port, listener, "*a", ssl); end function _M.add_host(host) hosts[host] = true; end function _M.remove_host(host) hosts[host] = nil; end function _M.set_default_host(host) default_host = host; end function _M.fire_event(event, ...) return events.fire_event(event, ...); end function _M.set_option(name, value) options[name] = value; end function _M.get_request_from_conn(conn) local response = conn and conn._http_open_response; return response and response.request or nil; end _M.listener = listener; _M.codes = codes; _M._events = events; return _M; prosody-13.0.1/net/PaxHeaders/http.lua0000644000000000000000000000011714773555365014650 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.555710391 prosody-13.0.1/net/http.lua0000644000175000017500000002554414773555365017061 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local b64 = require "prosody.util.encodings".base64.encode; local url = require "socket.url" 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 "prosody.net.resolvers.basic"; local connect = require "prosody.net.connect".connect; local ssl_available = pcall(require, "ssl"); local t_insert, t_concat = table.insert, table.concat; local pairs = pairs; local tonumber, tostring, traceback = tonumber, tostring, debug.traceback; local os_time = os.time; local xpcall = require "prosody.util.xpcall".xpcall; local error = error local log = require "prosody.util.logger".init("http"); local _ENV = nil; -- luacheck: std none local requests = {}; -- Open requests local function make_id(req) return (tostring(req):match("%x+$")); end local listener = { default_port = 80, default_mode = "*a" }; -- Request-related helper functions local function handleerr(err) log("error", "Traceback[http]: %s", traceback(tostring(err), 2)); return err; end local function log_if_failed(req, ret, ...) if not ret then log("error", "Request '%s': error in callback: %s", req.id, (...)); if not req.suppress_errors then error(...); end end return ...; end 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 local function cancel_request(request, reason) if request.callback then request.callback(reason or "cancelled", 0, request); request.callback = nil; end if request.conn then destroy_request(request); end end local function request_reader(request, data, err) if not request.parser then local function error_cb(reason) if request.callback then request.callback(reason or "connection-closed", 0, request); request.callback = nil; end destroy_request(request); end if not data then error_cb(err); return; end local finalize_sink; local function success_cb(r) if r.partial then -- Request should be streamed log("debug", "Request '%s': partial response (%s%s)", request.id, r.chunked and "chunked, " or "", r.body_length and ("%d bytes"):format(r.body_length) or "unknown length" ); if request.streaming_handler then log("debug", "Request '%s': Streaming via handler", request.id); r.body_sink, finalize_sink = request.streaming_handler(r); end return; elseif finalize_sink then log("debug", "Request '%s': Finalizing response stream"); finalize_sink(r); end if request.callback then request.callback(r.body, r.code, r, request); request.callback = nil; end local persistent = (","..(r.headers.connection or "keep-alive")..","):find(",keep-alive,") destroy_request(request, persistent); end local function options_cb() return request; end request.parser = httpstream_new(success_cb, error_cb, "client", options_cb); end request.parser:feed(data); end -- Connection listener callbacks function listener.onconnect(conn) local req = requests[conn]; -- Initialize request object req.write = function (...) return req.conn:write(...); end local callback = req.callback; req.callback = function (content, code, response, request) do local event = { http = req.http, url = req.url, request = req, response = response, content = content, code = code, callback = req.callback }; req.http.events.fire_event("response", event); content, code, response = event.content, event.code, event.response; end log("debug", "Request '%s': Calling callback, status %s", req.id, code or "---"); return log_if_failed(req.id, xpcall(callback, handleerr, content, code, response, request)); end req.reader = request_reader; req.state = "status"; req.cancel = cancel_request; requests[req.conn] = req; -- Validate certificate if not req.insecure and conn:ssl() then local sock = conn:socket(); local chain_valid = sock.getpeerverification and sock:getpeerverification(); if not chain_valid then req.callback("certificate-chain-invalid", 0, req); req.callback = nil; conn:close(); return; end local cert = sock.getpeercertificate and sock:getpeercertificate(); if not cert or not verify_identity(req.host, false, cert) then req.callback("certificate-verify-failed", 0, req); req.callback = nil; conn:close(); return; end end -- Send the request local request_line = { req.method or "GET", " ", req.path, " HTTP/1.1\r\n" }; if req.query then t_insert(request_line, 4, "?"..req.query); end for k, v in pairs(req.headers) do t_insert(request_line, k .. ": " .. v .. "\r\n"); end t_insert(request_line, "\r\n") conn:write(t_concat(request_line)); if req.body then conn:write(req.body); end end function listener.onincoming(conn, data) local request = requests[conn]; if not request then log("warn", "Received response from connection %s with no request attached!", conn); return; end if data and request.reader then request:reader(data); end end function listener.ondisconnect(conn, err) local request = requests[conn]; 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 function listener.onattach(conn, req) requests[conn] = req; req.conn = conn; end function listener.ondetach(conn) requests[conn] = nil; end function listener.onfail(req, reason) req.http.events.fire_event("request-connection-error", { http = req.http, request = req, url = req.url, err = reason }); req.callback(reason or "connection failed", 0, req); end local function request(self, u, ex, callback) local req = url.parse(u); if not (req and req.host) then callback("invalid-url", 0, req); return nil, "invalid-url"; end req.url = u; req.http = self; req.time = os_time(); if not req.path then req.path = "/"; end req.id = ex and ex.id or make_id(req); do local event = { http = self, url = u, request = req, options = ex, callback = callback }; local ret = self.events.fire_event("pre-request", event); if ret then return ret; end req, u, ex, req.callback = event.request, event.url, event.options, event.callback; end local method, headers, body; local host, port = req.host, req.port; local host_header = host; if (port == "80" and req.scheme == "http") or (port == "443" and req.scheme == "https") then port = nil; elseif port then host_header = host_header..":"..port; end headers = { ["Host"] = host_header; ["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 if ex then req.onlystatus = ex.onlystatus; body = ex.body; if body then method = "POST"; headers["Content-Length"] = tostring(#body); headers["Content-Type"] = "application/x-www-form-urlencoded"; end if ex.method then method = ex.method; end if ex.headers then for k, v in pairs(ex.headers) do headers[k] = v; end end req.insecure = ex.insecure; req.suppress_errors = ex.suppress_errors; req.streaming_handler = ex.streaming_handler; end log("debug", "Making %s %s request '%s' to %s", req.scheme:upper(), method or "GET", req.id, (ex and ex.suppress_url and host_header) or u); -- Attach to request object req.method, req.headers, req.body = method, headers, body; local using_https = req.scheme == "https"; if using_https and not ssl_available then error("SSL not available, unable to contact https URL"); end local port_number = port and tonumber(port) or (using_https and 443 or 80); local use_dane = self.options and self.options.use_dane; local sslctx = false; if using_https then sslctx = ex and ex.sslctx or self.options and self.options.sslctx; if ex and ex.use_dane ~= nil then use_dane = ex.use_dane; end if not sslctx then error("Attempt to make HTTPS request but no 'sslctx' provided in options"); 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); self.events.fire_event("request", { http = self, request = req, url = u }); return req; end local function new(options) local http = { options = options; request = function (self, u, ex, callback) if callback ~= nil then return request(self, u, ex, callback); else return promise.new(function (resolve, reject) request(self, u, ex, function (body, code, a, b) if code == 0 then reject(http_errors.new(body, { request = a })); else a.request = b; resolve(a); end end); end); end end; new = options and function (new_options) local final_options = {}; for k, v in pairs(options) do final_options[k] = v; end if new_options then for k, v in pairs(new_options) do final_options[k] = v; end end return new(final_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 local default_http = new({ sslctx = { mode = "client", protocol = "sslv23", options = { "no_sslv2", "no_sslv3" }, alpn = "http/1.1", verify = "peer" }; suppress_errors = true; }); return { request = function (u, ex, callback) return default_http:request(u, ex, callback); end; default = default_http; new = new; events = default_http.events; -- COMPAT urlencode = util_http.urlencode; urldecode = util_http.urldecode; formencode = util_http.formencode; formdecode = util_http.formdecode; destroy_request = destroy_request; features = { sni = true; }; }; prosody-13.0.1/net/PaxHeaders/resolvers0000644000000000000000000000013114773555365015131 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.559710407 30 ctime=1743706869.563710423 prosody-13.0.1/net/resolvers/0000755000175000017500000000000014773555365017411 5ustar00prosodyprosody00000000000000prosody-13.0.1/net/resolvers/PaxHeaders/basic.lua0000644000000000000000000000011714773555365016776 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.559710407 prosody-13.0.1/net/resolvers/basic.lua0000644000175000017500000001124314773555365021176 0ustar00prosodyprosody00000000000000local 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) if self.targets then if #self.targets == 0 then cb(nil); return; end local next_target = table.remove(self.targets, 1); cb(next_target[1], next_target[2], next_target[3], next_target[4], not not self.targets[1]); return; end if not self.hostname then self.last_error = "hostname failed IDNA"; cb(nil); return; end -- 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 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):catch(function (err) self.last_error = err; self.targets = {}; end); end local function new(hostname, port, conn_type, extra) local ascii_host = idna_to_ascii(hostname); local targets = nil; conn_type = conn_type or "tcp"; local is_ip = inet_pton(hostname); if not is_ip and hostname:sub(1,1) == '[' then is_ip = inet_pton(hostname:sub(2,-2)); end if is_ip then hostname = inet_ntop(is_ip); if #is_ip == 16 then targets = { { conn_type.."6", hostname, port, extra } }; elseif #is_ip == 4 then targets = { { conn_type.."4", hostname, port, extra } }; end end return setmetatable({ hostname = ascii_host; port = port; conn_type = conn_type; extra = extra or {}; targets = targets; }, resolver_mt); end return { new = new; }; prosody-13.0.1/net/resolvers/PaxHeaders/chain.lua0000644000000000000000000000011714773555365016777 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.563710423 prosody-13.0.1/net/resolvers/chain.lua0000644000175000017500000000132614773555365021200 0ustar00prosodyprosody00000000000000 local methods = {}; local resolver_mt = { __index = methods }; -- Find the next target to connect to, and -- pass it to cb() function methods:next(cb) if self.resolvers then if not self.resolver then if #self.resolvers == 0 then cb(nil); return; end local next_resolver = table.remove(self.resolvers, 1); self.resolver = next_resolver; end self.resolver:next(function (...) if self.resolver then self.last_error = self.resolver.last_error; end if ... == nil then self.resolver = nil; self:next(cb); else cb(...); end end); return; end end local function new(resolvers) return setmetatable({ resolvers = resolvers }, resolver_mt); end return { new = new; }; prosody-13.0.1/net/resolvers/PaxHeaders/manual.lua0000644000000000000000000000011714773555365017172 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.563710423 prosody-13.0.1/net/resolvers/manual.lua0000644000175000017500000000077514773555365021402 0ustar00prosodyprosody00000000000000local methods = {}; local resolver_mt = { __index = methods }; local unpack = table.unpack; -- Find the next target to connect to, and -- pass it to cb() function methods:next(cb) if #self.targets == 0 then cb(nil); return; end local next_target = table.remove(self.targets, 1); cb(unpack(next_target, 1, 4)); end local function new(targets, conn_type, extra) return setmetatable({ conn_type = conn_type; extra = extra; targets = targets or {}; }, resolver_mt); end return { new = new; }; prosody-13.0.1/net/resolvers/PaxHeaders/service.lua0000644000000000000000000000011714773555365017355 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.563710423 prosody-13.0.1/net/resolvers/service.lua0000644000175000017500000001127114773555365021556 0ustar00prosodyprosody00000000000000local 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.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 -- 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; end if ... == nil then self.resolver = nil; self:next(cb); else cb(...); end end); return; elseif self.in_progress then cb(nil); return; end if not self.hostname then self.last_error = "hostname failed IDNA"; cb(nil); return; end self.in_progress = true; local function ready() self:next(cb); end -- Resolve DNS to target list local dns_resolver = adns.resolver(); dns_resolver:lookup(function (answer, err) if not answer and not err then -- net.adns returns nil if there are zero records or nxdomain answer = {}; end if answer 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 self.resolver = basic.new(self.hostname, self.extra.default_port, self.conn_type, self.extra); else self.last_error = "zero SRV records found"; end ready(); return; end if #answer == 1 and answer[1].srv.target == "." then -- No service here self.last_error = "service explicitly unavailable"; ready(); return; end self._get_next_target = new_target_selector(answer); else self.last_error = err; end ready(); end, "_" .. self.service .. "._" .. self.conn_type .. "." .. self.hostname, "SRV", "IN"); end local function new(hostname, service, conn_type, extra) local is_ip = inet_pton(hostname); if not is_ip and hostname:sub(1,1) == '[' then is_ip = inet_pton(hostname:sub(2,-2)); end if is_ip and extra and extra.default_port then return basic.new(hostname, extra.default_port, conn_type, extra); end return setmetatable({ hostname = idna_to_ascii(hostname); service = service; conn_type = conn_type or "tcp"; extra = extra; }, resolver_mt); end return { new = new; }; prosody-13.0.1/net/PaxHeaders/server.lua0000644000000000000000000000011714773555365015177 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.563710423 prosody-13.0.1/net/server.lua0000644000175000017500000000754514773555365017411 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local function log(level, format, ...) print("net.server", level, format:format(...)); end local default_backend = "select"; local server_type = default_backend; if (prosody and prosody.config_loaded) then default_backend = "epoll"; log = require"prosody.util.logger".init("net.server"); server_type = require"prosody.core.configmanager".get("*", "network_backend") or default_backend; if require"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 if not pcall(require, "luaevent.core") then log("error", "libevent not found, falling back to %s", default_backend); server_type = default_backend; end end local server; local set_config; if server_type == "event" then server = require "prosody.net.server_event"; local defaults = {}; for k,v in pairs(server.cfg) do defaults[k] = v; end function set_config(settings) local event_settings = { ACCEPT_DELAY = settings.accept_retry_interval; ACCEPT_QUEUE = settings.tcp_backlog; CLEAR_DELAY = settings.event_clear_interval; CONNECT_TIMEOUT = settings.connect_timeout; DEBUG = settings.debug; HANDSHAKE_TIMEOUT = settings.ssl_handshake_timeout; MAX_CONNECTIONS = settings.max_connections; MAX_HANDSHAKE_ATTEMPTS = settings.max_ssl_handshake_roundtrips; MAX_READ_LENGTH = settings.max_receive_buffer_size; MAX_SEND_LENGTH = settings.max_send_buffer_size; READ_TIMEOUT = settings.read_timeout; WRITE_TIMEOUT = settings.send_timeout; }; for k,default in pairs(defaults) do server.cfg[k] = event_settings[k] or default; end end 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 "prosody.net.server_select"; local defaults = {}; for k,v in pairs(server.getsettings()) do defaults[k] = v; end function set_config(settings) local select_settings = {}; for k,default in pairs(defaults) do select_settings[k] = settings[k] or default; end server.changesettings(select_settings); end else server = require("prosody.net.server_"..server_type); set_config = server.set_config; if not server.get_backend then function server.get_backend() return server_type; end end end -- If server.hook_signal exists, replace signal.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) if type(signal_id) == "string" then signal_id = signal[signal_id:upper()]; end if type(signal_id) ~= "number" then return false, "invalid-signal"; end return server.hook_signal(signal_id, handler); end else server.hook_signal = signal.signal; end else if not server.hook_signal then server.hook_signal = function() return false, "signal hooking not supported" end end end if prosody and set_config then local config_get = require "prosody.core.configmanager".get; local function load_config() local settings = config_get("*", "network_settings") or {}; return set_config(settings); end load_config(); prosody.events.add_handler("config-reloaded", load_config); end 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; prosody-13.0.1/net/PaxHeaders/server_epoll.lua0000644000000000000000000000011714773555365016372 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.567710439 prosody-13.0.1/net/server_epoll.lua0000644000175000017500000010265514773555365020602 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2016-2018 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local setmetatable = setmetatable; local pcall = pcall; local type = type; local next = next; local pairs = pairs; local ipairs = ipairs; local traceback = debug.traceback; local logger = require "prosody.util.logger"; local log = logger.init("server_epoll"); local socket = require "socket"; 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 "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 "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; -- luacheck: std none local default_config = { __index = { -- If a connection is silent for this long, close it unless onreadtimeout says not to read_timeout = 14 * 60; -- How long to wait for a socket to become writable after queuing data to send send_timeout = 180; -- How long to wait for a socket to become writable after creation connect_timeout = 20; -- Some number possibly influencing how many pending connections can be accepted tcp_backlog = 128; -- If accepting a new incoming connection fails, wait this long before trying again accept_retry_interval = 10; -- If there is still more data to read from LuaSockets buffer, wait this long and read again read_retry_delay = 1e-06; -- 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; -- Maximum and minimum amount of time to sleep waiting for events (adjusted for pending timers) max_wait = 86400; min_wait = 0.001; -- Enable extra noisy debug logging verbose = false; -- EXPERIMENTAL -- Whether to kill connections in case of callback errors. fatal_errors = false; -- Or disable protection (like server_select) for potential performance gains protect_listeners = true; -- Attempt writes instantly opportunistic_writes = false; -- TCP Keepalives tcp_keepalive = false; -- boolean | number -- Whether to let the Nagle algorithm stay enabled nagle = true; -- Reuse write buffer tables keep_buffers = true; --- 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; local fds = createtable(10, 0); -- FD -> conn -- Timer and scheduling -- local timers = indexedbheap.create(); local function noop() end -- Keep track of recently closed timers to avoid re-adding them local closedtimers = {}; local function closetimer(id) if timers:remove(id) then closedtimers[id] = true; end end local function reschedule(id, time) time = monotonic() + time; timers:reprioritize(id, time); end -- Add relative timer local function addtimer(timeout, f, param) local time = monotonic() + timeout; if param ~= nil then local timer_callback = f function f(current_time, timer_id) local t = timer_callback(current_time, timer_id, param) return t; end end local id = timers:insert(f, time); return id; end -- Run callbacks of expired timers -- Return time until next timeout local function runtimers(next_delay, min_wait) -- Any timers at all? local elapsed = monotonic(); local now = realtime(); local peek = timers:peek(); local readd; while peek do if peek > elapsed then break; end local _, timer, id = timers:pop(); local ok, ret = xpcall(timer, traceback, now, id); if ok and type(ret) == "number" and not closedtimers[id] then local next_time = elapsed+ret; -- Delay insertion of timers to be re-added -- so they don't get called again this tick if readd then readd[id] = { timer, next_time }; else readd = { [id] = { timer, next_time } }; end elseif not ok then log("error", "Error in timer: %s", ret); end peek = timers:peek(); end if readd then for id, timer in pairs(readd) do timers:insert(timer[1], timer[2], id); end peek = timers:peek(); end if next(closedtimers) ~= nil then closedtimers = {}; end if peek == nil then return next_delay; else next_delay = peek - elapsed; end if next_delay < min_wait then return min_wait; end return next_delay; end -- Socket handler interface local interface = {}; local interface_mt = { __index = interface }; function interface_mt:__tostring() if self.sockname and self.peername then return ("FD %d (%s, %d, %s, %d)"):format(self:getfd(), self.peername, self.peerport, self.sockname, self.sockport); elseif self.sockname or self.peername then return ("FD %d (%s, %d)"):format(self:getfd(), self.sockname or self.peername, self.sockport or self.peerport); end return ("FD %d"):format(self:getfd()); end interface.log = log; function interface:debug(msg, ...) self.log("debug", msg, ...); end interface.noise = interface.debug; function interface:noise(msg, ...) if cfg.verbose then return self:debug(msg, ...); end end function interface:error(msg, ...) self.log("error", msg, ...); end -- Replace the listener and tell the old one function interface:setlistener(listeners, data) self:on("detach"); self.listeners = listeners; self:on("attach", data); end -- Call a listener callback function interface:on(what, ...) if not self.listeners then self:error("Interface is missing listener callbacks"); return; end local listener = self.listeners["on"..what]; if not listener then self:noise("Missing listener 'on%s'", what); -- uncomment for development and debugging return; end if not cfg.protect_listeners then return listener(self, ...); end local onerror = self.listeners.onerror or traceback; local ok, err = xpcall(listener, onerror, self, ...); if not ok then if cfg.fatal_errors then self:error("Closing due to error calling on%s: %s", what, err); self:destroy(); else self:error("Error calling on%s: %s", what, err); end return nil, err; end return err; end -- Allow this one to be overridden function interface:onincoming(...) return self:on("incoming", ...); end -- Return the file descriptor number function interface:getfd() if self.conn then return self.conn:getfd(); end return _SOCKETINVALID; end function interface:server() return self._server or self; end -- Get IP address function interface:ip() return self.peername or self.sockname; end -- Get a port number, doesn't matter which function interface:port() return self.peerport or self.sockport; end -- Client-side port (usually a random high port) function interface:clientport() if self._server then return self.peerport; else return self.sockport; end end -- Get port on the server function interface:serverport() if self._server then return self.sockport; else return self.peerport; end end -- Return underlying socket function interface:socket() return self.conn; end function interface:set_mode(new_mode) self.read_size = new_mode; end function interface:setoption(k, v) -- LuaSec doesn't expose setoption :( local ok, ret, err = pcall(self.conn.setoption, self.conn, k, v); if not ok then self:noise("Setting option %q = %q failed: %s", k, v, ret); return ok, ret; elseif not ret then self:noise("Setting option %q = %q failed: %s", k, v, err); return ret, err; end return ret; end -- Timeout for detecting dead or idle sockets function interface:setreadtimeout(t) if t == false then if self._readtimeout then closetimer(self._readtimeout); self._readtimeout = nil; end return end t = t or cfg.read_timeout; if self._readtimeout then reschedule(self._readtimeout, t); else self._readtimeout = addtimer(t, function () if self:on("readtimeout") then self:noise("Read timeout handled"); return cfg.read_timeout; else self:debug("Read timeout not handled, disconnecting"); self:on("disconnect", "read timeout"); self:destroy(); end end); end end -- Timeout for detecting dead sockets function interface:setwritetimeout(t) if t == false then if self._writetimeout then closetimer(self._writetimeout); self._writetimeout = nil; end return end t = t or cfg.send_timeout; if self._writetimeout then reschedule(self._writetimeout, t); else self._writetimeout = addtimer(t, function () self:noise("Write timeout"); self:on("disconnect", self._connected and "write timeout" or "connection timeout"); self:destroy(); end); end end function interface:add(r, w) local fd = self:getfd(); if fd < 0 then return nil, "invalid fd"; end if r == nil then r = self._wantread; end if w == nil then w = self._wantwrite; end local ok, err, errno = poll:add(fd, r, w); if not ok then if errno == EEXIST then self:debug("FD already registered in poller! (EEXIST)"); return self:set(r, w); -- So try to change its flags end self:debug("Could not register in poller: %s(%d)", err, errno); return ok, err; end self._wantread, self._wantwrite = r, w; fds[fd] = self; self:noise("Registered in poller"); return true; end function interface:set(r, w) local fd = self:getfd(); if fd < 0 then return nil, "invalid fd"; 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); return ok, err; end self._wantread, self._wantwrite = r, w; return true; end function interface:del() local fd = self:getfd(); if fd < 0 then return nil, "invalid fd"; end if fds[fd] ~= self then return nil, "unregistered fd"; end local ok, err, errno = poll:del(fd); if not ok and errno ~= ENOENT then self:debug("Could not unregister: %s(%d)", err, errno); return ok, err; end self._wantread, self._wantwrite = nil, nil; fds[fd] = nil; self:noise("Unregistered from poller"); return true; end function interface:setflags(r, w) if not(self._wantread or self._wantwrite) then if not(r or w) then return true; -- no change end return self:add(r, w); end if not(r or w) then return self:del(); end return self:set(r, w); end -- Called when socket is readable function interface:onreadable() local data, err, partial = self.conn:receive(self.read_size or cfg.read_size); if data then self:onconnect(); self:onincoming(data); else if err == "wantread" then self:set(true, nil); err = "timeout"; elseif err == "wantwrite" then self:set(nil, true); self:setwritetimeout(); err = "timeout"; elseif err == "timeout" and not self._connected then err = "connection timeout"; end if partial and partial ~= "" then self:onconnect(); self:onincoming(partial, err); end if err == "closed" and self._connected then self:debug("Connection closed by remote"); self:on("disconnect", err); self:destroy(); return; elseif err ~= "timeout" then self:debug("Read error, closing (%s)", err); self:on("disconnect", err); self:destroy(); return; end end if not self.conn then return; end if self._limit and (data or partial) then local cost = self._limit * #(data or partial); if cost > cfg.min_wait then self:setreadtimeout(false); self:pausefor(cost); return; end end if not self._wantread then return end if self.conn:dirty() then self:setreadtimeout(false); self:pausefor(cfg.read_retry_delay); else self:setreadtimeout(); end end -- Called when socket is writable function interface:onwritable() self._writing = true; -- prevent reentrant writes etc 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 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 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 buffer:discard(ok); else -- string or don't keep buffers self.writebuffer = nil; end self._writing = nil; self:setwritetimeout(false); self:ondrain(); -- Be aware of writes in ondrain return ok; elseif partial then if type(buffer) == "table" then buffer:discard(partial); else self.writebuffer = data:sub(partial + 1); end self:set(nil, true); self:setwritetimeout(); end self._writing = nil; if err == "wantwrite" or err == "timeout" then self:set(nil, true); self:setwritetimeout(); elseif err == "wantread" then self:set(true, nil); self:setreadtimeout(); elseif err ~= "timeout" then self:on("disconnect", err); self:destroy(); return ok, err; end return true, err; end -- The write buffer has been successfully emptied function interface:ondrain() return self:on("drain"); end -- Add data to write buffer and set flag for wanting to write function interface:write(data) local buffer = self.writebuffer; -- (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 self._opportunistic_write = true; local ret, err = self:onwritable(); self._opportunistic_write = nil; return ret, err; end self:setwritetimeout(); self:set(nil, true); end return #data; end interface.send = interface.write; -- Close, possibly after writing is done function interface:close() local status, err; if self.writebuffer and #self.writebuffer ~= 0 then self._connected = false; self:set(false, true); -- Flush final buffer contents self:setreadtimeout(false); self:setwritetimeout(); self.write, self.send = noop, noop; -- No more writing self:debug("Close after writing remaining buffered data"); self.ondrain = interface.close; elseif self.conn.shutdown and self._tls then status, err = self.conn:shutdown(); self.onreadable = interface.close; self.onwritable = interface.close; if err == nil then if status == true then self._tls = false; end return self:close(); elseif err == "wantread" then self:set(true, nil); self:setreadtimeout(); elseif err == "wantwrite" then self:set(nil, true); self:setwritetimeout(); else self._tls = false; end else self:debug("Closing now"); self.write, self.send = noop, noop; self.close = noop; self:on("disconnect"); self:destroy(); end end function interface:destroy() -- make sure tls sockets aren't put in blocking mode if self.conn.shutdown and self._tls then self.conn:shutdown(); end self:del(); self:setwritetimeout(false); self:setreadtimeout(false); self.onreadable = noop; self.onwritable = noop; self.destroy = noop; self.close = noop; self.on = noop; self.conn:close(); self.conn = nil; end function interface:ssl() return self._tls; end 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 ~= 0 then self:debug("Start TLS after write"); self.ondrain = interface.starttls; self:set(nil, true); -- make sure wantwrite is set else if self.ondrain == interface.starttls then self.ondrain = nil; end self.onwritable = interface.inittls; self.onreadable = interface.inittls; self:set(true, true); self:setreadtimeout(false); self:setwritetimeout(self._connected and cfg.ssl_handshake_timeout or cfg.connect_timeout); self:debug("Prepared to start TLS"); end end function interface:inittls(tls_ctx, now) if self._tls then return end if tls_ctx then self.tls_ctx = tls_ctx; end self._tls = true; self.starttls = false; self:debug("Starting TLS now"); self:updatenames(); -- Can't getpeer/sockname after wrap() local conn, err = self.tls_ctx:wrap(self.conn); if not conn then self:on("disconnect", err); self:destroy(); return conn, err; end conn:settimeout(0); self.conn = conn; if conn.sni then if self.servername then conn:sni(self.servername); 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 -- TODO Error handling if not conn:setdane(self.servername or self.extra.dane_hostname) then self:debug("Could not enable DANE on connection"); else self:debug("Enabling DANE with %d TLSA records", #self.extra.tlsa); self:noise("DANE hostname is %q", self.servername or self.extra.dane_hostname); for _, tlsa in ipairs(self.extra.tlsa) do self:noise("TLSA: %s", tlsa); conn:settlsa(tlsa.use, tlsa.select, tlsa.match, tlsa.data); end end end self:on("starttls"); self.ondrain = nil; self.onwritable = interface.tlshandshake; self.onreadable = interface.tlshandshake; if now then return self:tlshandshake() end self:setreadtimeout(false); self:setwritetimeout(cfg.ssl_handshake_timeout); self:set(true, true); end function interface:tlshandshake() self:setreadtimeout(false); self:noise("Continuing TLS handshake"); local ok, err = self.conn:dohandshake(); if ok then local info = self.conn.info and self.conn:info(); if type(info) == "table" then self:debug("TLS handshake complete (%s with %s)", info.protocol, info.cipher); else self:debug("TLS handshake complete"); end self:setwritetimeout(false); self.onwritable = nil; self.onreadable = nil; self:on("status", "ssl-handshake-complete"); self:set(true, true); self:onconnect(); self:onreadable(); elseif err == "wantread" then self:noise("TLS handshake to wait until readable"); self:set(true, false); self:setwritetimeout(cfg.ssl_handshake_timeout); elseif err == "wantwrite" then self:noise("TLS handshake to wait until writable"); self:set(false, true); self:setwritetimeout(cfg.ssl_handshake_timeout); else self:debug("TLS handshake error: %s", err); self:on("disconnect", err); self:destroy(); end end local function wrapsocket(client, server, read_size, listeners, tls_ctx, extra) -- luasocket object -> interface object client:settimeout(0); local conn_id = ("conn%s"):format(new_id()); local conn = setmetatable({ conn = client; _server = server; created = realtime(); listeners = listeners; read_size = read_size or (server and server.read_size); writebuffer = nil; tls_ctx = tls_ctx or (server and server.tls_ctx); tls_direct = server and server.tls_direct; id = conn_id; log = logger.init(conn_id); extra = extra; }, interface_mt); if extra then if extra.servername then conn.servername = extra.servername; end end return conn; end function interface:updatenames() local conn = self.conn; local ok, peername, peerport = pcall(conn.getpeername, conn); if ok and peername then self.peername, self.peerport = peername, peerport or 0; end local ok, sockname, sockport = pcall(conn.getsockname, conn); if ok and sockname then self.sockname, self.sockport = sockname, sockport or 0; end end -- A server interface has new incoming connections waiting -- This replaces the onreadable callback function interface:onacceptable() local conn, err = self.conn:accept(); if not conn then self:debug("Error accepting new client: %s, server will be paused for %ds", err, cfg.accept_retry_interval); self:pausefor(cfg.accept_retry_interval); 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; if self.tls_direct then client:add(true, true); client:inittls(self.tls_ctx, true); else client:add(true, false); client:onconnect(); client:onreadable(); end end -- Initialization for outgoing connections function interface:init() self:setwritetimeout(cfg.connect_timeout); self:defaultoptions(); return self:add(true, true); end function interface:defaultoptions() if cfg.nagle == false then self:setoption("tcp-nodelay", true); end if cfg.tcp_keepalive then self:setoption("keepalive", true); if type(cfg.tcp_keepalive) == "number" then self:setoption("tcp-keepidle", cfg.tcp_keepalive); end end end function interface:pause() self:noise("Pause reading"); self:setreadtimeout(false); return self:set(false); end function interface:resume() self:noise("Resume reading"); self:setreadtimeout(); return self:set(true); end -- Pause connection for some time function interface:pausefor(t) self:noise("Pause for %fs", t); if self._pausefor then closetimer(self._pausefor); self._pausefor = nil; end if t == false then return; end self:set(false); self._pausefor = addtimer(t, function () self._pausefor = nil; self:set(true); self:noise("Resuming after pause"); if self.conn and self.conn:dirty() then self:noise("Have buffered incoming data to process"); self:onreadable(); end end); end function interface:setlimit(Bps) if Bps > 0 then self._limit = 1/Bps; else self._limit = nil; end end function interface:pause_writes() if self._write_lock then return end self:noise("Pause writes"); self._write_lock = true; self:setwritetimeout(false); self:set(nil, false); end function interface:resume_writes() if not self._write_lock then return end self:noise("Resume writes"); self._write_lock = nil; if self.writebuffer and #self.writebuffer ~= 0 then self:setwritetimeout(); self:set(nil, true); end end -- Connected! function interface:onconnect() self._connected = true; self:updatenames(); self:debug("Connected (%s)", self); self.onconnect = noop; self:on("connect"); end local function wrapserver(conn, addr, port, listeners, config) local server = setmetatable({ conn = conn; created = realtime(); listeners = listeners; read_size = config and config.read_size; onreadable = interface.onacceptable; tls_ctx = config and config.tls_ctx; tls_direct = config and config.tls_direct; hosts = config and config.sni_hosts; sockname = addr; sockport = port; 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); return wrapserver(conn, addr, port, listeners, config); end -- COMPAT local function addserver(addr, port, listeners, read_size, tls_ctx) return listen(addr, port, listeners, { read_size = read_size; tls_ctx = tls_ctx; tls_direct = tls_ctx and true or false; }); 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 local ok, err = client:init(); if not ok then return ok, err; end if tls_ctx then client:starttls(tls_ctx); end return client; end -- New outgoing TCP connection local function addclient(addr, port, listeners, read_size, tls_ctx, typ, extra) local create; if not typ then local n = inet_pton(addr); if not n then return nil, "invalid-ip"; end if #n == 16 then typ = "tcp6"; else typ = "tcp4"; end end if typ then create = socket[typ]; end if type(create) ~= "function" then return nil, "invalid socket type"; end local conn, err = create(); 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 client:updatenames(); local ok, err = client:init(); if not client.peername then -- otherwise not set until connected client.peername, client.peerport = addr, port; end if not ok then return ok, err; end client:debug("Client %s created", client); if tls_ctx then client:starttls(tls_ctx); end return client, conn; end local function watchfd(fd, onreadable, onwritable) local conn = setmetatable({ conn = fd; onreadable = onreadable; onwritable = onwritable; close = function (self) self:del(); end }, interface_mt); if type(fd) == "number" then conn.getfd = function () return fd; end; -- Otherwise it'll need to be something LuaSocket-compatible end conn.id = new_id(); conn.log = logger.init(("fdwatch%s"):format(conn.id)); conn:add(onreadable, onwritable); return conn; end; -- Dump all data from one connection into another local function link(from, to, read_size) from:debug("Linking to %s", to.id); function from:onincoming(data) self:pause(); to:write(data); end function to:ondrain() -- luacheck: ignore 212/self from:resume(); end from:set_mode(read_size); from:set(true, nil); to:set(nil, true); end -- COMPAT -- net.adns calls this but then replaces :send so this can be a noop function interface:set_send(new_send) -- luacheck: ignore 212 end -- Close all connections and servers local function closeall() for fd, conn in pairs(fds) do -- luacheck: ignore 213/fd conn:close(); end end local quitting = nil; -- Signal main loop about shutdown via above upvalue local function setquitting(quit) if quit then quitting = "quitting"; closeall(); addtimer(1, function () if quitting then closeall(); return 1; end end); if cfg.shutdown_deadline then addtimer(cfg.shutdown_deadline, function () if quitting then for fd, conn in pairs(fds) do -- luacheck: ignore 213/fd conn:destroy(); end end end); end else quitting = nil; 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) if once then return loop_once(); end local t = 0; while not quitting do local fd, r, w = poll:wait(t); if fd then t = 0; 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 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 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; addclient = addclient; add_task = addtimer; timer = { -- API-compatible with util.timer add_task = addtimer; stop = closetimer; reschedule = reschedule; to_absolute_time = function (t) return t-monotonic()+realtime(); end; }; listen = listen; loop = loop; closeall = closeall; setquitting = setquitting; wrapclient = wrapclient; wrapserver = wrapserver; watchfd = watchfd; link = link; 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 }; addevent = function (fd, mode, callback) log("warn", "Using deprecated libevent emulation, please update code to use watchfd API instead"); local function onevent(self) local ret = self:callback(); if ret == -1 then self:set(false, false); elseif ret then self:set(mode == "r" or mode == "rw", mode == "w" or mode == "rw"); end end local conn = setmetatable({ getfd = function () return fd; end; callback = callback; onreadable = onevent; onwritable = onevent; close = function (self) self:del(); fds[fd] = nil; end; }, interface_mt); conn.id = conn:getfd(); conn.log = logger.init(("fdwatch%d"):format(conn.id)); local ok, err = conn:add(mode == "r" or mode == "rw", mode == "w" or mode == "rw"); if not ok then return ok, err; end return conn; end; }; prosody-13.0.1/net/PaxHeaders/server_event.lua0000644000000000000000000000011714773555365016400 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.567710439 prosody-13.0.1/net/server_event.lua0000644000175000017500000010054514773555365020604 0ustar00prosodyprosody00000000000000--[[ server.lua based on lua/libevent by blastbeat notes: -- when using luaevent, never register 2 or more EV_READ at one socket, same for EV_WRITE -- you can't even register a new EV_READ/EV_WRITE callback inside another one -- to do some of the above, use timeout events or something what will called from outside -- don't let garbagecollect eventcallbacks, as long they are running -- when using luasec, there are 4 cases of timeout errors: wantread or wantwrite during reading or writing --]] -- luacheck: ignore 212/self 431/err 211/ret local SCRIPT_NAME = "server_event.lua" local SCRIPT_VERSION = "0.05" local SCRIPT_AUTHOR = "blastbeat" local LAST_MODIFIED = "2009/11/20" local cfg = { MAX_CONNECTIONS = 100000, -- max per server connections (use "ulimit -n" on *nix) MAX_HANDSHAKE_ATTEMPTS= 1000, -- attempts to finish ssl handshake HANDSHAKE_TIMEOUT = 60, -- timeout in seconds per handshake attempt MAX_READ_LENGTH = 1024 * 1024 * 1024 * 1024, -- max bytes allowed to read from sockets MAX_SEND_LENGTH = 1024 * 1024 * 1024 * 1024, -- max bytes size of write buffer (for writing on sockets) ACCEPT_QUEUE = 128, -- might influence the length of the pending sockets queue ACCEPT_DELAY = 10, -- seconds to wait until the next attempt of a full server to accept READ_TIMEOUT = 14 * 60, -- timeout in seconds for read data from socket WRITE_TIMEOUT = 180, -- timeout in seconds for write data on socket CONNECT_TIMEOUT = 20, -- timeout in seconds for connection attempts CLEAR_DELAY = 5, -- seconds to wait for clearing interface list (and calling ondisconnect listeners) READ_RETRY_DELAY = 1e-06, -- if, after reading, there is still data in buffer, wait this long and continue reading DEBUG = true, -- show debug messages } local pairs = pairs local select = select local require = require local tostring = tostring local setmetatable = setmetatable local t_insert = table.insert local t_concat = table.concat local s_sub = string.sub local coroutine_wrap = coroutine.wrap local coroutine_yield = coroutine.yield local has_luasec = pcall ( require , "ssl" ) local socket = require "socket" local levent = require "luaevent.core" 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 ("prosody.util.logger").init("socket") local function debug(...) return log("debug", ("%s "):rep(select('#', ...)), ...) end -- local vdebug = debug; local bitor = ( function( ) -- thx Rici Lake local hasbit = function( x, p ) return x % ( p + p ) >= p end return function( x, y ) local p = 1 local z = 0 local limit = x > y and x or y while p <= limit do if hasbit( x, p ) or hasbit( y, p ) then z = z + p end p = p + p end return z end end )( ) local base = levent.new( ) local addevent = base.addevent local EV_READ = levent.EV_READ local EV_WRITE = levent.EV_WRITE local EV_TIMEOUT = levent.EV_TIMEOUT local EV_SIGNAL = levent.EV_SIGNAL local EV_READWRITE = bitor( EV_READ, EV_WRITE ) local interfacelist = { } -- Client interface methods local interface_mt = {}; interface_mt.__index = interface_mt; -- Private methods function interface_mt:_close() return self:_destroy(); end function interface_mt:_start_connection(plainssl) -- called from wrapclient local callback = function( event ) if EV_TIMEOUT == event then -- timeout during connection self.fatalerror = "connection timeout" self:ontimeout() -- call timeout listener self:_close() debug( "new connection failed. id:", self.id, "error:", self.fatalerror ) else if EV_READWRITE == event then if self.readcallback(event) == -1 then -- Fatal error occurred return -1; end end if plainssl and has_luasec then -- start ssl session self:starttls(self._sslctx, true) else -- normal connection self:_start_session(true) end debug( "new connection established. id:", self.id ) end self.eventconnect = nil return -1 end self.eventconnect = addevent( base, self.conn, EV_READWRITE, callback, cfg.CONNECT_TIMEOUT ) return true end function interface_mt:_start_session(call_onconnect) -- new session, for example after startssl if self.type == "client" then local callback = function( ) self:_lock( false, false, false ) --vdebug( "start listening on client socket with id:", self.id ) self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT ); -- register callback if call_onconnect then self:onconnect() end self.eventsession = nil return -1 end self.eventsession = addevent( base, nil, EV_TIMEOUT, callback, 0 ) else self:_lock( false ) --vdebug( "start listening on server socket with id:", self.id ) self.eventread = addevent( base, self.conn, EV_READ, self.readcallback ) -- register callback end return true end function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed, therefore we have to close read/write events first --vdebug( "starting ssl session with client id:", self.id ) local _ _ = self.eventread and self.eventread:close( ) -- close events; this must be called outside of the event callbacks! _ = self.eventwrite and self.eventwrite:close( ) self.eventread, self.eventwrite = nil, nil local err self.conn, err = self._sslctx:wrap(self.conn) if err then self.fatalerror = err self.conn = nil -- cannot be used anymore if call_onconnect then self.ondisconnect = nil -- don't call this when client isn't really connected end self:_close() debug( "fatal error while ssl wrapping:", err ) return false end if self.conn.sni then if self.servername then self.conn:sni(self.servername); elseif next(self._sslctx._sni_contexts) ~= nil then self.conn:sni(self._sslctx._sni_contexts, true); end end self.conn:settimeout( 0 ) -- set non blocking local handshakecallback = coroutine_wrap(function( event ) local _, err local attempt = 0 local maxattempt = cfg.MAX_HANDSHAKE_ATTEMPTS while attempt < maxattempt do -- no endless loop attempt = attempt + 1 debug( "ssl handshake of client with id:"..tostring(self)..", attempt:"..attempt ) if attempt > maxattempt then self.fatalerror = "max handshake attempts exceeded" elseif EV_TIMEOUT == event then self.fatalerror = "timeout during handshake" else _, err = self.conn:dohandshake( ) if not err then self:_lock( false, false, false ) -- unlock the interface; sending, closing etc allowed self.send = self.conn.send -- caching table lookups with new client object self.receive = self.conn.receive if not call_onconnect then -- trigger listener self:onstatus("ssl-handshake-complete"); end self:_start_session( call_onconnect ) debug( "ssl handshake done" ) self.eventhandshake = nil return -1 end if err == "wantwrite" then event = EV_WRITE elseif err == "wantread" then event = EV_READ else debug( "ssl handshake error:", err ) self.fatalerror = err end end if self.fatalerror then if call_onconnect then self.ondisconnect = nil -- don't call this when client isn't really connected end self:_close() debug( "handshake failed because:", self.fatalerror ) self.eventhandshake = nil return -1 end event = coroutine_yield( event, cfg.HANDSHAKE_TIMEOUT ) -- yield this monster... end end ) debug "starting handshake..." self:_lock( false, true, true ) -- unlock read/write events, but keep interface locked self.eventhandshake = addevent( base, self.conn, EV_READWRITE, handshakecallback, cfg.HANDSHAKE_TIMEOUT ) return true end function interface_mt:_destroy() -- close this interface + events and call last listener debug( "closing client with id:", self.id, self.fatalerror ) self:_lock( true, true, true ) -- first of all, lock the interface to avoid further actions local _ _ = self.eventread and self.eventread:close( ) if self.type == "client" then _ = self.eventwrite and self.eventwrite:close( ) _ = self.eventhandshake and self.eventhandshake:close( ) _ = self.eventstarthandshake and self.eventstarthandshake:close( ) _ = self.eventconnect and self.eventconnect:close( ) _ = self.eventsession and self.eventsession:close( ) _ = self.eventwritetimeout and self.eventwritetimeout:close( ) _ = self.eventreadtimeout and self.eventreadtimeout:close( ) -- call ondisconnect listener (won't be the case if handshake failed on connect) _ = self.ondisconnect and self:ondisconnect( self.fatalerror ~= "client to close" and self.fatalerror) _ = self.conn and self.conn:close( ) -- close connection _ = self._server and self._server:counter(-1); self.eventread, self.eventwrite = nil, nil self.eventstarthandshake, self.eventhandshake, self.eventclose = nil, nil, nil self.readcallback, self.writecallback = nil, nil else self.conn:close( ) self.eventread, self.eventclose = nil, nil self.interface, self.readcallback = nil, nil end interfacelist[ self ] = nil return true end function interface_mt:_lock(nointerface, noreading, nowriting) -- lock or unlock this interface or events self.nointerface, self.noreading, self.nowriting = nointerface, noreading, nowriting return nointerface, noreading, nowriting end --TODO: Deprecate function interface_mt:lock_read(switch) log("warn", ":lock_read is deprecated, use :pause() and :resume()"); if switch then return self:pause(); else return self:resume(); end end 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 self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT ); -- register callback return true; end end function interface_mt:pause_writes() return self:_lock(self.nointerface, self.noreading, true); end function interface_mt:resume_writes() self:_lock(self.nointerface, self.noreading, false); if self.writecallback and not self.eventwrite then self.eventwrite = addevent( base, self.conn, EV_WRITE, self.writecallback, cfg.WRITE_TIMEOUT ); -- register callback return true; end end function interface_mt:counter(c) if c then self._connections = self._connections + c end return self._connections end -- Public methods function interface_mt:write(data) if self.nointerface then return nil, "locked"; end --vdebug( "try to send data to client, id/data:", self.id, data ) data = tostring( data ) local len = #data local total = len + self.writebufferlen if total > cfg.MAX_SEND_LENGTH then -- check buffer length local err = "send buffer exceeded" debug( "error:", err ) -- to much, check your app return nil, err end t_insert(self.writebuffer, data) -- new buffer self.writebufferlen = total if not self.eventwrite and not self.nowriting then -- register new write event --vdebug( "register new write event" ) self.eventwrite = addevent( base, self.conn, EV_WRITE, self.writecallback, cfg.WRITE_TIMEOUT ) end return true end function interface_mt:close() if self.nointerface then return nil, "locked"; end debug( "try to close client connection with id:", self.id ) if self.type == "client" then self.fatalerror = "client to close" if self.eventwrite then -- wait for incomplete write request self:_lock( true, true, false ) debug "closing delayed until writebuffer is empty" return nil, "writebuffer not empty, waiting" else -- close now self:_lock( true, true, true ) self:_close() return true end else debug( "try to close server with id:", tostring(self.id)) self.fatalerror = "server to close" self:_lock( true ) self:_close( 0 ) return true end end function interface_mt:socket() return self.conn end function interface_mt:server() return self._server or self; end function interface_mt:port() return self._port end function interface_mt:serverport() return self._serverport end function interface_mt:ip() return self._ip end function interface_mt:ssl() return self._usingssl end interface_mt.clientport = interface_mt.port -- COMPAT server_select function interface_mt:type() return self._type or "client" end function interface_mt:connections() return self._connections end function interface_mt:address() return self.addr end function interface_mt:set_sslctx(sslctx) self._sslctx = sslctx; if sslctx then self.starttls = nil; -- use starttls() of interface_mt else self.starttls = false; -- prevent starttls() end end function interface_mt:set_mode(pattern) if pattern then self._pattern = pattern; end return self._pattern; end function interface_mt:set_send(new_send) -- luacheck: ignore 212 -- No-op, we always use the underlying connection's send end function interface_mt:starttls(sslctx, call_onconnect) debug( "try to start ssl at client id:", self.id ) local err self._sslctx = sslctx; if self._usingssl then -- startssl was already called err = "ssl already active" end if err then debug( "error:", err ) return nil, err end self._usingssl = true self.startsslcallback = function( ) -- we have to start the handshake outside of a read/write event self.startsslcallback = nil self:_start_ssl(call_onconnect); self.eventstarthandshake = nil return -1 end if not self.eventwrite then self:_lock( true, true, true ) -- lock the interface, to not disturb the handshake self.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, self.startsslcallback, 0 ) -- add event to start handshake else -- wait until writebuffer is empty self:_lock( true, true, false ) debug "ssl session delayed until writebuffer is empty..." end self.starttls = false; return true end function interface_mt:setoption(option, value) if self.conn.setoption then return self.conn:setoption(option, value); end return false, "setoption not implemented"; end function interface_mt:setlistener(listener, data) self:ondetach(); -- Notify listener that it is no longer responsible for this connection self.onconnect = listener.onconnect; self.ondisconnect = listener.ondisconnect; self.onincoming = listener.onincoming; self.ontimeout = listener.ontimeout; self.onreadtimeout = listener.onreadtimeout; self.onstatus = listener.onstatus; self.ondetach = listener.ondetach; self.onattach = listener.onattach; self.onpredrain = listener.onpredrain; self.ondrain = listener.ondrain; self:onattach(data); end -- Stub handlers function interface_mt:onconnect() end function interface_mt:onincoming() end function interface_mt:ondisconnect() end function interface_mt:ontimeout() end function interface_mt:onreadtimeout() end function interface_mt:onpredrain() end function interface_mt:ondrain() end function interface_mt:ondetach() end function interface_mt:onattach() end function interface_mt:onstatus() end -- End of client interface methods local function handleclient( client, ip, port, server, pattern, listener, sslctx, extra ) -- creates an client interface --vdebug("creating client interfacce...") local interface = { type = "client"; conn = client; currenttime = socket_gettime( ); -- safe the origin writebuffer = {}; -- writebuffer writebufferlen = 0; -- length of writebuffer send = client.send; -- caching table lookups receive = client.receive; onconnect = listener.onconnect; -- will be called when client disconnects ondisconnect = listener.ondisconnect; -- will be called when client disconnects onincoming = listener.onincoming; -- will be called when client sends data ontimeout = listener.ontimeout; -- called when fatal socket timeout occurs onreadtimeout = listener.onreadtimeout; -- called when socket inactivity timeout occurs onpredrain = listener.onpredrain; -- called before writes ondrain = listener.ondrain; -- called when writebuffer is empty ondetach = listener.ondetach; -- called when disassociating this listener from this connection onstatus = listener.onstatus; -- called for status changes (e.g. of SSL/TLS) eventread = false, eventwrite = false, eventclose = false, eventhandshake = false, eventstarthandshake = false; -- event handler eventconnect = false, eventsession = false; -- more event handler... eventwritetimeout = false; -- even more event handler... eventreadtimeout = false; fatalerror = false; -- error message writecallback = false; -- will be called on write events readcallback = false; -- will be called on read events nointerface = true; -- lock/unlock parameter of this interface noreading = false, nowriting = false; -- locks of the read/writecallback startsslcallback = false; -- starting handshake callback position = false; -- position of client in interfacelist -- Properties _ip = ip, _port = port, _server = server, _pattern = pattern, _serverport = (server and server:port() or nil), _sslctx = sslctx; -- parameters _usingssl = false; -- client is using ssl; extra = extra; servername = extra and extra.servername; } if not has_luasec then interface.starttls = false; end interface.id = tostring(interface):match("%x+$"); interface.writecallback = function( event ) -- called on write events --vdebug( "new client write event, id/ip/port:", interface, ip, port ) if interface.nowriting or ( interface.fatalerror and ( "client to close" ~= interface.fatalerror ) ) then -- leave this event --vdebug( "leaving this event because:", interface.nowriting or interface.fatalerror ) interface.eventwrite = false return -1 end if EV_TIMEOUT == event then -- took too long to write some data to socket -> disconnect interface.fatalerror = "timeout during writing" debug( "writing failed:", interface.fatalerror ) interface:_close() interface.eventwrite = false return -1 else -- can write :) if interface._usingssl then -- handle luasec if interface.eventreadtimeout then -- we have to read first local ret = interface.readcallback( ) -- call readcallback --vdebug( "tried to read in writecallback, result:", ret ) end if interface.eventwritetimeout then -- luasec only interface.eventwritetimeout:close( ) -- first we have to close timeout event which where registered after a wantread error interface.eventwritetimeout = false end end interface:onpredrain(); interface.writebuffer = { t_concat(interface.writebuffer) } local succ, err, byte = interface.conn:send( interface.writebuffer[1], 1, interface.writebufferlen ) --vdebug( "write data:", interface.writebuffer, "error:", err, "part:", byte ) if succ then -- writing successful interface.writebuffer[1] = nil interface.writebufferlen = 0 interface:ondrain(); if interface.fatalerror then debug "closing client after writing" interface:_close() -- close interface if needed elseif interface.startsslcallback then -- start ssl connection if needed debug "starting ssl handshake after writing" interface.eventstarthandshake = addevent( base, nil, EV_TIMEOUT, interface.startsslcallback, 0 ) elseif interface.writebufferlen ~= 0 then -- data possibly written from ondrain return EV_WRITE, cfg.WRITE_TIMEOUT elseif interface.eventreadtimeout then return EV_WRITE, cfg.WRITE_TIMEOUT end interface.eventwrite = nil return -1 elseif byte and (err == "timeout" or err == "wantwrite") then -- want write again --vdebug( "writebuffer is not empty:", err ) interface.writebuffer[1] = s_sub( interface.writebuffer[1], byte + 1, interface.writebufferlen ) -- new buffer interface.writebufferlen = interface.writebufferlen - byte if "wantread" == err then -- happens only with luasec local callback = function( ) interface:_close() interface.eventwritetimeout = nil return -1; end interface.eventwritetimeout = addevent( base, nil, EV_TIMEOUT, callback, cfg.WRITE_TIMEOUT ) -- reg a new timeout event debug( "wantread during write attempt, reg it in readcallback but don't know what really happens next..." ) -- hopefully this works with luasec; its simply not possible to use 2 different write events on a socket in luaevent return -1 end return EV_WRITE, cfg.WRITE_TIMEOUT else -- connection was closed during writing or fatal error interface.fatalerror = err or "fatal error" debug( "connection failed in write event:", interface.fatalerror ) interface:_close() interface.eventwrite = nil return -1 end end end interface.readcallback = function( event ) -- called on read events --vdebug( "new client read event, id/ip/port:", tostring(interface.id), tostring(ip), tostring(port) ) if interface.noreading or interface.fatalerror then -- leave this event --vdebug( "leaving this event because:", tostring(interface.noreading or interface.fatalerror) ) interface.eventread = nil return -1 end if EV_TIMEOUT == event and not interface.conn:dirty() and interface:onreadtimeout() ~= true then interface.fatalerror = "timeout during receiving" debug( "connection failed:", interface.fatalerror ) interface:_close() interface.eventread = nil return -1 -- took too long to get some data from client -> disconnect end if interface._usingssl then -- handle luasec if interface.eventwritetimeout then -- ok, in the past writecallback was registered local ret = interface.writecallback( ) -- call it --vdebug( "tried to write in readcallback, result:", tostring(ret) ) end if interface.eventreadtimeout then interface.eventreadtimeout:close( ) interface.eventreadtimeout = nil end end local buffer, err, part = interface.conn:receive( interface._pattern ) -- receive buffer with "pattern" --vdebug( "read data:", tostring(buffer), "error:", tostring(err), "part:", tostring(part) ) buffer = buffer or part if buffer and #buffer > cfg.MAX_READ_LENGTH then -- check buffer length interface.fatalerror = "receive buffer exceeded" debug( "fatal error:", interface.fatalerror ) interface:_close() interface.eventread = nil return -1 end if err and ( err ~= "timeout" and err ~= "wantread" ) then if "wantwrite" == err then -- need to read on write event if not interface.eventwrite then -- register new write event if needed interface.eventwrite = addevent( base, interface.conn, EV_WRITE, interface.writecallback, cfg.WRITE_TIMEOUT ) end interface.eventreadtimeout = addevent( base, nil, EV_TIMEOUT, function( ) interface:_close() end, cfg.READ_TIMEOUT) debug( "wantwrite during read attempt, reg it in writecallback but don't know what really happens next..." ) -- to be honest i don't know what happens next, if it is allowed to first read, the write etc... else -- connection was closed or fatal error interface.fatalerror = err debug( "connection failed in read event:", interface.fatalerror ) interface:_close() interface.eventread = nil return -1 end else interface.onincoming( interface, buffer, err ) -- send new data to listener end if interface.noreading then interface.eventread = nil; return -1; end if interface.conn:dirty() then -- still data left in buffer return EV_TIMEOUT, cfg.READ_RETRY_DELAY; end return EV_READ, cfg.READ_TIMEOUT end client:settimeout( 0 ) -- set non blocking setmetatable(interface, interface_mt) interfacelist[ interface ] = true -- add to interfacelist return interface end local function handleserver( server, addr, port, pattern, listener, sslctx, startssl ) -- creates a server interface debug "creating server interface..." local interface = { _connections = 0; type = "server"; conn = server; onconnect = listener.onconnect; -- will be called when new client connected eventread = false; -- read event handler eventclose = false; -- close event handler readcallback = false; -- read event callback fatalerror = false; -- error message nointerface = true; -- lock/unlock parameter _ip = addr, _port = port, _pattern = pattern, _sslctx = sslctx; hosts = {}; } interface.id = tostring(interface):match("%x+$"); interface.readcallback = function( event ) -- server handler, called on incoming connections --vdebug( "server can accept, id/addr/port:", interface, addr, port ) if interface.fatalerror then --vdebug( "leaving this event because:", self.fatalerror ) interface.eventread = nil return -1 end local delay = cfg.ACCEPT_DELAY if EV_TIMEOUT == event then if interface._connections >= cfg.MAX_CONNECTIONS then -- check connection count debug( "to many connections, seconds to wait for next accept:", delay ) return EV_TIMEOUT, delay -- timeout... else return EV_READ -- accept again end end --vdebug("max connection check ok, accepting...") -- luacheck: ignore 231/err local client, err = server:accept() -- try to accept; TODO: check err while client do if interface._connections >= cfg.MAX_CONNECTIONS then client:close( ) -- refuse connection debug( "maximal connections reached, refuse client connection; accept delay:", delay ) return EV_TIMEOUT, delay -- delay for next accept attempt end local client_ip, client_port = addr, port; if client.getpeername then -- Only IP sockets have this method, UNIX sockets don't client_ip, client_port = client:getpeername( ) end interface._connections = interface._connections + 1 -- increase connection count local clientinterface = handleclient( client, client_ip, client_port, interface, pattern, listener, sslctx ) --vdebug( "client id:", clientinterface, "startssl:", startssl ) if has_luasec and startssl then clientinterface:starttls(sslctx, true) else clientinterface:_start_session( true ) end debug( "accepted incoming client connection from:", client_ip or "", client_port or "", "to", port or ""); client, err = server:accept() -- try to accept again end return EV_READ end server:settimeout( 0 ) setmetatable(interface, interface_mt) interfacelist[ interface ] = true interface:_start_session() return interface end local function wrapserver(conn, addr, port, listeners, config) config = config or {} if config.sslctx and not has_luasec then debug "fatal error: luasec not found" return nil, "luasec not found" end local interface = handleserver( conn, addr, port, config.read_size, listeners, config.tls_ctx, config.tls_direct) -- new server handler debug( "new server created with id:", tostring(interface)) return interface end local function listen(addr, port, listener, config) config = config or {} if config.sslctx and not has_luasec then debug "fatal error: luasec not found" return nil, "luasec not found" end local server, err = socket.bind( addr, port, cfg.ACCEPT_QUEUE ) -- create server socket if not server then debug( "creating server socket on "..addr.." port "..port.." failed:", err ) return nil, err end local interface = handleserver( server, addr, port, config.read_size, listener, config.tls_ctx, config.tls_direct) -- new server handler debug( "new server created with id:", tostring(interface)) return interface end local function addserver( addr, port, listener, pattern, sslctx ) -- TODO: check arguments --vdebug( "creating new tcp server with following parameters:", addr or "nil", port or "nil", sslctx or "nil", startssl or "nil") return listen( addr, port, listener, { read_size = pattern, tls_ctx = sslctx, tls_direct = not not sslctx, }); end local function wrapclient( client, ip, port, listeners, pattern, sslctx, extra ) local interface = handleclient( client, ip, port, nil, pattern, listeners, sslctx, extra ) interface:_start_connection(sslctx) return interface, client --function handleclient( client, ip, port, server, pattern, listener, _, sslctx ) -- creates an client interface end local function addclient( addr, serverport, listener, pattern, sslctx, typ, extra ) if sslctx and not has_luasec then debug "need luasec, but not available" return nil, "luasec not found" end if not typ then local n = inet_pton(addr); if not n then return nil, "invalid-ip"; end if #n == 16 then typ = "tcp6"; elseif #n == 4 then typ = "tcp4"; end end local create = socket[typ]; if type( create ) ~= "function" then return nil, "invalid socket type" end local client, err = create() -- creating new socket if not client then debug( "cannot create socket:", err ) return nil, err end client:settimeout( 0 ) -- set nonblocking local res, err = client:setpeername( addr, serverport ) -- connect if res or ( err == "timeout" ) then -- luacheck: ignore 211/port local ip, port = client:getsockname( ) local interface = wrapclient( client, ip, serverport, listener, pattern, sslctx, extra ) debug( "new connection id:", interface.id ) return interface, err else debug( "new connection failed:", err ) return nil, err end end local function loop( ) -- starts the event loop base:loop( ) return "quitting"; end local function newevent( ... ) return addevent( base, ... ) end local function closeallservers ( arg ) for item in pairs( interfacelist ) do if item.type == "server" then item:close( arg ) end end end local function setquitting(yes) if yes then -- Quit now if yes ~= "once" then closeallservers(); end base:loopexit(); end end local function get_backend() return "libevent " .. base:method(); end -- We need to hold onto the events to stop them -- being garbage-collected local signal_events = {}; -- [signal_num] -> event object local function hook_signal(signal_num, handler) local function _handler() local ret = handler(); if ret ~= false then -- Continue handling this signal? return EV_SIGNAL; -- Yes end return -1; -- Close this event end signal_events[signal_num] = base:addevent(signal_num, EV_SIGNAL, _handler); return signal_events[signal_num]; end local function link(sender, receiver, buffersize) local sender_locked; function receiver:ondrain() if sender_locked then sender:resume(); sender_locked = nil; end end function sender:onincoming(data) receiver:write(data); if receiver.writebufferlen >= buffersize then sender_locked = true; sender:pause(); end end sender:set_mode("*a"); end local function add_task(delay, callback) local event_handle; event_handle = base:addevent(nil, 0, function () local ret = callback(socket_gettime()); if ret then return 0, ret; elseif event_handle then return -1; end end , delay); return event_handle; end local function watchfd(fd, onreadable, onwriteable) local handle = {}; function handle:setflags(r,w) if r ~= nil then if r and not self.wantread then self.wantread = base:addevent(fd, EV_READ, function () onreadable(self); end); elseif not r and self.wantread then self.wantread:close(); self.wantread = nil; end end if w ~= nil then if w and not self.wantwrite then self.wantwrite = base:addevent(fd, EV_WRITE, function () onwriteable(self); end); elseif not r and self.wantread then self.wantwrite:close(); self.wantwrite = nil; end end end handle:setflags(onreadable, onwriteable); return handle; end return { cfg = cfg, base = base, loop = loop, link = link, event = levent, event_base = base, addevent = newevent, addserver = addserver, listen = listen, addclient = addclient, wrapclient = wrapclient, wrapserver = wrapserver, setquitting = setquitting, closeall = closeallservers, get_backend = get_backend, hook_signal = hook_signal, 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, __VERSION = SCRIPT_VERSION, } prosody-13.0.1/net/PaxHeaders/server_select.lua0000644000000000000000000000011714773555365016536 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.571710454 prosody-13.0.1/net/server_select.lua0000644000175000017500000010120514773555365020734 0ustar00prosodyprosody00000000000000-- -- server.lua by blastbeat of the luadch project -- Re-used here under the MIT/X Consortium License -- -- Modifications (C) 2008-2010 Matthew Wild, Waqas Hussain -- -- // wrapping luadch stuff // -- local use = function( what ) return _G[ what ] end 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 ----------------------------------// DECLARATION //-- --// constants //-- local STAT_UNIT = 1 -- byte --// lua functions //-- local type = use "type" local pairs = use "pairs" local ipairs = use "ipairs" local tonumber = use "tonumber" local tostring = use "tostring" --// lua libs //-- local table = use "table" local string = use "string" local coroutine = use "coroutine" --// lua lib methods //-- local math_min = math.min local math_huge = math.huge local table_concat = table.concat local table_insert = table.insert local string_sub = string.sub local coroutine_wrap = coroutine.wrap local coroutine_yield = coroutine.yield --// extern libs //-- local luasocket = use "socket" or require "socket" local luasocket_gettime = luasocket.gettime 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 socket_bind = luasocket.bind local socket_select = luasocket.select --// functions //-- local id local loop local stats local idfalse local closeall local addsocket local addserver local listen local addtimer local getserver local wrapserver local getsettings local closesocket local removesocket local removeserver local wrapconnection local changesettings --// tables //-- local _server local _readlist local _timerlist local _sendlist local _socketlist local _closelist local _readtimes local _writetimes local _fullservers --// simple data types //-- local _ local _readlistlen local _sendlistlen local _timerlistlen local _sendtraffic local _readtraffic local _selecttimeout local _tcpbacklog local _accepretry local _starttime local _currenttime local _maxsendlen local _maxreadlen local _checkinterval local _sendtimeout local _readtimeout local _maxselectlen local _maxfd local _maxsslhandshake ----------------------------------// DEFINITION //-- _server = { } -- key = port, value = table; list of listening servers _readlist = { } -- array with sockets to read from _sendlist = { } -- array with sockets to write to _timerlist = { } -- array of timer functions _socketlist = { } -- key = socket, value = wrapped socket (handlers) _readtimes = { } -- key = handler, value = timestamp of last data reading _writetimes = { } -- key = handler, value = timestamp of last data writing/sending _closelist = { } -- handlers to close _fullservers = { } -- servers in a paused state while there are too many clients _readlistlen = 0 -- length of readlist _sendlistlen = 0 -- length of sendlist _timerlistlen = 0 -- length of timerlist _sendtraffic = 0 -- some stats _readtraffic = 0 _selecttimeout = 1 -- timeout of socket.select _tcpbacklog = 128 -- some kind of hint to the OS _accepretry = 10 -- seconds to wait until the next attempt of a full server to accept _maxsendlen = 51000 * 1024 -- max len of send buffer _maxreadlen = 25000 * 1024 -- max len of read buffer _checkinterval = 30 -- interval in secs to check idle clients _sendtimeout = 60000 -- allowed send idle time in secs _readtimeout = 14 * 60 -- allowed read idle time in secs local is_windows = package.config:sub(1,1) == "\\" -- check the directory separator, to determine whether this is Windows _maxfd = (is_windows and math.huge) or luasocket._SETSIZE or 1024 -- max fd number, limit to 1024 by default to prevent glibc buffer overflow, but not on Windows _maxselectlen = luasocket._SETSIZE or 1024 -- But this still applies on Windows _maxsslhandshake = 30 -- max handshake round-trips ----------------------------------// PRIVATE //-- wrapserver = function( listeners, socket, ip, serverport, pattern, sslctx, ssldirect ) -- this function wraps a server -- FIXME Make sure FD < _maxfd if socket:getfd() >= _maxfd then out_error("server.lua: Disallowed FD number: "..socket:getfd()) socket:close() return nil, "fd-too-large" end local connections = 0 local dispatch, disconnect = listeners.onconnect, listeners.ondisconnect local accept = socket.accept --// public methods of the object //-- local handler = { } handler.shutdown = function( ) end handler.ssl = function( ) return sslctx ~= nil end handler.sslctx = function( ) return sslctx end handler.hosts = {} -- sni handler.remove = function( ) connections = connections - 1 if handler then handler.resume( ) end end handler.close = function() socket:close( ) _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) _readlistlen = removesocket( _readlist, socket, _readlistlen ) _server[ip..":"..serverport] = nil; _socketlist[ socket ] = nil handler = nil socket = nil --mem_free( ) out_put "server.lua: closed server handler and removed sockets from list" end handler.pause = function( hard ) if not handler.paused then _readlistlen = removesocket( _readlist, socket, _readlistlen ) if hard then _socketlist[ socket ] = nil socket:close( ) socket = nil; end handler.paused = true; out_put("server.lua: server [", ip, "]:", serverport, " paused") end end handler.resume = function( ) if handler.paused then if not socket then socket = socket_bind( ip, serverport, _tcpbacklog ); socket:settimeout( 0 ) end _readlistlen = addsocket(_readlist, socket, _readlistlen) _socketlist[ socket ] = handler _fullservers[ handler ] = nil handler.paused = false; out_put("server.lua: server [", ip, "]:", serverport, " resumed") end end handler.ip = function( ) return ip end handler.serverport = function( ) return serverport end handler.socket = function( ) return socket end handler.readbuffer = function( ) if _readlistlen >= _maxselectlen or _sendlistlen >= _maxselectlen then handler.pause( ) _fullservers[ handler ] = _currenttime out_put( "server.lua: refused new client connection: server full" ) return false end local client, err = accept( socket ) -- try to accept if client then local ip, clientport = client:getpeername( ) local handler, client, err = wrapconnection( handler, listeners, client, ip, serverport, clientport, pattern, sslctx, ssldirect ) -- wrap new client socket if err then -- error while wrapping ssl socket return false end connections = connections + 1 out_put( "server.lua: accepted new client connection from ", tostring(ip), ":", tostring(clientport), " to ", tostring(serverport)) if dispatch and not ssldirect then -- SSL connections will notify onconnect when handshake completes return dispatch( handler ); end return; elseif err then -- maybe timeout or something else out_put( "server.lua: error with new client connection: ", tostring(err) ) handler.pause( ) _fullservers[ handler ] = _currenttime return false end end return handler end wrapconnection = function( server, listeners, socket, ip, serverport, clientport, pattern, sslctx, ssldirect, extra ) -- this function wraps a client to a handler object if socket:getfd() >= _maxfd then out_error("server.lua: Disallowed FD number: "..socket:getfd()) -- PROTIP: Switch to libevent socket:close( ) -- Should we send some kind of error here? if server then _fullservers[ server ] = _currenttime server.pause( ) end return nil, nil, "fd-too-large" end socket:settimeout( 0 ) --// local import of socket methods //-- local send local receive local shutdown --// private closures of the object //-- local ssl local pending local dispatch = listeners.onincoming local status = listeners.onstatus local disconnect = listeners.ondisconnect local predrain = listeners.onpredrain local drain = listeners.ondrain local onreadtimeout = listeners.onreadtimeout; local detach = listeners.ondetach local bufferqueue = { } -- buffer array local bufferqueuelen = 0 -- end of buffer array local toclose local needtls local bufferlen = 0 local noread = false local nosend = false local sendtraffic, readtraffic = 0, 0 local maxsendlen = _maxsendlen local maxreadlen = _maxreadlen --// public methods of the object //-- local handler = bufferqueue -- saves a table ^_^ handler.extra = extra if extra then handler.servername = extra.servername end handler.dispatch = function( ) return dispatch end handler.disconnect = function( ) return disconnect end handler.onreadtimeout = onreadtimeout; handler.setlistener = function( self, listeners, data ) if detach then detach(self) -- Notify listener that it is no longer responsible for this connection end dispatch = listeners.onincoming disconnect = listeners.ondisconnect status = listeners.onstatus predrain = listeners.onpredrain drain = listeners.ondrain handler.onreadtimeout = listeners.onreadtimeout detach = listeners.ondetach if listeners.onattach then listeners.onattach(self, data) end end handler._setpending = function( ) pending = true end handler.getstats = function( ) return readtraffic, sendtraffic end handler.ssl = function( ) return ssl end 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 handler.receive = function( pattern, prefix ) return receive( socket, pattern, prefix ) end handler.shutdown = function( pattern ) return shutdown( socket, pattern ) end handler.setoption = function (self, option, value) if socket.setoption then return socket:setoption(option, value); end return false, "setoption not implemented"; end handler.force_close = function ( self, err ) if bufferqueuelen ~= 0 then out_put("server.lua: discarding unwritten data for ", tostring(ip), ":", tostring(clientport)) bufferqueuelen = 0; end return self:close(err); end handler.close = function( self, err ) if not handler then return true; end _readlistlen = removesocket( _readlist, socket, _readlistlen ) _readtimes[ handler ] = nil if bufferqueuelen ~= 0 then handler:sendbuffer() -- Try now to send any outstanding data if bufferqueuelen ~= 0 then -- Still not empty, so we'll try again later if handler then handler.write = nil -- ... but no further writing allowed end toclose = true return false end end if socket then _ = shutdown and shutdown( socket ) socket:close( ) _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) _socketlist[ socket ] = nil socket = nil else out_put "server.lua: socket already closed" end if handler then _writetimes[ handler ] = nil _closelist[ handler ] = nil local _handler = handler; handler = nil if disconnect then disconnect(_handler, err or false); disconnect = nil end end if server then server.remove( ) end out_put "server.lua: closed client handler and removed socket from list" return true end handler.server = function ( ) return server end handler.ip = function( ) return ip end handler.serverport = function( ) return serverport end handler.clientport = function( ) return clientport end handler.port = handler.clientport -- COMPAT server_event local write = function( self, data ) if not handler then return false end bufferlen = bufferlen + #data if bufferlen > maxsendlen then _closelist[ handler ] = "send buffer exceeded" -- cannot close the client at the moment, have to wait to the end of the cycle return false elseif not nosend and socket and not _sendlist[ socket ] then _sendlistlen = addsocket(_sendlist, socket, _sendlistlen) end bufferqueuelen = bufferqueuelen + 1 bufferqueue[ bufferqueuelen ] = data if handler then _writetimes[ handler ] = _writetimes[ handler ] or _currenttime end return true end handler.write = write handler.bufferqueue = function( self ) return bufferqueue end handler.socket = function( self ) return socket end handler.set_mode = function( self, new ) pattern = new or pattern return pattern end handler.set_send = function ( self, newsend ) send = newsend or send return send end handler.bufferlen = function( self, readlen, sendlen ) maxsendlen = sendlen or maxsendlen maxreadlen = readlen or maxreadlen return bufferlen, maxreadlen, maxsendlen end handler.lock_read = function (self, switch) out_error( "server.lua, lock_read() is deprecated, use pause() and resume()" ) if switch == true then return self:pause() elseif switch == false then return self:resume() end return noread end handler.pause = function (self) local tmp = _readlistlen _readlistlen = removesocket( _readlist, socket, _readlistlen ) _readtimes[ handler ] = nil if _readlistlen ~= tmp then noread = true end return noread; end handler.resume = function (self) if noread then noread = false _readlistlen = addsocket(_readlist, socket, _readlistlen) _readtimes[ handler ] = _currenttime end return noread; end handler.lock = function( self, switch ) out_error( "server.lua, lock() is deprecated" ) handler.lock_read (self, switch) if switch == true then handler.pause_writes (self) elseif switch == false then handler.resume_writes (self) end return noread, nosend end handler.pause_writes = function (self) local tmp = _sendlistlen _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) _writetimes[ handler ] = nil nosend = true end handler.resume_writes = function (self) nosend = false if bufferlen > 0 and socket then _sendlistlen = addsocket(_sendlist, socket, _sendlistlen) end end local _readbuffer = function( ) -- this function reads data local buffer, err, part = receive( socket, pattern ) -- receive buffer with "pattern" if not err or (err == "wantread" or err == "timeout") then -- received something local buffer = buffer or part or "" local len = #buffer if len > maxreadlen then handler:close( "receive buffer exceeded" ) return false end local count = len * STAT_UNIT readtraffic = readtraffic + count _readtraffic = _readtraffic + count _readtimes[ handler ] = _currenttime --out_put( "server.lua: read data '", buffer:gsub("[^%w%p ]", "."), "', error: ", err ) if pending then -- connection established pending = nil if listeners.onconnect then listeners.onconnect(handler) end end return dispatch( handler, buffer, err ) else -- connections was closed or fatal error out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " read error: ", tostring(err) ) _ = handler and handler:force_close( err ) return false end end local _sendbuffer = function( ) -- this function sends data local succ, err, byte, buffer, count; if socket then if pending then pending = nil if listeners.onconnect then listeners.onconnect(handler); end end if predrain then predrain(handler); end buffer = table_concat( bufferqueue, "", 1, bufferqueuelen ) succ, err, byte = send( socket, buffer, 1, bufferlen ) count = ( succ or byte or 0 ) * STAT_UNIT sendtraffic = sendtraffic + count _sendtraffic = _sendtraffic + count for i = bufferqueuelen,1,-1 do bufferqueue[ i ] = nil end --out_put( "server.lua: sended '", buffer, "', bytes: ", tostring(succ), ", error: ", tostring(err), ", part: ", tostring(byte), ", to: ", tostring(ip), ":", tostring(clientport) ) else succ, err, count = false, "unexpected close", 0; end if succ then -- sending successful bufferqueuelen = 0 bufferlen = 0 _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) -- delete socket from writelist _writetimes[ handler ] = nil if drain then drain(handler) end _ = needtls and handler:starttls(nil) _ = toclose and handler:force_close( ) return true elseif byte and ( err == "timeout" or err == "wantwrite" ) then -- want write buffer = string_sub( buffer, byte + 1, bufferlen ) -- new buffer bufferqueue[ 1 ] = buffer -- insert new buffer in queue bufferqueuelen = 1 bufferlen = bufferlen - byte _writetimes[ handler ] = _currenttime return true else -- connection was closed during sending or fatal error out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " write error: ", tostring(err) ) _ = handler and handler:force_close( err ) return false end end -- Set the sslctx local handshake; function handler.set_sslctx(self, new_sslctx) sslctx = new_sslctx; local read, wrote handshake = coroutine_wrap( function( client ) -- create handshake coroutine local err for _ = 1, _maxsslhandshake do _sendlistlen = ( wrote and removesocket( _sendlist, client, _sendlistlen ) ) or _sendlistlen _readlistlen = ( read and removesocket( _readlist, client, _readlistlen ) ) or _readlistlen read, wrote = nil, nil _, err = client:dohandshake( ) if not err then out_put( "server.lua: ssl handshake done" ) handler.readbuffer = _readbuffer -- when handshake is done, replace the handshake function with regular functions handler.sendbuffer = _sendbuffer _ = status and status( handler, "ssl-handshake-complete" ) if self.autostart_ssl and listeners.onconnect then listeners.onconnect(self); if bufferqueuelen ~= 0 then _sendlistlen = addsocket(_sendlist, client, _sendlistlen) end end _readlistlen = addsocket(_readlist, client, _readlistlen) return true else if err == "wantwrite" then _sendlistlen = addsocket(_sendlist, client, _sendlistlen) wrote = true elseif err == "wantread" then _readlistlen = addsocket(_readlist, client, _readlistlen) read = true else break; end err = nil; coroutine_yield( ) -- handshake not finished end end err = ( err or "handshake too long" ); out_put( "server.lua: ", err ); _ = handler and handler:force_close(err) return false, err -- handshake failed end ) end if has_luasec then handler.starttls = function( self, _sslctx) if _sslctx then handler:set_sslctx(_sslctx); end if bufferqueuelen > 0 then out_put "server.lua: we need to do tls, but delaying until send buffer empty" needtls = true return end out_put( "server.lua: attempting to start tls on " .. tostring( socket ) ) local oldsocket, err = 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") ) return nil, err -- fatal error end if socket.sni then if self.servername then socket:sni(self.servername); elseif next(sslctx._sni_contexts) ~= nil then socket:sni(sslctx._sni_contexts, true); end end socket:settimeout( 0 ) -- add the new socket to our system send = socket.send receive = socket.receive shutdown = id _socketlist[ socket ] = handler _readlistlen = addsocket(_readlist, socket, _readlistlen) -- remove traces of the old socket _readlistlen = removesocket( _readlist, oldsocket, _readlistlen ) _sendlistlen = removesocket( _sendlist, oldsocket, _sendlistlen ) _socketlist[ oldsocket ] = nil handler.starttls = nil needtls = nil -- Secure now (if handshake fails connection will close) ssl = true handler.readbuffer = handshake handler.sendbuffer = handshake return handshake( socket ) -- do handshake end end handler.readbuffer = _readbuffer handler.sendbuffer = _sendbuffer send = socket.send receive = socket.receive shutdown = ( ssl and id ) or socket.shutdown _socketlist[ socket ] = handler _readlistlen = addsocket(_readlist, socket, _readlistlen) if sslctx and ssldirect and has_luasec then out_put "server.lua: auto-starting ssl negotiation..." handler.autostart_ssl = true; local ok, err = handler:starttls(sslctx); if ok == false then return nil, nil, err end end return handler, socket end id = function( ) end idfalse = function( ) return false end addsocket = function( list, socket, len ) if not list[ socket ] then len = len + 1 list[ len ] = socket list[ socket ] = len end return len; end removesocket = function( list, socket, len ) -- this function removes sockets from a list ( copied from copas ) local pos = list[ socket ] if pos then list[ socket ] = nil local last = list[ len ] list[ len ] = nil if last ~= socket then list[ last ] = pos list[ pos ] = last end return len - 1 end return len end closesocket = function( socket ) _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) _readlistlen = removesocket( _readlist, socket, _readlistlen ) _socketlist[ socket ] = nil socket:close( ) --mem_free( ) end local function link(sender, receiver, buffersize) local sender_locked; local _sendbuffer = receiver.sendbuffer; function receiver.sendbuffer() _sendbuffer(receiver); if sender_locked and receiver.bufferlen() < buffersize then sender:lock_read(false); -- Unlock now sender_locked = nil; end end local _readbuffer = sender.readbuffer; function sender.readbuffer() _readbuffer(); if not sender_locked and receiver.bufferlen() >= buffersize then sender_locked = true; sender:lock_read(true); end end sender:set_mode("*a"); end ----------------------------------// PUBLIC //-- listen = function ( addr, port, listeners, config ) addr = addr or "*" config = config or {} local err local sslctx = config.tls_ctx; local ssldirect = config.tls_direct; local pattern = config.read_size; if type( listeners ) ~= "table" then err = "invalid listener table" elseif type ( addr ) ~= "string" then err = "invalid address" elseif type( port ) ~= "number" or not ( port >= 0 and port <= 65535 ) then err = "invalid port" elseif _server[ addr..":"..port ] then err = "listeners on '[" .. addr .. "]:" .. port .. "' already exist" elseif sslctx and not has_luasec then err = "luasec not found" end if err then out_error( "server.lua, [", addr, "]:", port, ": ", err ) return nil, err end local server, err = socket_bind( addr, port, _tcpbacklog ) if err then out_error( "server.lua, [", addr, "]:", port, ": ", err ) return nil, err end local handler, err = wrapserver( listeners, server, addr, port, pattern, sslctx, ssldirect ) -- wrap new server socket if not handler then server:close( ) return nil, err end server:settimeout( 0 ) _readlistlen = addsocket(_readlist, server, _readlistlen) _server[ addr..":"..port ] = handler _socketlist[ server ] = handler out_put( "server.lua: new "..(sslctx and "ssl " or "").."server listener on '[", addr, "]:", port, "'" ) return handler end addserver = function( addr, port, listeners, pattern, sslctx ) -- this function provides a way for other scripts to reg a server return listen(addr, port, listeners, { read_size = pattern; tls_ctx = sslctx; tls_direct = sslctx and true or false; }); end getserver = function ( addr, port ) return _server[ addr..":"..port ]; end removeserver = function( addr, port ) local handler = _server[ addr..":"..port ] if not handler then return nil, "no server found on '[" .. addr .. "]:" .. tostring( port ) .. "'" end handler:close( ) _server[ addr..":"..port ] = nil return true end closeall = function( ) for _, handler in pairs( _socketlist ) do handler:close( ) _socketlist[ _ ] = nil end _readlistlen = 0 _sendlistlen = 0 _timerlistlen = 0 _server = { } _readlist = { } _sendlist = { } _timerlist = { } _socketlist = { } --mem_free( ) end getsettings = function( ) return { select_timeout = _selecttimeout; tcp_backlog = _tcpbacklog; max_send_buffer_size = _maxsendlen; max_receive_buffer_size = _maxreadlen; select_idle_check_interval = _checkinterval; send_timeout = _sendtimeout; read_timeout = _readtimeout; max_connections = _maxselectlen; max_ssl_handshake_roundtrips = _maxsslhandshake; highest_allowed_fd = _maxfd; accept_retry_interval = _accepretry; } end changesettings = function( new ) if type( new ) ~= "table" then return nil, "invalid settings table" end _selecttimeout = tonumber( new.select_timeout ) or _selecttimeout _maxsendlen = tonumber( new.max_send_buffer_size ) or _maxsendlen _maxreadlen = tonumber( new.max_receive_buffer_size ) or _maxreadlen _checkinterval = tonumber( new.select_idle_check_interval ) or _checkinterval _tcpbacklog = tonumber( new.tcp_backlog ) or _tcpbacklog _sendtimeout = tonumber( new.send_timeout ) or _sendtimeout _readtimeout = tonumber( new.read_timeout ) or _readtimeout _accepretry = tonumber( new.accept_retry_interval ) or _accepretry _maxselectlen = new.max_connections or _maxselectlen _maxsslhandshake = new.max_ssl_handshake_roundtrips or _maxsslhandshake _maxfd = new.highest_allowed_fd or _maxfd return true end addtimer = function( listener ) if type( listener ) ~= "function" then return nil, "invalid listener function" end _timerlistlen = _timerlistlen + 1 _timerlist[ _timerlistlen ] = listener return true end local add_task do local data = {}; local new_data = {}; function add_task(delay, callback) local current_time = luasocket_gettime(); delay = delay + current_time; if delay >= current_time then table_insert(new_data, {delay, callback}); else local r = callback(current_time); if r and type(r) == "number" then return add_task(r, callback); end end end addtimer(function(current_time) if #new_data > 0 then for _, d in pairs(new_data) do table_insert(data, d); end new_data = {}; end local next_time = math_huge; for i, d in pairs(data) do local t, callback = d[1], d[2]; if t <= current_time then data[i] = nil; local r = callback(current_time); if type(r) == "number" then add_task(r, callback); next_time = math_min(next_time, r); end else next_time = math_min(next_time, t - current_time); end end return next_time; end); end stats = function( ) return _readtraffic, _sendtraffic, _readlistlen, _sendlistlen, _timerlistlen end local quitting; local function setquitting(quit) quitting = quit; end loop = function(once) -- this is the main loop of the program if quitting then return "quitting"; end if once then quitting = "once"; end _currenttime = luasocket_gettime( ) repeat -- Fire timers local next_timer_time = math_huge; for i = 1, _timerlistlen do local t = _timerlist[ i ]( _currenttime ) -- fire timers if t then next_timer_time = math_min(next_timer_time, t); end end local read, write, err = socket_select( _readlist, _sendlist, math_min(_selecttimeout, next_timer_time) ) for _, socket in ipairs( read ) do -- receive data local handler = _socketlist[ socket ] if handler then handler:readbuffer( ) else closesocket( socket ) out_put "server.lua: found no handler and closed socket (readlist)" -- this can happen end end for _, socket in ipairs( write ) do -- send data waiting in writequeues local handler = _socketlist[ socket ] if handler then handler:sendbuffer( ) else closesocket( socket ) out_put "server.lua: found no handler and closed socket (writelist)" -- this should not happen end end for handler, err in pairs( _closelist ) do handler.disconnect( )( handler, err ) handler:force_close() -- forced disconnect _closelist[ handler ] = nil; end _currenttime = luasocket_gettime( ) -- Check for socket timeouts if _currenttime - _starttime > _checkinterval then _starttime = _currenttime for handler, timestamp in pairs( _writetimes ) do if _currenttime - timestamp > _sendtimeout then handler.disconnect( )( handler, "send timeout" ) handler:force_close() -- forced disconnect end end for handler, timestamp in pairs( _readtimes ) do if _currenttime - timestamp > _readtimeout then if not(handler.onreadtimeout) or handler:onreadtimeout() ~= true then handler.disconnect( )( handler, "read timeout" ) handler:close( ) -- forced disconnect? else _readtimes[ handler ] = _currenttime -- reset timer end end end end for server, paused_time in pairs( _fullservers ) do if _currenttime - paused_time > _accepretry then _fullservers[ server ] = nil; server.resume(); end end until quitting; if quitting == "once" then quitting = nil; return; end closeall(); return "quitting" end local function step() return loop(true); end local function get_backend() return "select"; end --// EXPERIMENTAL //-- local wrapclient = function( socket, ip, serverport, listeners, pattern, sslctx, extra ) local handler, socket, err = wrapconnection( nil, listeners, socket, ip, serverport, "clientport", pattern, sslctx, sslctx, extra) if not handler then return nil, err end _socketlist[ socket ] = handler if not sslctx then handler._setpending() _readlistlen = addsocket(_readlist, socket, _readlistlen) _sendlistlen = addsocket(_sendlist, socket, _sendlistlen) end return handler, socket end local addclient = function( address, port, listeners, pattern, sslctx, typ, extra ) local err if type( listeners ) ~= "table" then err = "invalid listener table" elseif type ( address ) ~= "string" then err = "invalid address" elseif type( port ) ~= "number" or not ( port >= 0 and port <= 65535 ) then err = "invalid port" elseif sslctx and not has_luasec then err = "luasec not found" end if not typ then local n = inet_pton(address); if not n then return nil, "invalid-ip"; end if #n == 16 then typ = "tcp6"; elseif #n == 4 then typ = "tcp4"; end end local create = luasocket[typ]; if type( create ) ~= "function" then err = "invalid socket type" end if err then out_error( "server.lua, addclient: ", err ) return nil, err end local client, err = create( ) if err then return nil, err end client:settimeout( 0 ) local ok, err = client:setpeername( address, port ) if ok or err == "timeout" or err == "Operation already in progress" then return wrapclient( client, address, port, listeners, pattern, sslctx, extra ) else return nil, err end end local closewatcher = function (handler) local socket = handler.conn; _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) _readlistlen = removesocket( _readlist, socket, _readlistlen ) _socketlist[ socket ] = nil end; local addremove = function (handler, read, send) local socket = handler.conn _socketlist[ socket ] = handler if read ~= nil then if read then _readlistlen = addsocket( _readlist, socket, _readlistlen ) else _sendlistlen = removesocket( _sendlist, socket, _sendlistlen ) end end if send ~= nil then if send then _sendlistlen = addsocket( _sendlist, socket, _sendlistlen ) else _readlistlen = removesocket( _readlist, socket, _readlistlen ) end end end local watchfd = function ( fd, onreadable, onwriteable ) local socket = fd if type(fd) == "number" then socket = { getfd = function () return fd; end } end local handler = { conn = socket; readbuffer = onreadable or id; sendbuffer = onwriteable or id; close = closewatcher; setflags = addremove; }; addremove( handler, onreadable, onwriteable ) return handler end ----------------------------------// BEGIN //-- use "setmetatable" ( _socketlist, { __mode = "k" } ) use "setmetatable" ( _readtimes, { __mode = "k" } ) use "setmetatable" ( _writetimes, { __mode = "k" } ) _starttime = luasocket_gettime( ) local function setlogger(new_logger) local old_logger = log; if new_logger then log = new_logger; end return old_logger; end ----------------------------------// PUBLIC INTERFACE //-- return { _addtimer = addtimer, add_task = add_task; addclient = addclient, wrapclient = wrapclient, watchfd = watchfd, loop = loop, link = link, step = step, stats = stats, closeall = closeall, addserver = addserver, listen = listen, getserver = getserver, setlogger = setlogger, getsettings = getsettings, setquitting = setquitting, removeserver = removeserver, get_backend = get_backend, changesettings = changesettings, tls_builder = function(basedir) return sslconfig._new(tls_impl.new_context, basedir) end, } prosody-13.0.1/net/PaxHeaders/stun.lua0000644000000000000000000000011714773555365014662 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.571710454 prosody-13.0.1/net/stun.lua0000644000175000017500000002166414773555365017072 0ustar00prosodyprosody00000000000000local 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 -- Following draft-uberti-behave-turn-rest-00, convert a 'secret' string -- into a username/password pair that can be used to auth to a TURN server local function get_user_pass_from_secret(secret, ttl, opt_username) ttl = ttl or 86400; local username; if opt_username then username = ("%d:%s"):format(os.time() + ttl, opt_username); else username = ("%d"):format(os.time() + ttl); end local password = base64.encode(hashes.hmac_sha1(secret, username)); return username, password, ttl; end -- Following RFC 8489 9.2, convert credentials to a HMAC key for signing local function get_long_term_auth_key(realm, username, password) return hashes.md5(username..":"..realm..":"..password); end --- Packet building/parsing local packet_methods = {}; local packet_mt = { __index = packet_methods }; local magic_cookie = string.char(0x21, 0x12, 0xA4, 0x42); local function lookup_table(t) local lookup = {}; for k, v in pairs(t) do lookup[k] = v; lookup[v] = k; end return lookup; end local methods = { binding = 0x001; -- TURN allocate = 0x003; refresh = 0x004; send = 0x006; data = 0x007; ["create-permission"] = 0x008; ["channel-bind"] = 0x009; }; local method_lookup = lookup_table(methods); local classes = { request = 0; indication = 1; success = 2; error = 3; }; local class_lookup = lookup_table(classes); local addr_families = { "IPv4", "IPv6" }; local addr_family_lookup = lookup_table(addr_families); local attributes = { ["mapped-address"] = 0x0001; ["username"] = 0x0006; ["message-integrity"] = 0x0008; ["error-code"] = 0x0009; ["unknown-attributes"] = 0x000A; ["realm"] = 0x0014; ["nonce"] = 0x0015; ["xor-mapped-address"] = 0x0020; ["software"] = 0x8022; ["alternate-server"] = 0x8023; ["fingerprint"] = 0x8028; ["message-integrity-sha256"] = 0x001C; ["password-algorithm"] = 0x001D; ["userhash"] = 0x001E; ["password-algorithms"] = 0x8002; ["alternate-domains"] = 0x8003; -- TURN ["requested-transport"] = 0x0019; ["xor-peer-address"] = 0x0012; ["data"] = 0x0013; ["xor-relayed-address"] = 0x0016; }; local attribute_lookup = lookup_table(attributes); function packet_methods:serialize_header(length) assert(#self.transaction_id == 12, "invalid transaction id length"); local header = struct.pack(">I2I2", self.type, length )..magic_cookie..self.transaction_id; return header; end function packet_methods:serialize() local payload = table.concat(self.attributes); return self:serialize_header(#payload)..payload; end function packet_methods:is_request() return bit32.band(self.type, 0x0110) == 0x0000; end function packet_methods:is_indication() return bit32.band(self.type, 0x0110) == 0x0010; end function packet_methods:is_success_resp() return bit32.band(self.type, 0x0110) == 0x0100; end function packet_methods:is_err_resp() return bit32.band(self.type, 0x0110) == 0x0110; end function packet_methods:get_method() local method = bit32.bor( bit32.rshift(bit32.band(self.type, 0x3E00), 2), bit32.rshift(bit32.band(self.type, 0x00E0), 1), bit32.band(self.type, 0x000F) ); return method, method_lookup[method]; end function packet_methods:get_class() local class = bit32.bor( bit32.rshift(bit32.band(self.type, 0x0100), 7), bit32.rshift(bit32.band(self.type, 0x0010), 4) ); return class, class_lookup[class]; end function packet_methods:set_type(method, class) if type(method) == "string" then method = assert(method_lookup[method:lower()], "unknown method: "..method); end if type(class) == "string" then class = assert(classes[class], "unknown class: "..class); end self.type = bit32.bor( bit32.lshift(bit32.band(method, 0x1F80), 2), bit32.lshift(bit32.band(method, 0x0070), 1), bit32.band(method, 0x000F), bit32.lshift(bit32.band(class, 0x0002), 7), bit32.lshift(bit32.band(class, 0x0001), 4) ); end local function _serialize_attribute(attr_type, value) local len = #value; local padding = string.rep("\0", (4 - len)%4); return struct.pack(">I2I2", attr_type, len )..value..padding; end function packet_methods:add_attribute(attr_type, value) if type(attr_type) == "string" then attr_type = assert(attributes[attr_type], "unknown attribute: "..attr_type); end table.insert(self.attributes, _serialize_attribute(attr_type, value)); end function packet_methods:deserialize(bytes) local type, len, cookie = struct.unpack(">I2I2I4", bytes); assert(#bytes == (len + 20), "incorrect packet length"); assert(cookie == 0x2112A442, "invalid magic cookie"); self.type = type; self.transaction_id = bytes:sub(9, 20); self.attributes = {}; local pos = 21; while pos < #bytes do local attr_hdr = bytes:sub(pos, pos+3); assert(#attr_hdr == 4, "packet truncated in attribute header"); local attr_type, attr_len = struct.unpack(">I2I2", attr_hdr); --luacheck: ignore 211/attr_type if attr_len == 0 then table.insert(self.attributes, attr_hdr); pos = pos + 20; else local data = bytes:sub(pos + 4, pos + 3 + attr_len); assert(#data == attr_len, "packet truncated in attribute value"); table.insert(self.attributes, attr_hdr..data); local n_padding = (4 - attr_len)%4; pos = pos + 4 + attr_len + n_padding; end end return self; end function packet_methods:get_attribute(attr_type, idx) idx = math.max(idx or 1, 1); if type(attr_type) == "string" then attr_type = assert(attribute_lookup[attr_type:lower()], "unknown attribute: "..attr_type); end for _, attribute in ipairs(self.attributes) do if struct.unpack(">I2", attribute) == attr_type then if idx == 1 then return attribute:sub(5); else idx = idx - 1; end end end end function packet_methods:_unpack_address(data, xor) local family, port = struct.unpack("x>BI2", data); local addr = data:sub(5); if xor then port = bit32.bxor(port, 0x2112); addr = sxor(addr, magic_cookie..self.transaction_id); end return { family = addr_families[family] or "unknown"; port = port; address = net.ntop(addr); }; end function packet_methods:_pack_address(family, addr, port, xor) if xor then port = bit32.bxor(port, 0x2112); addr = sxor(addr, magic_cookie..self.transaction_id); end local family_port = struct.pack("x>BI2", family, port); return family_port..addr end function packet_methods:get_mapped_address() local data = self:get_attribute("mapped-address"); if not data then return; end return self:_unpack_address(data, false); end function packet_methods:get_xor_mapped_address() local data = self:get_attribute("xor-mapped-address"); if not data then return; end return self:_unpack_address(data, true); end function packet_methods:add_xor_peer_address(address, port) local parsed_ip = assert(new_ip(address)); local family = assert(addr_family_lookup[parsed_ip.proto], "Unknown IP address family: "..parsed_ip.proto); self:add_attribute("xor-peer-address", self:_pack_address(family, parsed_ip.packed, port or 0, true)); end function packet_methods:get_xor_relayed_address(idx) local data = self:get_attribute("xor-relayed-address", idx); if not data then return; end return self:_unpack_address(data, true); end function packet_methods:get_xor_relayed_addresses() return { self:get_xor_relayed_address(1); self:get_xor_relayed_address(2); }; end function packet_methods:add_message_integrity(key) -- Add attribute with a dummy value so we can artificially increase -- the packet 'length' self:add_attribute("message-integrity", string.rep("\0", 20)); -- Get the packet data, minus the message-integrity attribute itself local pkt = self:serialize():sub(1, -25); local hash = hashes.hmac_sha1(key, pkt, false); self.attributes[#self.attributes] = nil; assert(#hash == 20, "invalid hash length"); self:add_attribute("message-integrity", hash); end do local transports = { udp = 0x11; }; function packet_methods:add_requested_transport(transport) local transport_code = transports[transport]; assert(transport_code, "unsupported transport: "..tostring(transport)); self:add_attribute("requested-transport", string.char( transport_code, 0x00, 0x00, 0x00 )); end end function packet_methods:get_error() local err_attr = self:get_attribute("error-code"); if not err_attr then return nil; end local number = err_attr:byte(4); local class = bit32.band(0x07, err_attr:byte(3)); local msg = err_attr:sub(5); return (class*100)+number, msg; end local function new_packet(method, class) local p = setmetatable({ transaction_id = random.bytes(12); length = 0; attributes = {}; }, packet_mt); p:set_type(method or "binding", class or "request"); return p; end return { new_packet = new_packet; get_user_pass_from_secret = get_user_pass_from_secret; get_long_term_auth_key = get_long_term_auth_key; }; prosody-13.0.1/net/PaxHeaders/tls_luasec.lua0000644000000000000000000000011714773555365016027 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.571710454 prosody-13.0.1/net/tls_luasec.lua0000644000175000017500000000652314773555365020234 0ustar00prosodyprosody00000000000000-- 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; }; prosody-13.0.1/net/PaxHeaders/unbound.lua0000644000000000000000000000011714773555365015343 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.575710471 prosody-13.0.1/net/unbound.lua0000644000175000017500000001425714773555365017553 0ustar00prosodyprosody00000000000000-- libunbound based net.adns replacement for Prosody IM -- Copyright (C) 2013-2015 Kim Alvefur -- -- This file is MIT licensed. -- -- luacheck: ignore prosody local setmetatable = setmetatable; local tostring = tostring; local t_concat = table.concat; local s_format = string.format; local s_lower = string.lower; local s_upper = string.upper; local noop = function() end; local logger = require "prosody.util.logger"; local log = logger.init("unbound"); local net_server = require "prosody.net.server"; local libunbound = require"lunbound"; local promise = require"prosody.util.promise"; local new_id = require "prosody.util.id".short; 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; local builtin_defaults = { hoststxt = false } local function add_defaults(conf) conf = conf or {}; for option, default in pairs(builtin_defaults) do if conf[option] == nil then conf[option] = default; end end for option, default in pairs(libunbound.config) do if conf[option] == nil then conf[option] = default; end end return conf; end local unbound_config; if prosody then 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")); end); end -- Note: libunbound will default to using root hints if resolvconf is unset local function connect_server(unbound, server) log("debug", "Setting up net.server event handling for %s", unbound); return server.watchfd(unbound, function () log("debug", "Processing queries for %s", unbound); unbound:process() end); end local unbound, server_conn; local function initialize() unbound = libunbound.new(unbound_config); server_conn = connect_server(unbound, net_server); end if prosody then prosody.events.add_handler("server-started", initialize); end local answer_mt = { __tostring = function(self) if self._string then return self._string end local h = s_format("Status: %s", errors[self.status]); if self.secure then h = h .. ", Secure"; elseif self.bogus then 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 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; return _string; end; }; local waiting_queries = {}; local function prep_answer(a) if not a then return end local status = errors[a.rcode]; local qclass = classes[a.qclass]; local qtype = types[a.qtype]; a.status, a.class, a.type = status, qclass, qtype; local t = s_lower(qtype); local rr_mt = { __index = a, __tostring = function(self) return tostring(self[t]) end }; local parser = parsers[qtype]; for i = 1, #a do if a.bogus then -- Discard bogus data a[i] = nil; else a[i] = setmetatable({[t] = parser(a[i])}, rr_mt); end end return setmetatable(a, answer_mt); end local function measure(_qclass, _qtype) return measure; end local function lookup(callback, qname, qtype, qclass) if not unbound then initialize(); end qtype = qtype and s_upper(qtype) or "A"; qclass = qclass and s_upper(qclass) or "IN"; local ntype, nclass = types[qtype], classes[qclass]; local m; local ret; local log_query = logger.init("unbound.query"..new_id()); local function callback_wrapper(a, err) m(); waiting_queries[ret] = nil; if a then prep_answer(a); log_query("debug", "Results for %s %s %s: %s (%s)", qname, qclass, qtype, a.rcode == 0 and (#a .. " items") or a.status, a.secure and "Secure" or a.bogus or "Insecure"); -- Insecure as in unsigned else log_query("error", "Results for %s %s %s: %s", qname, qclass, qtype, tostring(err)); end local ok, cerr = pcall(callback, a, err); if not ok then log_query("error", "Error in callback: %s", cerr); end end log_query("debug", "Resolve %s %s %s", qname, qclass, qtype); m = measure(qclass, qtype); local err; ret, err = unbound:resolve_async(callback_wrapper, qname, ntype, nclass); if ret then waiting_queries[ret] = callback; else log_query("error", "Resolver error: %s", err); end return ret, err; end local function lookup_sync(qname, qtype, qclass) if not unbound then initialize(); end qtype = qtype and s_upper(qtype) or "A"; qclass = qclass and s_upper(qclass) or "IN"; local ntype, nclass = types[qtype], classes[qclass]; local a, err = unbound:resolve(qname, ntype, nclass); if not a then return a, err; end return prep_answer(a); end local function cancel(id) local cb = waiting_queries[id]; unbound:cancel(id); if cb then cb(nil, "canceled"); waiting_queries[id] = nil; end return true; end -- Reinitiate libunbound context, drops cache local function purge() for id in pairs(waiting_queries) do cancel(id); end if server_conn then server_conn:close(); end initialize(); return true; end local function not_implemented() error "not implemented"; end -- Public API local _M = { lookup = lookup; cancel = cancel; new_async_socket = not_implemented; dns = { lookup = lookup_sync; cancel = cancel; cache = noop; socket_wrapper_set = noop; settimeout = noop; query = noop; purge = purge; random = noop; peek = noop; types = types; classes = classes; }; }; local function lookup_promise(_, qname, qtype, qclass) return promise.new(function (resolve, reject) local function callback(answer, err) if err then return reject(err); else return resolve(answer); end end local ret, err = lookup(callback, qname, qtype, qclass) if not ret then reject(err); end end); end local wrapper = { lookup = function (_, callback, qname, qtype, qclass) return lookup(callback, qname, qtype, qclass) end; lookup_promise = lookup_promise; _resolver = { settimeout = function () end; closeall = function () end; }; } _M.instrument = function(measure_) measure = measure_; end; function _M.resolver() return wrapper; end return _M; prosody-13.0.1/net/PaxHeaders/websocket0000644000000000000000000000013114773555365015073 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.575710471 30 ctime=1743706869.575710471 prosody-13.0.1/net/websocket/0000755000175000017500000000000014773555365017353 5ustar00prosodyprosody00000000000000prosody-13.0.1/net/websocket/PaxHeaders/frames.lua0000644000000000000000000000011714773555365017134 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.575710471 prosody-13.0.1/net/websocket/frames.lua0000644000175000017500000001000414773555365021326 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2012 Florian Zeitz -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local random_bytes = require "prosody.util.random".bytes; local bit = require "prosody.util.bitcompat"; local band = bit.band; local bor = bit.bor; local sbit = require "prosody.util.strbitop"; local sxor = sbit.sxor; local s_char = string.char; 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); end local function pack_uint64be(x) return s_pack(">I8", x); end local function read_uint16be(str, pos) if type(str) ~= "string" then str, pos = str:sub(pos, pos+1), 1; end return s_unpack(">I2", str, pos); end local function read_uint64be(str, pos) if type(str) ~= "string" then str, pos = str:sub(pos, pos+7), 1; end return s_unpack(">I8", str, pos); end local function parse_frame_header(frame) if frame:len() < 2 then return; end local byte1, byte2 = frame:byte(1, 2); local result = { FIN = band(byte1, 0x80) > 0; RSV1 = band(byte1, 0x40) > 0; RSV2 = band(byte1, 0x20) > 0; RSV3 = band(byte1, 0x10) > 0; opcode = band(byte1, 0x0F); MASK = band(byte2, 0x80) > 0; length = band(byte2, 0x7F); }; local length_bytes = 0; if result.length == 126 then length_bytes = 2; elseif result.length == 127 then length_bytes = 8; end local header_length = 2 + length_bytes + (result.MASK and 4 or 0); if frame:len() < header_length then return; end if length_bytes == 2 then result.length = read_uint16be(frame, 3); elseif length_bytes == 8 then result.length = read_uint64be(frame, 3); end if result.MASK then result.key = frame:sub(length_bytes+3, length_bytes+6); end return result, header_length; end -- XORs the string `str` with the array of bytes `key` local function apply_mask(str, key, from, to) return sxor(str:sub(from or 1, to or -1), key); end local function parse_frame_body(frame, header, pos) if header.MASK then return apply_mask(frame, header.key, pos, pos + header.length - 1); else return frame:sub(pos, pos + header.length - 1); end end local function parse_frame(frame) local result, pos = parse_frame_header(frame); if result == nil or frame:len() < (pos + result.length) then return nil, nil, result; end result.data = parse_frame_body(frame, result, pos+1); return result, pos + result.length; end local function build_frame(desc) local data = desc.data or ""; assert(desc.opcode and desc.opcode >= 0 and desc.opcode <= 0xF, "Invalid WebSocket opcode"); if desc.opcode >= 0x8 then -- RFC 6455 5.5 assert(#data <= 125, "WebSocket control frames MUST have a payload length of 125 bytes or less."); end local b1 = bor(desc.opcode, desc.FIN and 0x80 or 0, desc.RSV1 and 0x40 or 0, desc.RSV2 and 0x20 or 0, desc.RSV3 and 0x10 or 0); local b2 = #data; local length_extra; if b2 <= 125 then -- 7-bit length length_extra = ""; elseif b2 <= 0xFFFF then -- 2-byte length b2 = 126; length_extra = pack_uint16be(#data); else -- 8-byte length b2 = 127; length_extra = pack_uint64be(#data); end local key = "" if desc.MASK then key = desc.key if not key then key = random_bytes(4); end b2 = bor(b2, 0x80); data = apply_mask(data, key); end return s_char(b1, b2) .. length_extra .. key .. data end local function parse_close(data) local code, message if #data >= 2 then code = read_uint16be(data, 1); if #data > 2 then message = data:sub(3); end end return code, message end local function build_close(code, message, mask) local data = pack_uint16be(code); if message then assert(#message<=123, "Close reason must be <=123 bytes"); data = data .. message; end return build_frame({ opcode = 0x8; FIN = true; MASK = mask; data = data; }); end return { parse_header = parse_frame_header; parse_body = parse_frame_body; parse = parse_frame; build = build_frame; parse_close = parse_close; build_close = build_close; }; prosody-13.0.1/net/PaxHeaders/websocket.lua0000644000000000000000000000011714773555365015657 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.575710471 prosody-13.0.1/net/websocket.lua0000644000175000017500000002062014773555365020056 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2012 Florian Zeitz -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local t_concat = table.concat; 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. local websockets = {}; local websocket_listeners = {}; function websocket_listeners.ondisconnect(conn, err) local s = websockets[conn]; if not s then return; end websockets[conn] = nil; if s.close_timer then timer.stop(s.close_timer); s.close_timer = nil; end s.readyState = 3; if s.close_code == nil and s.onerror then s:onerror(err); end if s.onclose then s:onclose(s.close_code, s.close_message or err); end end function websocket_listeners.ondetach(conn) websockets[conn] = nil; end local function fail(s, code, reason) log("warn", "WebSocket connection failed, closing. %d %s", code, reason); s:close(code, reason); s.conn:close(); return false end function websocket_listeners.onincoming(conn, buffer, err) -- luacheck: ignore 212/err local s = websockets[conn]; s.readbuffer = s.readbuffer..buffer; while true do local frame, len = frames.parse(s.readbuffer); if frame == nil then break end s.readbuffer = s.readbuffer:sub(len+1); log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data); -- Error cases if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero return fail(s, 1002, "Reserved bits not zero"); end if frame.opcode < 0x8 then local databuffer = s.databuffer; if frame.opcode == 0x0 then -- Continuation frames if not databuffer then return fail(s, 1002, "Unexpected continuation frame"); end databuffer[#databuffer+1] = frame.data; elseif frame.opcode == 0x1 or frame.opcode == 0x2 then -- Text or Binary frame if databuffer then return fail(s, 1002, "Continuation frame expected"); end databuffer = {type=frame.opcode, frame.data}; s.databuffer = databuffer; else return fail(s, 1002, "Reserved opcode"); end if frame.FIN then s.databuffer = nil; if s.onmessage then s:onmessage(t_concat(databuffer), databuffer.type); end end else -- Control frame if frame.length > 125 then -- Control frame with too much payload return fail(s, 1002, "Payload too large"); elseif not frame.FIN then -- Fragmented control frame return fail(s, 1002, "Fragmented control frame"); end if frame.opcode == 0x8 then -- Close request if frame.length == 1 then return fail(s, 1002, "Close frame with payload, but too short for status code"); end local status_code, message = frames.parse_close(frame.data); if status_code == nil then --[[ RFC 6455 7.4.1 1005 is a reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting a status code to indicate that no status code was actually present. ]] status_code = 1005 elseif status_code < 1000 then return fail(s, 1002, "Closed with invalid status code"); elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then return fail(s, 1002, "Closed with reserved status code"); end s.close_code, s.close_message = status_code, message; s:close(1000); return true; elseif frame.opcode == 0x9 then -- Ping frame frame.opcode = 0xA; frame.MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked conn:write(frames.build(frame)); elseif frame.opcode == 0xA then -- Pong frame log("debug", "Received unexpected pong frame: %s", frame.data); else return fail(s, 1002, "Reserved opcode"); end end end return true; end local websocket_methods = {}; local function close_timeout_cb(now, timerid, s) -- luacheck: ignore 212/now 212/timerid s.close_timer = nil; log("warn", "Close timeout waiting for server to close, closing manually."); s.conn:close(); end function websocket_methods:close(code, reason) if self.readyState < 2 then code = code or 1000; log("debug", "closing WebSocket with code %i: %s" , code , reason); self.readyState = 2; local conn = self.conn; conn:write(frames.build_close(code, reason, true)); -- Do not close socket straight away, wait for acknowledgement from server. self.close_timer = timer.add_task(close_timeout, close_timeout_cb, self); elseif self.readyState == 2 then log("debug", "tried to close a closing WebSocket, closing the raw socket."); -- Stop timer if self.close_timer then timer.stop(self.close_timer); self.close_timer = nil; end local conn = self.conn; conn:close(); else log("debug", "tried to close a closed WebSocket, ignoring."); end end function websocket_methods:send(data, opcode) if self.readyState < 1 then return nil, "WebSocket not open yet, unable to send data."; elseif self.readyState >= 2 then return nil, "WebSocket closed, unable to send data."; end if opcode == "text" or opcode == nil then opcode = 0x1; elseif opcode == "binary" then opcode = 0x2; end local frame = { FIN = true; MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked opcode = opcode; data = tostring(data); }; log("debug", "WebSocket sending frame: opcode=%0x, %i bytes", frame.opcode, #frame.data); return self.conn:write(frames.build(frame)); end local websocket_metatable = { __index = websocket_methods; }; local function connect(url, ex, listeners) ex = ex or {}; --[[RFC 6455 4.1.7: The request MUST include a header field with the name |Sec-WebSocket-Key|. The value of this header field MUST be a nonce consisting of a randomly selected 16-byte value that has been base64-encoded (see Section 4 of [RFC4648]). The nonce MUST be selected randomly for each connection. ]] local key = base64.encode(random_bytes(16)); -- Either a single protocol string or an array of protocol strings. local protocol = ex.protocol; if type(protocol) == "string" then protocol = { protocol, [protocol] = true }; elseif type(protocol) == "table" and protocol[1] then for _, v in ipairs(protocol) do protocol[v] = true; end else protocol = nil; end local headers = { ["Upgrade"] = "websocket"; ["Connection"] = "Upgrade"; ["Sec-WebSocket-Key"] = key; ["Sec-WebSocket-Protocol"] = protocol and t_concat(protocol, ", "); ["Sec-WebSocket-Version"] = "13"; ["Sec-WebSocket-Extensions"] = ex.extensions; } if ex.headers then for k,v in pairs(ex.headers) do headers[k] = v; end end local s = setmetatable({ readbuffer = ""; databuffer = nil; conn = nil; close_code = nil; close_message = nil; close_timer = nil; readyState = 0; protocol = nil; url = url; onopen = listeners.onopen; onclose = listeners.onclose; onmessage = listeners.onmessage; onerror = listeners.onerror; }, websocket_metatable); local http_url = url:gsub("^(ws)", "http"); local http_req = http.request(http_url, { -- luacheck: ignore 211/http_req method = "GET"; headers = headers; sslctx = ex.sslctx; insecure = ex.insecure; }, function(b, c, r, http_req) if c ~= 101 or r.headers["connection"]:lower() ~= "upgrade" or r.headers["upgrade"] ~= "websocket" or r.headers["sec-websocket-accept"] ~= base64.encode(sha1(key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) or (protocol and not protocol[r.headers["sec-websocket-protocol"]]) then s.readyState = 3; log("warn", "WebSocket connection to %s failed: %s", url, b); if s.onerror then s:onerror("connecting-failed"); end return; end s.protocol = r.headers["sec-websocket-protocol"]; -- Take possession of socket from http local conn = http_req.conn; http_req.conn = nil; s.conn = conn; websockets[conn] = s; conn:setlistener(websocket_listeners); log("debug", "WebSocket connected successfully to %s", url); s.readyState = 1; if s.onopen then s:onopen(); end websocket_listeners.onincoming(conn, b); end); return s; end return { connect = connect; }; prosody-13.0.1/PaxHeaders/plugins0000644000000000000000000000013114773555365014000 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.575710471 30 ctime=1743706869.663710821 prosody-13.0.1/plugins/0000755000175000017500000000000014773555365016260 5ustar00prosodyprosody00000000000000prosody-13.0.1/plugins/PaxHeaders/adhoc0000644000000000000000000000013114773555365015056 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.575710471 30 ctime=1743706869.575710471 prosody-13.0.1/plugins/adhoc/0000755000175000017500000000000014773555365017336 5ustar00prosodyprosody00000000000000prosody-13.0.1/plugins/adhoc/PaxHeaders/adhoc.lib.lua0000644000000000000000000000011714773555365017465 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.575710471 prosody-13.0.1/plugins/adhoc/adhoc.lib.lua0000644000175000017500000000635714773555365021677 0ustar00prosodyprosody00000000000000-- Copyright (C) 2009-2010 Florian Zeitz -- -- This file is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st, uuid = require "prosody.util.stanza", require "prosody.util.uuid"; local xmlns_cmd = "http://jabber.org/protocol/commands"; local states = {} local _M = {}; local function _cmdtag(desc, status, sessionid, action) local cmd = st.stanza("command", { xmlns = xmlns_cmd, node = desc.node, status = status }); if sessionid then cmd.attr.sessionid = sessionid; end if action then cmd.attr.action = action; end return cmd; end function _M.new(name, node, handler, permission) if not permission then error "adhoc.new() expects a permission argument, none given" elseif permission == "user" then error "the permission mode 'user' has been renamed 'any', please update your code" end if permission == "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 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"; form = cmdtag:get_child("x", "jabber:x:data"); }; local data, state = command:handler(dataIn, states[sessionid]); states[sessionid] = state; local cmdreply; if data.status == "completed" then states[sessionid] = nil; cmdreply = command:cmdtag("completed", sessionid); elseif data.status == "canceled" then states[sessionid] = nil; cmdreply = command:cmdtag("canceled", sessionid); elseif data.status == "error" then states[sessionid] = nil; local reply = st.error_reply(stanza, data.error); origin.send(reply); return true; else cmdreply = command:cmdtag("executing", sessionid); data.actions = data.actions or { "complete" }; end for name, content in pairs(data) do if name == "info" then cmdreply:tag("note", {type="info"}):text(content):up(); elseif name == "warn" then cmdreply:tag("note", {type="warn"}):text(content):up(); elseif name == "error" then cmdreply:tag("note", {type="error"}):text(content.message):up(); elseif name == "actions" then local actions = st.stanza("actions", { execute = content.default }); for _, action in ipairs(content) do if (action == "prev") or (action == "next") or (action == "complete") then actions:tag(action):up(); else module:log("error", "Command %q at node %q provided an invalid action %q", command.name, command.node, action); end end cmdreply:add_child(actions); elseif name == "form" then cmdreply:add_child((content.layout or content):form(content.values)); elseif name == "result" then cmdreply:add_child((content.layout or content):form(content.values, "result")); elseif name == "other" then cmdreply:add_child(content); end end local reply = st.reply(stanza); reply:add_child(cmdreply); origin.send(reply); return true; end return _M; prosody-13.0.1/plugins/adhoc/PaxHeaders/mod_adhoc.lua0000644000000000000000000000011714773555365017557 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.575710471 prosody-13.0.1/plugins/adhoc/mod_adhoc.lua0000644000175000017500000000613414773555365021762 0ustar00prosodyprosody00000000000000-- Copyright (C) 2009 Thilo Cestonaro -- Copyright (C) 2009-2011 Florian Zeitz -- -- This file is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 command = commands[node]; 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(); reply:tag("feature", { var = "jabber:x:data" }):up(); event.exists = true; else origin.send(st.error_reply(stanza, "auth", "forbidden", "This item is not available to you")); return true; end elseif node == xmlns_cmd then reply:tag("identity", { name = "Ad-Hoc Commands", category = "automation", type = "command-list" }):up(); event.exists = true; end end); module:hook("host-disco-items-node", function (event) local reply, disco_node = event.reply, event.node; if disco_node ~= xmlns_cmd then return; end for node, command in it.sorted_pairs(commands) do if check_permissions(event, node, command) then reply:tag("item", { name = command.name, node = node, jid = module:get_host() }); reply:up(); end end event.exists = true; end); module:hook("iq-set/host/"..xmlns_cmd..":command", function (event) local origin, stanza = event.origin, event.stanza; local node = stanza.tags[1].attr.node local command = commands[node]; if command 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(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(command, origin, stanza); return true; end end, 500); local function adhoc_added(event) local item = event.item; -- Dang this was noisy module:log("debug", "Command added by mod_%s: %q, %q", item._provided_by or "", item.name, item.node); commands[item.node] = item; end local function adhoc_removed(event) commands[event.item.node] = nil; end module:handle_items("adhoc", adhoc_added, adhoc_removed); -- COMPAT pre module:provides() introduced in 0.9 module:handle_items("adhoc-provider", adhoc_added, adhoc_removed); prosody-13.0.1/plugins/PaxHeaders/mod_account_activity.lua0000644000000000000000000000011714773555365020773 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.575710471 prosody-13.0.1/plugins/mod_account_activity.lua0000644000175000017500000001150514773555365023174 0ustar00prosodyprosody00000000000000local jid = require "prosody.util.jid"; local time = os.time; local store = module:open_store(nil, "keyval+"); module:hook("authentication-success", function(event) local session = event.session; if session.username then store:set_key(session.username, "timestamp", time()); end end); module:hook("resource-unbind", function(event) local session = event.session; if session.username then store:set_key(session.username, "timestamp", time()); end end); local user_sessions = prosody.hosts[module.host].sessions; function get_last_active(username) --luacheck: ignore 131/get_last_active if user_sessions[username] then return os.time(), true; -- Currently connected else local last_activity = store:get(username); if not last_activity then return nil; end return last_activity.timestamp; end end module:add_item("shell-command", { section = "user"; section_desc = "View user activity data"; name = "activity"; desc = "View the last recorded user activity for an account"; args = { { name = "jid"; type = "string" } }; host_selector = "jid"; handler = function(self, userjid) --luacheck: ignore 212/self local username = jid.prepped_split(userjid); local last_timestamp, is_online = get_last_active(username); if not last_timestamp then return true, "No activity"; end return true, ("%s (%s)"):format(os.date("%Y-%m-%d %H:%M:%S", last_timestamp), (is_online and "online" or "offline")); end; }); module:add_item("shell-command", { section = "user"; section_desc = "View user activity data"; name = "list_inactive"; desc = "List inactive user accounts"; args = { { name = "host"; type = "string" }; { name = "duration"; type = "string" }; }; host_selector = "host"; handler = function(self, host, duration) --luacheck: ignore 212/self local um = require "prosody.core.usermanager"; local duration_sec = require "prosody.util.human.io".parse_duration(duration or ""); if not duration_sec then return false, ("Invalid duration %q - try something like \"30d\""):format(duration); end local now = os.time(); local n_inactive, n_unknown = 0, 0; for username in um.users(host) do local last_active = store:get_key(username, "timestamp"); if not last_active then local created_at = um.get_account_info(username, host).created; if created_at and (now - created_at) > duration_sec then self.session.print(username, ""); n_inactive = n_inactive + 1; elseif not created_at then n_unknown = n_unknown + 1; end elseif (now - last_active) > duration_sec then self.session.print(username, os.date("%Y-%m-%dT%T", last_active)); n_inactive = n_inactive + 1; end end if n_unknown > 0 then return true, ("%d accounts inactive since %s (%d unknown)"):format(n_inactive, os.date("%Y-%m-%dT%T", now - duration_sec), n_unknown); end return true, ("%d accounts inactive since %s"):format(n_inactive, os.date("%Y-%m-%dT%T", now - duration_sec)); end; }); module:add_item("shell-command", { section = "migrate"; section_desc = "Perform data migrations"; name = "account_activity_lastlog2"; desc = "Migrate account activity information from mod_lastlog2"; args = { { name = "host"; type = "string" } }; host_selector = "host"; handler = function(self, host) --luacheck: ignore 212/host local lastlog2 = module:open_store("lastlog2", "keyval+"); local n_updated, n_errors, n_skipped = 0, 0, 0; local async = require "prosody.util.async"; local p = require "prosody.util.promise".new(function (resolve) local async_runner = async.runner(function () local n = 0; for username in lastlog2:items() do n = n + 1; if n % 100 == 0 then self.session.print(("Processed %d..."):format(n)); async.sleep(0); end local lastlog2_data = lastlog2:get(username); if lastlog2_data then local current_data, err = store:get(username); if not current_data then if not err then current_data = {}; else n_errors = n_errors + 1; end end if current_data then local imported_timestamp = current_data.timestamp; local latest; for k, v in pairs(lastlog2_data) do if k ~= "registered" and (not latest or v.timestamp > latest) then latest = v.timestamp; end end if latest and (not imported_timestamp or imported_timestamp < latest) then local ok, err = store:set_key(username, "timestamp", latest); if ok then n_updated = n_updated + 1; else self.session.print(("WW: Failed to import %q: %s"):format(username, err)); n_errors = n_errors + 1; end else n_skipped = n_skipped + 1; end end end end return resolve(("%d accounts imported, %d errors, %d skipped"):format(n_updated, n_errors, n_skipped)); end); async_runner:run(true); end); return p; end; }); prosody-13.0.1/plugins/PaxHeaders/mod_admin_adhoc.lua0000644000000000000000000000011714773555365017651 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.579710487 prosody-13.0.1/plugins/mod_admin_adhoc.lua0000644000175000017500000011010014773555365022041 0ustar00prosodyprosody00000000000000-- Copyright (C) 2009-2011 Florian Zeitz -- -- This file is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: ignore 212/self 212/data 212/state 412/err 422/err local _G = _G; local prosody = _G.prosody; local hosts = prosody.hosts; local t_concat = table.concat; local t_sort = table.sort; local module_host = module:get_host(); 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 "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; local function generate_error_message(errors) local errmsg = {}; for name, err in pairs(errors) do errmsg[#errmsg + 1] = name .. ": " .. err; end return { status = "completed", error = { message = t_concat(errmsg, "\n") } }; end -- Adding a new user local add_user_layout = dataforms_new{ title = "Adding a User"; instructions = "Fill out this form to add a user."; { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for the account to be added" }; { name = "password", type = "text-private", label = "The password for this account" }; { name = "password-verify", type = "text-private", label = "Retype password" }; }; local add_user_command_handler = adhoc_simple(add_user_layout, function(fields, err, data) if err then return generate_error_message(err); end local username, host = jid.split(fields.accountjid); if module_host ~= host then return { status = "completed", error = { message = "Trying to add a user on " .. host .. " but command was sent to " .. module_host}}; end if (fields["password"] == fields["password-verify"]) and username and host then if usermanager_user_exists(username, host) then return { status = "completed", error = { message = "Account already exists" } }; else if usermanager_create_user(username, fields.password, host) then module:log("info", "Created new account %s@%s by %s", username, host, jid.bare(data.from)); return { status = "completed", info = "Account successfully created" }; else return { status = "completed", error = { message = "Failed to write data to disk" } }; end end else module:log("debug", "Invalid data, password mismatch or empty username while creating account for %s", fields.accountjid or ""); return { status = "completed", error = { message = "Invalid data.\nPassword mismatch, or empty username" } }; end end); -- Changing a user's password local change_user_password_layout = dataforms_new{ title = "Changing a User Password"; instructions = "Fill out this form to change a user's password."; { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for this account" }; { name = "password", type = "text-private", required = true, label = "The password for this account" }; }; local change_user_password_command_handler = adhoc_simple(change_user_password_layout, function(fields, err, data) if err then return generate_error_message(err); end local username, host = jid.split(fields.accountjid); if module_host ~= host then return { status = "completed", error = { message = "Trying to change the password of a user on " .. host .. " but command was sent to " .. module_host } }; end if usermanager_user_exists(username, host) and usermanager_set_password(username, fields.password, host, nil) then module:log("info", "Password of account %s@%s changed by %s", username, host, jid.bare(data.from)); return { status = "completed", info = "Password successfully changed" }; else return { status = "completed", error = { message = "User does not exist" } }; end end); -- Reloading the config local function config_reload_handler(self, data, state) module:log("info", "%s reloads the config", jid.bare(data.from)); local ok, err = prosody.reload_config(); if ok then return { status = "completed", info = "Configuration reloaded (modules may need to be reloaded for this to have an effect)" }; else return { status = "completed", error = { message = "Failed to reload config: " .. tostring(err) } }; end end -- Deleting a user's account local delete_user_layout = dataforms_new{ title = "Deleting a User"; instructions = "Fill out this form to delete 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 delete" }; }; local delete_user_command_handler = adhoc_simple(delete_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_delete_user(username, host) then module:log("info", "User %s has been deleted by %s", aJID, jid.bare(data.from)); succeeded[#succeeded+1] = aJID; else module:log("debug", "Tried to delete non-existent user %s", aJID); failed[#failed+1] = aJID; end end return {status = "completed", info = (#succeeded ~= 0 and "The following accounts were successfully deleted:\n"..t_concat(succeeded, "\n").."\n" or "").. (#failed ~= 0 and "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); local host = hosts[hostname]; local sessions = host.sessions[node] and host.sessions[node].sessions; for resource, session in pairs(sessions or {}) do if not givenResource or (resource == givenResource) then module:log("debug", "Disconnecting %s@%s/%s", node, hostname, resource); session:close(); end end return true; end local end_user_session_layout = dataforms_new{ title = "Ending a User Session"; instructions = "Fill out this form to end a user's session."; { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) for which to end sessions", required = true }; }; local end_user_session_handler = adhoc_simple(end_user_session_layout, function(fields, err) 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 disconnect_user(aJID) then succeeded[#succeeded+1] = aJID; else failed[#failed+1] = aJID; end end return {status = "completed", info = (#succeeded ~= 0 and "The following accounts were successfully disconnected:\n"..t_concat(succeeded, "\n").."\n" or "").. (#failed ~= 0 and "The following accounts could not be disconnected:\n"..t_concat(failed, "\n") or "") }; end); -- Getting a user's roster local get_user_roster_layout = dataforms_new{ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for which to retrieve the roster" }; }; local get_user_roster_result_layout = dataforms_new{ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "accountjid", type = "jid-single", label = "This is the roster for" }; { name = "roster", type = "text-multi", label = "Roster XML" }; }; local get_user_roster_handler = adhoc_simple(get_user_roster_layout, function(fields, err) if err then return generate_error_message(err); end local user, host = jid.split(fields.accountjid); if host ~= module_host then return { status = "completed", error = { message = "Tried to get roster for a user on " .. host .. " but command was sent to " .. module_host } }; elseif not usermanager_user_exists(user, host) then return { status = "completed", error = { message = "User does not exist" } }; end local roster = rm_load_roster(user, host); local query = st.stanza("query", { xmlns = "jabber:iq:roster" }); for contact_jid in pairs(roster) do if contact_jid then query:tag("item", { jid = contact_jid, subscription = roster[contact_jid].subscription, ask = roster[contact_jid].ask, name = roster[contact_jid].name, }); for group in pairs(roster[contact_jid].groups) do query:tag("group"):text(group):up(); end query:up(); end end local query_text = tostring(query):gsub("><", ">\n<"); local result = get_user_roster_result_layout:form({ accountjid = user.."@"..host, roster = query_text }, "result"); result:add_child(query); return { status = "completed", other = result }; end); -- Getting user statistics local get_user_stats_layout = dataforms_new{ title = "Get User Statistics"; instructions = "Fill out this form to gather user statistics."; { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for statistics" }; }; local get_user_stats_result_layout = dataforms_new{ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "ipaddresses", type = "text-multi", label = "IP Addresses" }; { name = "rostersize", type = "text-single", label = "Roster size", datatype = "xs:integer" }; { name = "onlineresources", type = "text-multi", label = "Online Resources" }; }; local get_user_stats_handler = adhoc_simple(get_user_stats_layout, function(fields, err) if err then return generate_error_message(err); end local user, host = jid.split(fields.accountjid); if host ~= module_host then return { status = "completed", error = { message = "Tried to get stats for a user on " .. host .. " but command was sent to " .. module_host } }; elseif not usermanager_user_exists(user, host) then return { status = "completed", error = { message = "User does not exist" } }; end local roster = rm_load_roster(user, host); local rostersize = 0; local IPs = ""; local resources = ""; for contact_jid in pairs(roster) do if contact_jid then rostersize = rostersize + 1; end end for resource, session in pairs((hosts[host].sessions[user] and hosts[host].sessions[user].sessions) or {}) do resources = resources .. "\n" .. resource; IPs = IPs .. "\n" .. session.ip; end return { status = "completed", result = {layout = get_user_stats_result_layout, values = {ipaddresses = IPs, rostersize = rostersize, onlineresources = resources}} }; end); -- Getting a list of online users local get_online_users_layout = dataforms_new{ title = "Getting List of Online Users"; instructions = "How many users should be returned at most?"; { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "max_items", type = "list-single", label = "Maximum number of users", options = { "25", "50", "75", "100", "150", "200", "all" } }; { name = "details", type = "boolean", label = "Show details" }; }; local get_online_users_result_layout = dataforms_new{ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "onlineuserjids", type = "text-multi", label = "The list of all online users" }; }; local get_online_users_command_handler = adhoc_simple(get_online_users_layout, function(fields, err) if err then return generate_error_message(err); end local max_items = nil if fields.max_items ~= "all" then max_items = tonumber(fields.max_items); end local count = 0; local users = {}; for username, user in pairs(hosts[module_host].sessions or {}) do if (max_items ~= nil) and (count >= max_items) then break; end users[#users+1] = username.."@"..module_host; count = count + 1; if fields.details then for resource, session in pairs(user.sessions or {}) do local status, priority, ip = "unavailable", tostring(session.priority or "-"), session.ip or ""; if session.presence then status = session.presence:child_with_name("show"); if status then status = status:get_text() or "[invalid!]"; else status = "available"; end end users[#users+1] = " - "..resource..": "..status.."("..priority.."), IP: ["..ip.."]"; end end end return { status = "completed", result = {layout = get_online_users_result_layout, values = {onlineuserjids=t_concat(users, "\n")}} }; end); -- Getting a list of S2S connections (this host) local list_s2s_this_result = dataforms_new { title = "List of S2S connections on this host"; { name = "FORM_TYPE"; type = "hidden"; value = "http://prosody.im/protocol/s2s#list" }; { name = "sessions"; type = "text-multi"; label = "Connections:" }; { name = "num_in"; type = "text-single"; label = "#incoming connections:"; datatype = "xs:integer" }; { name = "num_out"; type = "text-single"; label = "#outgoing connections:"; datatype = "xs:integer" }; }; local function session_flags(session, line) line = line or {}; if session.id then line[#line+1] = "["..session.id.."]" else line[#line+1] = "["..session.type..(tostring(session):match("%x*$")).."]" end local flags = {}; if session.cert_identity_status == "valid" then flags[#flags+1] = "authenticated"; end if session.dialback_key then flags[#flags+1] = "dialback"; end if session.external_auth then flags[#flags+1] = "SASL"; end if session.secure then flags[#flags+1] = "encrypted"; end if session.compressed then flags[#flags+1] = "compressed"; end if session.smacks then flags[#flags+1] = "sm"; end if session.ip and session.ip:match(":") then flags[#flags+1] = "IPv6"; end if session.incoming and session.outgoing then flags[#flags+1] = "bidi"; elseif session.is_bidi or session.bidi_session then flags[#flags+1] = "bidi"; end line[#line+1] = "("..t_concat(flags, ", ")..")"; return t_concat(line, " "); end local function list_s2s_this_handler(self, data, state) local count_in, count_out = 0, 0; local s2s_list = {}; local s2s_sessions = module:shared"/*/s2s/sessions"; for _, session in pairs(s2s_sessions) do local remotehost, localhost, direction; if session.direction == "outgoing" then direction = "->"; count_out = count_out + 1; remotehost, localhost = session.to_host or "?", session.from_host or "?"; else direction = "<-"; count_in = count_in + 1; remotehost, localhost = session.from_host or "?", session.to_host or "?"; end local sess_lines = { r = remotehost, session_flags(session, { "", direction, remotehost or "?" })}; if localhost == module_host then s2s_list[#s2s_list+1] = sess_lines; end end t_sort(s2s_list, function(a, b) return a.r < b.r; end); for i, sess_lines in ipairs(s2s_list) do s2s_list[i] = sess_lines[1]; end return { status = "completed", result = { layout = list_s2s_this_result; values = { sessions = t_concat(s2s_list, "\n"), num_in = count_in, num_out = count_out } } }; end -- Getting a list of loaded modules local list_modules_result = dataforms_new { title = "List of loaded modules"; { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#list" }; { name = "modules", type = "text-multi", label = "The following modules are loaded:" }; }; local function list_modules_handler(self, data, state) local modules = array.collect(keys(hosts[module_host].modules)):sort():concat("\n"); return { status = "completed", result = { layout = list_modules_result; values = { modules = modules } } }; end -- Loading a module local load_module_layout = dataforms_new { title = "Load module"; instructions = "Specify the module to be loaded"; { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#load" }; { name = "module", type = "text-single", required = true, label = "Module to be loaded:"}; }; local load_module_handler = adhoc_simple(load_module_layout, function(fields, err) if err then return generate_error_message(err); end if modulemanager.is_loaded(module_host, fields.module) then return { status = "completed", info = "Module already loaded" }; end local ok, err = modulemanager.load(module_host, fields.module); if ok then return { status = "completed", info = 'Module "'..fields.module..'" successfully loaded on host "'..module_host..'".' }; else return { status = "completed", error = { message = 'Failed to load module "'..fields.module..'" on host "'..module_host.. '". Error was: "'..tostring(err or "")..'"' } }; end end); -- Globally loading a module local globally_load_module_layout = dataforms_new { title = "Globally load module"; instructions = "Specify the module to be loaded on all hosts"; { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#global-load" }; { name = "module", type = "text-single", required = true, label = "Module to globally load:"}; }; local globally_load_module_handler = adhoc_simple(globally_load_module_layout, function(fields, err, data) local ok_list, err_list = {}, {}; if err then return generate_error_message(err); end local ok, err = modulemanager.load(module_host, fields.module); if ok then ok_list[#ok_list + 1] = module_host; else err_list[#err_list + 1] = module_host .. " (Error: " .. tostring(err) .. ")"; end -- Is this a global module? if modulemanager.is_loaded("*", fields.module) and not modulemanager.is_loaded(module_host, fields.module) then module:log("info", "mod_%s loaded by %s", fields.module, jid.bare(data.from)); return { status = "completed", info = 'Global module '..fields.module..' loaded.' }; end -- This is either a shared or "normal" module, load it on all other hosts for host_name, host in pairs(hosts) do if host_name ~= module_host and host.type == "local" then local ok, err = modulemanager.load(host_name, fields.module); if ok then ok_list[#ok_list + 1] = host_name; else err_list[#err_list + 1] = host_name .. " (Error: " .. tostring(err) .. ")"; end end end module:log("info", "mod_%s loaded by %s", fields.module, jid.bare(data.from)); local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully loaded onto the hosts:\n"..t_concat(ok_list, "\n")) or "") .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. (#err_list > 0 and ("Failed to load the module "..fields.module.." onto the hosts:\n"..t_concat(err_list, "\n")) or ""); return { status = "completed", info = info }; end); -- Reloading modules local reload_modules_layout = dataforms_new { title = "Reload modules"; instructions = "Select the modules to be reloaded"; { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#reload" }; { name = "modules", type = "list-multi", required = true, label = "Modules to be reloaded:"}; }; local reload_modules_handler = adhoc_initial(reload_modules_layout, function() return { modules = array.collect(keys(hosts[module_host].modules)):sort() }; end, function(fields, err, data) if err then return generate_error_message(err); end local ok_list, err_list = {}, {}; for _, module_ in ipairs(fields.modules) do local ok, err = modulemanager.reload(module_host, module_); if ok then ok_list[#ok_list + 1] = module_; else err_list[#err_list + 1] = module_ .. "(Error: " .. tostring(err) .. ")"; end module:log("info", "mod_%s reloaded by %s", module_, jid.bare(data.from)); end local info = (#ok_list > 0 and ("The following modules were successfully reloaded on host "..module_host..":\n"..t_concat(ok_list, "\n")) or "") .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. (#err_list > 0 and ("Failed to reload the following modules on host "..module_host..":\n"..t_concat(err_list, "\n")) or ""); return { status = "completed", info = info }; end); -- Globally reloading a module local globally_reload_module_layout = dataforms_new { title = "Globally reload module"; instructions = "Specify the module to reload on all hosts"; { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#global-reload" }; { name = "module", type = "list-single", required = true, label = "Module to globally reload:"}; }; local globally_reload_module_handler = adhoc_initial(globally_reload_module_layout, function() local loaded_modules = array(keys(modulemanager.get_modules("*"))); for _, host in pairs(hosts) do loaded_modules:append(array(keys(host.modules))); end loaded_modules = array(set.new(loaded_modules):items()):sort(); return { module = loaded_modules }; end, function(fields, err, data) local is_global = false; if err then return generate_error_message(err); end if modulemanager.is_loaded("*", fields.module) then local ok, err = modulemanager.reload("*", fields.module); if not ok then return { status = "completed", info = 'Global module '..fields.module..' failed to reload: '..err }; end is_global = true; end local ok_list, err_list = {}, {}; for host_name in pairs(hosts) do if modulemanager.is_loaded(host_name, fields.module) then local ok, err = modulemanager.reload(host_name, fields.module); if ok then ok_list[#ok_list + 1] = host_name; else err_list[#err_list + 1] = host_name .. " (Error: " .. tostring(err) .. ")"; end end end if #ok_list == 0 and #err_list == 0 then if is_global then return { status = "completed", info = 'Successfully reloaded global module '..fields.module }; else return { status = "completed", info = 'Module '..fields.module..' not loaded on any host.' }; end end module:log("info", "mod_%s reloaded by %s", fields.module, jid.bare(data.from)); local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully reloaded on the hosts:\n"..t_concat(ok_list, "\n")) or "") .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. (#err_list > 0 and ("Failed to reload the module "..fields.module.." on the hosts:\n"..t_concat(err_list, "\n")) or ""); return { status = "completed", info = info }; end); local function send_to_online(message, server) local sessions; if server then sessions = { [server] = hosts[server] }; else sessions = hosts; end local c = 0; for domain, session in pairs(sessions) do for user in pairs(session.sessions or {}) do c = c + 1; message.attr.from = domain; message.attr.to = user.."@"..domain; core_post_stanza(session, message); end end return c; end -- Shutting down the service local shut_down_service_layout = dataforms_new{ title = "Shutting Down the Service"; instructions = "Fill out this form to shut down the service."; { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "delay", type = "list-single", label = "Time delay before shutting down", value = "5", options = { {label = "5 seconds", value = "5"}, {label = "30 seconds", value = "30"}, {label = "60 seconds", value = "60"}, {label = "90 seconds", value = "90"}, {label = "2 minutes", value = "120"}, {label = "3 minutes", value = "180"}, {label = "4 minutes", value = "240"}, {label = "5 minutes", value = "300"}, }; }; { name = "announcement", type = "text-multi", label = "Announcement" }; }; local shut_down_service_handler = adhoc_simple(shut_down_service_layout, function(fields, err, data) if err then return generate_error_message(err); end module:log("info", "Server being shut down by %s", jid.bare(data.from)); if fields.announcement and #fields.announcement > 0 then local message = st.message({type = "headline"}, fields.announcement):up() :tag("subject"):text("Server is shutting down"); send_to_online(message); end timer_add_task(tonumber(fields.delay or "5"), function() prosody.shutdown("Shutdown by adhoc command") end); return { status = "completed", info = "Server is about to shut down" }; end); -- Unloading modules local unload_modules_layout = dataforms_new { title = "Unload modules"; instructions = "Select the modules to be unloaded"; { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#unload" }; { name = "modules", type = "list-multi", required = true, label = "Modules to be unloaded:"}; }; local unload_modules_handler = adhoc_initial(unload_modules_layout, function() return { modules = array.collect(keys(hosts[module_host].modules)):sort() }; end, function(fields, err, data) if err then return generate_error_message(err); end local ok_list, err_list = {}, {}; for _, module_ in ipairs(fields.modules) do local ok, err = modulemanager.unload(module_host, module_); if ok then ok_list[#ok_list + 1] = module_; else err_list[#err_list + 1] = module_ .. "(Error: " .. tostring(err) .. ")"; end module:log("info", "mod_%s unloaded by %s", module_, jid.bare(data.from)); end local info = (#ok_list > 0 and ("The following modules were successfully unloaded on host "..module_host..":\n"..t_concat(ok_list, "\n")) or "") .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. (#err_list > 0 and ("Failed to unload the following modules on host "..module_host..":\n"..t_concat(err_list, "\n")) or ""); return { status = "completed", info = info }; end); -- Globally unloading a module local globally_unload_module_layout = dataforms_new { title = "Globally unload module"; instructions = "Specify a module to unload on all hosts"; { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#global-unload" }; { name = "module", type = "list-single", required = true, label = "Module to globally unload:"}; }; local globally_unload_module_handler = adhoc_initial(globally_unload_module_layout, function() local loaded_modules = array(keys(modulemanager.get_modules("*"))); for _, host in pairs(hosts) do loaded_modules:append(array(keys(host.modules))); end loaded_modules = array(set.new(loaded_modules):items()):sort(); return { module = loaded_modules }; end, function(fields, err, data) local is_global = false; if err then return generate_error_message(err); end if modulemanager.is_loaded("*", fields.module) then local ok, err = modulemanager.unload("*", fields.module); if not ok then return { status = "completed", info = 'Global module '..fields.module..' failed to unload: '..err }; end is_global = true; end local ok_list, err_list = {}, {}; for host_name in pairs(hosts) do if modulemanager.is_loaded(host_name, fields.module) then local ok, err = modulemanager.unload(host_name, fields.module); if ok then ok_list[#ok_list + 1] = host_name; else err_list[#err_list + 1] = host_name .. " (Error: " .. tostring(err) .. ")"; end end end if #ok_list == 0 and #err_list == 0 then if is_global then return { status = "completed", info = 'Successfully unloaded global module '..fields.module }; else return { status = "completed", info = 'Module '..fields.module..' not loaded on any host.' }; end end module:log("info", "mod_%s globally unloaded by %s", fields.module, jid.bare(data.from)); local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully unloaded on the hosts:\n"..t_concat(ok_list, "\n")) or "") .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. (#err_list > 0 and ("Failed to unload the module "..fields.module.." on the hosts:\n"..t_concat(err_list, "\n")) or ""); return { status = "completed", info = info }; end); -- Activating a host local activate_host_layout = dataforms_new { title = "Activate host"; instructions = ""; { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/hosts#activate" }; { name = "host", type = "text-single", required = true, label = "Host:"}; }; local activate_host_handler = adhoc_simple(activate_host_layout, function(fields, err, data) if err then return generate_error_message(err); end local ok, err = hostmanager_activate(fields.host); if ok then module:log("info", "Host '%s' activated by %s", fields.host, jid.bare(data.from)); return { status = "completed", info = fields.host .. " activated" }; else return { status = "canceled", error = err } end end); -- Deactivating a host local deactivate_host_layout = dataforms_new { title = "Deactivate host"; instructions = ""; { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/hosts#activate" }; { name = "host", type = "text-single", required = true, label = "Host:"}; }; local deactivate_host_handler = adhoc_simple(deactivate_host_layout, function(fields, err, data) if err then return generate_error_message(err); end local ok, err = hostmanager_deactivate(fields.host); if ok then module:log("info", "Host '%s' deactivated by %s", fields.host, jid.bare(data.from)); return { status = "completed", info = fields.host .. " deactivated" }; else return { status = "canceled", error = err } end end); -- luacheck: max_line_length 180 local add_user_desc = adhoc_new("Add User", "http://jabber.org/protocol/admin#add-user", add_user_command_handler, "admin"); 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"); local get_online_users_desc = adhoc_new("Get List of Online Users", "http://jabber.org/protocol/admin#get-online-users-list", get_online_users_command_handler, "admin"); local list_s2s_this_desc = adhoc_new("List S2S connections", "http://prosody.im/protocol/s2s#list", list_s2s_this_handler, "admin"); local list_modules_desc = adhoc_new("List loaded modules", "http://prosody.im/protocol/modules#list", list_modules_handler, "admin"); local load_module_desc = adhoc_new("Load module", "http://prosody.im/protocol/modules#load", load_module_handler, "admin"); local globally_load_module_desc = adhoc_new("Globally load module", "http://prosody.im/protocol/modules#global-load", globally_load_module_handler, "global_admin"); local reload_modules_desc = adhoc_new("Reload modules", "http://prosody.im/protocol/modules#reload", reload_modules_handler, "admin"); local globally_reload_module_desc = adhoc_new("Globally reload module", "http://prosody.im/protocol/modules#global-reload", globally_reload_module_handler, "global_admin"); local shut_down_service_desc = adhoc_new("Shut Down Service", "http://jabber.org/protocol/admin#shutdown", shut_down_service_handler, "global_admin"); local unload_modules_desc = adhoc_new("Unload modules", "http://prosody.im/protocol/modules#unload", unload_modules_handler, "admin"); local globally_unload_module_desc = adhoc_new("Globally unload module", "http://prosody.im/protocol/modules#global-unload", globally_unload_module_handler, "global_admin"); local activate_host_desc = adhoc_new("Activate host", "http://prosody.im/protocol/hosts#activate", activate_host_handler, "global_admin"); local deactivate_host_desc = adhoc_new("Deactivate host", "http://prosody.im/protocol/hosts#deactivate", deactivate_host_handler, "global_admin"); 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); module:provides("adhoc", get_online_users_desc); module:provides("adhoc", list_s2s_this_desc); module:provides("adhoc", list_modules_desc); module:provides("adhoc", load_module_desc); module:provides("adhoc", globally_load_module_desc); module:provides("adhoc", reload_modules_desc); module:provides("adhoc", globally_reload_module_desc); module:provides("adhoc", shut_down_service_desc); module:provides("adhoc", unload_modules_desc); module:provides("adhoc", globally_unload_module_desc); module:provides("adhoc", activate_host_desc); module:provides("adhoc", deactivate_host_desc); prosody-13.0.1/plugins/PaxHeaders/mod_admin_shell.lua0000644000000000000000000000011714773555365017702 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.583710502 prosody-13.0.1/plugins/mod_admin_shell.lua0000644000175000017500000026614014773555365022112 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: ignore 212/self module:set_global(); module:depends("admin_socket"); 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 parse_args = require "prosody.util.argparse".parse; local _G = _G; local prosody = _G.prosody; local unpack = table.unpack; local cache = require "prosody.util.cache"; local new_short_id = require "prosody.util.id".short; local iterators = require "prosody.util.iterators"; local keys, values = iterators.keys, iterators.values; 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 "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 "prosody.util.human.units".format; local format_table = require "prosody.util.human.io".table; local function capitalize(s) if not s then return end return (s:gsub("^%a", string.upper):gsub("_", " ")); end local function pre(prefix, str, alt) if type(str) ~= "string" or str == "" then return alt or ""; end return prefix .. str; end local function suf(str, suffix, alt) if type(str) ~= "string" or str == "" then return alt or ""; end return str .. suffix; end 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. If you're ever wondering how to get out, simply type 'quit' (ctrl+d should also work). 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) local f, err = envloadfile(name, env); if not f then return f, err; end return f(); end; return env; end console = {}; local runner_callbacks = {}; function runner_callbacks:error(err) module:log("error", "Traceback[shell]: %s", err); self.data.print("Fatal error while running command, it did not complete"); self.data.print("Error: "..tostring(err)); end local function send_repl_output(session, line, attr) return session.send(st.stanza("repl-output", attr):text(tostring(line))); end local function request_repl_input(session, input_type) if input_type ~= "password" then return promise.reject("internal error - unsupported input type "..tostring(input_type)); end local pending_inputs = session.pending_inputs; if not pending_inputs then pending_inputs = cache.new(5, function (input_id, input_promise) --luacheck: ignore 212/input_id input_promise.reject(); end); session.pending_inputs = pending_inputs; end local input_id = new_short_id(); local p = promise.new(function (resolve, reject) pending_inputs:set(input_id, { resolve = resolve, reject = reject }); end):finally(function () pending_inputs:set(input_id, nil); end); session.send(st.stanza("repl-request-input", { type = input_type, id = input_id })); return p; end module:hook("admin-disconnected", function (event) local pending_inputs = event.session.pending_inputs; if not pending_inputs then return; end for input_promise in pending_inputs:values() do input_promise.reject(); end end); module:hook("admin/repl-requested-input", function (event) local input_id = event.stanza.attr.id; local input_promise = event.origin.pending_inputs:get(input_id); if not input_promise then event.origin.send(st.stanza("repl-result", { type = "error" }):text("Internal error - unexpected input")); return true; end local status = event.stanza.attr.status or "submit"; local text = event.stanza:get_text(); if status == "submit" then input_promise.resolve(text); else input_promise.reject(status == "cancel" and (text ~= "" and text) or "cancelled"); end return true; end); function console:new_session(admin_session) local session = { send = function (t) return send_repl_output(admin_session, t); end; print = function (...) local t = {}; for i=1,select("#", ...) do t[i] = tostring(select(i, ...)); 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; request_input = function (input_type) return request_repl_input(admin_session, input_type); 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); session.thread = async.runner(function (line) console:process_line(session, line); end, runner_callbacks, session); -- Load up environment with helper objects for name, t in pairs(def_env) do if type(t) == "table" then session.env[name] = setmetatable({ session = session }, { __index = t }); end end session.env.output:configure(); return session; end local function process_cmd_line(session, arg_line) local chunk = load("return "..arg_line, "=shell", "t", {}); local ok, args = pcall(chunk); if not ok then return nil, args; end local section_name, command = args[1], args[2]; local section_mt = getmetatable(def_env[section_name]); local section_help = section_mt and section_mt.help; local command_help = section_help and section_help.commands[command]; if not command_help then if commands[section_name] then commands[section_name](session, table.concat(args, " ")); return; end if section_help then return nil, "Command not found or necessary module not loaded. Try 'help "..section_name.." for a list of available commands."; end return nil, "Command not found. Is the necessary module loaded?"; end local fmt = { "%s"; ":%s("; ")" }; if command_help.flags then local flags, flags_err, flags_err_extra = parse_args(args, command_help.flags); if not flags then if flags_err == "missing-value" then return nil, "Expected value after "..flags_err_extra; elseif flags_err == "param-not-found" then return nil, "Unknown parameter: "..flags_err_extra; end return nil, flags_err; end table.remove(flags, 2); table.remove(flags, 1); local n_fixed_args = #command_help.args; local arg_str = {}; for i = 1, n_fixed_args do if flags[i] ~= nil then table.insert(arg_str, ("%q"):format(flags[i])); else table.insert(arg_str, "nil"); end end table.insert(arg_str, "flags"); for i = n_fixed_args + 1, #flags do if flags[i] ~= nil then table.insert(arg_str, ("%q"):format(flags[i])); else table.insert(arg_str, "nil"); end end table.insert(fmt, 3, "%s"); return "local flags = ...; return "..string.format(table.concat(fmt), section_name, command, table.concat(arg_str, ", ")), flags; end for i = 3, #args do if args[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 return "return "..string.format(table.concat(fmt), table.unpack(args)); end local function handle_line(event) local session = event.origin.shell_session; if not session then 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; session.repl = event.stanza.attr.repl ~= "0"; local result = st.stanza("repl-result"); if line:match("^>") then line = line:gsub("^>", ""); useglobalenv = true; else 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); return; end end session.env._ = line; if not useglobalenv and commands[line:lower()] then commands[line:lower()](session, line); event.origin.send(result); return; end if useglobalenv and not session.globalenv then session.globalenv = redirect_output(_G, session); end local function send_result(taskok, message) if not message then if type(taskok) ~= "string" and useglobalenv then taskok = session.serialize(taskok); end result:text("Result: "..tostring(taskok)); elseif (not taskok) and message then result.attr.type = "error"; result:text("Error: "..tostring(message)); else result:text("OK: "..tostring(message)); end event.origin.send(result); end local taskok, message; local env = (useglobalenv and session.globalenv) or session.env or nil; local flags; local source; if line:match("^{") then -- Input is a serialized array of strings, typically from -- a command-line invocation of 'prosodyctl shell something' source, flags = process_cmd_line(session, line); if not source then if flags then -- err send_result(false, flags); else -- no err, but nothing more to do -- This happens if it was a "simple" command event.origin.send(result); end return; end end local chunkname = "=console"; -- luacheck: ignore 311/err local chunk, err = envload(source or ("return "..line), chunkname, env); if not chunk then if not source then chunk, err = envload(line, chunkname, env); end if not chunk then err = err:gsub("^%[string .-%]:%d+: ", ""); err = err:gsub("^:%d+: ", ""); err = err:gsub("''", "the end of the line"); result.attr.type = "error"; result:text("Sorry, I couldn't understand that... "..err); event.origin.send(result); return; end end taskok, message = chunk(flags); if promise.is_promise(taskok) then taskok:next(function (resolved_message) send_result(true, resolved_message); end, function (rejected_message) send_result(nil, rejected_message); end); else send_result(taskok, message); end end module:hook("admin/repl-input", function (event) local ok, err = pcall(handle_line, event); if not ok then event.origin.send(st.stanza("repl-result", { type = "error" }):text(err)); end return true; end); local function describe_command(s, hidden) 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); hidden = hidden; }; end local function hidden_command(s) return describe_command(s, true); end -- Console commands -- -- These are simple commands, not valid standalone in Lua -- Help about individual topics is handled by def_env.help function commands.help(session, data) local print = session.print; 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 -- Session environment -- -- 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", { preset = "pretty"; maxdepth = 2; table_iterator = "pairs"; }) def_env.output = new_section("Configure admin console output"); function def_env.output:configure(opts) if type(opts) ~= "table" then opts = { preset = opts }; end if not opts.fallback then -- XXX Error message passed to fallback is lost, does it matter? opts.fallback = tostring; end for k,v in pairs(serialize_defaults) do if opts[k] == nil then opts[k] = v; end end if opts.table_iterator == "pairs" then opts.table_iterator = pairs; elseif type(opts.table_iterator) ~= "function" then opts.table_iterator = nil; -- rawpairs is the default end self.session.serialize = serialization.new(opts); end 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 if not command_help.hidden then c = c + 1; local desc = command_help.desc or command_help.module and ("Provided by mod_"..command_help.module) or ""; if self.session.repl then local args = array.pluck(command_help.args, "name"):concat(", "); print(("%s:%s(%s) - %s"):format(section_name, command, args, desc)); else local args = array.pluck(command_help.args, "name"):concat("> <"); if args ~= "" then args = "<"..args..">"; end print(("%s %s %s"):format(section_name, command, args)); print((" %s"):format(desc)); if command_help.flags then local flags = command_help.flags; print(""); print((" Flags:")); if flags.kv_params then for name in it.sorted_pairs(flags.kv_params) do print(" --"..name:gsub("_", "-")); end end if flags.value_params then for name in it.sorted_pairs(flags.value_params) do print(" --"..name:gsub("_", "-").." <"..name..">"); end end if flags.array_params then for name in it.sorted_pairs(flags.array_params) do print(" --"..name:gsub("_", "-").." <"..name..">, ..."); end end end print(""); end end 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(); dofile "prosody" prosody = _G.prosody; 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; t = (t - seconds)/60; local minutes = t%60; t = (t - minutes)/60; local hours = t%24; t = (t - hours)/24; local days = t; return true, string.format("This server has been running for %d day%s, %d hour%s and %d minute%s (since %s)", days, (days ~= 1 and "s") or "", hours, (hours ~= 1 and "s") or "", 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"; end 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")); end local mem, lua_mem = pposix.meminfo(), collectgarbage("count"); local print = self.session.print; print("Process: "..human((mem.allocated+mem.allocated_mmap)/1024)); print(" Used: "..human(mem.used/1024).." ("..human(lua_mem).." by Lua)"); print(" Free: "..human(mem.unused/1024).." ("..human(mem.returnable/1024).." returnable)"); return true, "OK"; end def_env.module = new_section("Commands to load/reload/unload modules/plugins"); local function get_hosts_set(hosts) if type(hosts) == "table" then if hosts[1] then return set.new(hosts); elseif hosts._items then return hosts; end elseif type(hosts) == "string" then return set.new { hosts }; elseif hosts == nil then return set.new(array.collect(keys(prosody.hosts))); end end -- Hosts with a module or all virtualhosts if no module given -- matching modules_enabled in the global section local function get_hosts_with_module(hosts, module) local hosts_set = get_hosts_set(hosts) / function (host) if module then -- Module given, filter in hosts with this module loaded if modulemanager.is_loaded(host, module) then return host; else return nil; end end if not hosts then -- No hosts given, filter in VirtualHosts if prosody.hosts[host].type == "local" then return host; else return nil end end; -- No module given, but hosts are, don't filter at all return host; end; if module and modulemanager.get_module("*", module) then hosts_set:add("*"); end 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"; end local print = self.session.print; hosts = get_hosts_with_module(hosts, name); if hosts:empty() then return false, "mod_" .. name .. " does not appear to be loaded on the specified hosts"; end 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", ["http-provider"] = "HTTP services", ["net-provider"] = "Network service", ["storage-provider"] = "Storage driver", ["measure"] = "Legacy metrics", ["metric"] = "Metrics", ["task"] = "Periodic task", }; local item_formatters = { ["feature"] = tostring, ["identity"] = function(ident) return ident.type .. "/" .. ident.category; end, ["adhoc-provider"] = item_name, ["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"] = 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, %s)", item.name or item.id, item.when, task_timefmt(item.last)); end }; for host in hosts do local mod = modulemanager.get_module(host, name); if mod.module.host == "*" then print("in global context"); else print("on " .. tostring(prosody.hosts[mod.module.host])); end print(" path: " .. (mod.module.path or "n/a")); if mod.module.status_message then print(" status: [" .. mod.module.status_type .. "] " .. mod.module.status_message); end if mod.module.items and next(mod.module.items) ~= nil then print(" provides:"); for kind, items in pairs(mod.module.items) do local label = friendly_descriptions[kind] or kind:gsub("%-", " "):gsub("^%a", string.upper); print(string.format(" - %s (%d item%s)", label, #items, #items > 1 and "s" or "")); local formatter = item_formatters[kind]; if formatter then for _, item in ipairs(items) do print(" - " .. formatter(item, mod.module)); end end end end if mod.module.dependencies and next(mod.module.dependencies) ~= nil then print(" dependencies:"); for dep in pairs(mod.module.dependencies) do -- 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 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); if not mod then ok = false; if err == "global-module-already-loaded" then if count > 0 then ok, err, count = true, nil, 1; end break; end self.session.print(err or "Unknown error loading module"); 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 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 ok = false; self.session.print(err or "Unknown error unloading module"); 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 return ok, (ok and "Module unloaded from "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); end local function _sort_hosts(a, b) if a == "*" then return true elseif b == "*" then return false 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) -- Reload the module for each host local ok, err, count = true, nil, 0; for _, host in ipairs(hosts) do if modulemanager.is_loaded(host, name) then ok, err = modulemanager.reload(host, name); if not ok then ok = false; self.session.print(err or "Unknown error reloading module"); else count = count + 1; if ok == nil then ok = true; end self.session.print("Reloaded on "..host); end end end 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); local print = self.session.print; for _, host in ipairs(hosts) do print((host == "*" and "Global" or host)..":"); local modules = array.collect(keys(modulemanager.get_modules(host) or {})):sort(); if #modules == 0 then if prosody.hosts[host] then print(" No modules loaded"); else print(" Host not found"); end else for _, name in ipairs(modules) do local status, status_text = modulemanager.get_module(host, name).module:get_status(); local status_summary = ""; if status == "warn" or status == "error" then status_summary = (" (%s: %s)"):format(status, status_text); end print((" %s%s"):format(name, status_summary)); end end end end def_env.config = new_section("Reloading the configuration, etc."); function def_env.config:load(filename, format) 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"; end 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 "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 = new_section("Commands to manage local client-to-server sessions"); local function get_jid(session) if session.username then return session.full_jid or jid_join(session.username, session.host, session.resource); end local conn = session.conn; local ip = session.ip or "?"; local clientport = conn and conn:clientport() or "?"; local serverip = conn and conn.server and conn:server():ip() or "?"; local serverport = conn and conn:serverport() or "?" return jid_join("["..ip.."]:"..clientport, session.host or "["..serverip.."]:"..serverport); end local function get_c2s() local c2s = array.collect(values(prosody.full_sessions)); c2s:append(array.collect(values(module:shared"/*/c2s/sessions"))); c2s:append(array.collect(values(module:shared"/*/bosh/sessions"))); c2s:unique(); return c2s; end local function _sort_by_jid(a, b) if a.host == b.host then if a.username == b.username then return (a.resource or "") > (b.resource or ""); end return (a.username or "") > (b.username or ""); end return _sort_hosts(a.host or "", b.host or ""); end local function show_c2s(callback) get_c2s():sort(_sort_by_jid):map(function (session) callback(get_jid(session), session) 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"; end local function get_s2s_hosts(session) --> local,remote if session.direction == "outgoing" then return session.host or session.from_host, session.to_host; elseif session.direction == "incoming" then return session.host or session.to_host, session.from_host; end end available_columns = { jid = { title = "JID"; description = "Full JID of user session"; width = "3p"; key = "full_jid"; mapper = function(full_jid, session) return full_jid or get_jid(session) end; }; host = { title = "Host"; description = "Local hostname"; key = "host"; width = "1p"; mapper = function(host, session) return host or get_s2s_hosts(session) or "?"; end; }; remote = { title = "Remote"; description = "Remote hostname"; width = "1p"; mapper = function(_, session) return select(2, get_s2s_hosts(session)); end; }; port = { title = "Port"; description = "Server port used"; width = #string.format("%d", 0xffff); -- max 16 bit unsigned integer align = "right"; key = "conn"; mapper = function(conn) if conn then return conn:serverport(); 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 = #"<->"; key = "direction"; mapper = function(dir, session) if session.incoming and session.outgoing then return "<->"; end if dir == "outgoing" then return "-->"; end if dir == "incoming" then return "<--"; end end; }; 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 = math.max(#"BOSH", #"WebSocket", #"TCP"); mapper = function(_, session) if session.bosh_version then return "BOSH"; elseif session.websocket_request then return "WebSocket"; else return "TCP"; end end; }; ipv = { title = "IPv"; description = "Internet Protocol version (4 or 6)"; 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 = 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 = math.max(#"online", #"chat"); key = "presence"; mapper = function(p) if not p then return ""; end return p:get_child_text("show") or "online"; end; }; secure = { title = "Security"; description = "TLS version or security status"; key = "conn"; 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 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)"; -- 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 info = conn and conn.ssl_info and conn:ssl_info(); if info then return info.cipher end end; }; cert = { title = "Certificate"; description = "Validation status of certificate"; key = "cert_identity_status"; width = math.max(#"Expired", #"Self-signed", #"Untrusted", #"Mismatched", #"Unknown"); mapper = function(cert_status, session) 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"; end -- TODO cert_chain_errors can be a string, handle that return "Unknown"; end; }; sni = { title = "SNI"; description = "Hostname requested in TLS"; width = "1p"; -- same as host, remote etc mapper = function(_, session) if not session.conn then return end local sock = session.conn:socket(); return sock and sock.getsniname and sock:getsniname() or ""; end; }; alpn = { title = "ALPN"; description = "Protocol requested in TLS"; 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(); return sock and sock.getalpn and sock:getalpn() or ""; end; }; smacks = { title = "SM"; description = "Stream Management (XEP-0198) status"; key = "smacks"; -- 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 return "yes"; end; }; smacks_queue = { title = "SM Queue"; description = "Length of Stream Management stanza queue"; key = "outgoing_stanza_queue"; width = 8; align = "right"; mapper = function (queue) return queue and tostring(queue:count_unacked()); end }; csi = { title = "CSI State"; description = "Client State Indication (XEP-0352)"; key = "state"; -- TODO include counter }; s2s_sasl = { title = "SASL"; description = "Server authentication status"; key = "external_auth"; width = 10; mapper = capitalize }; dialback = { title = "Dialback"; description = "Legacy server verification"; key = "dialback_key"; 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 return "Not used"; end return "Not initiated"; elseif session.type == "s2sin_unauthed" or session.type == "s2sout_unauthed" then return "Initiated"; else return "Completed"; 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) if type(colspec) == "string" then colspec = array(colspec:gmatch("%S+")); end 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 = "1p"; key = col }; elseif type(col) ~= "table" then return false, ("argument %d: expected string|table but got %s"):format(i, type(col)); else columns[i] = col; end end 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"; "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 match_jid == "*" or jid_compare(jid, match_jid); end local group_by_host = true; for _, col in ipairs(columns) do if col.key == "full_jid" or col.key == "host" then group_by_host = false; break end end if not group_by_host then print(row()); end local currenthost = nil; local c2s_sessions = get_c2s(); local total_count = #c2s_sessions; c2s_sessions:filter(match):sort(_sort_by_jid); local shown_count = #c2s_sessions; for _, session in ipairs(c2s_sessions) do if group_by_host and session.host ~= currenthost then currenthost = session.host; print("#",prosody.hosts[currenthost] or "Unknown host"); print(row()); end print(row(session)); end if total_count ~= shown_count then return true, ("%d out of %d c2s sessions shown"):format(shown_count, total_count); end 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 local function build_reason(text, condition) if text or condition then return { text = text, condition = condition or "undefined-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) if jid == match_jid or jid_bare(jid) == match_jid then count = count + 1; session:close(build_reason(text, condition)); end end); 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 show_c2s(function (jid, session) count = count + 1; session:close(build_reason(text, condition)); end); return true, "Total: "..count.." sessions closed"; end 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); if (a_local or "") == (b_local or "") then return _sort_hosts(a_remote or "", b_remote or ""); end 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, self.session.width); local function match(session) return match_s2s_jid(session, match_jid); end local group_by_host = true; local currenthost = nil; for _, col in ipairs(columns) do if col.key == "host" then group_by_host = false; break end end if not group_by_host then print(row()); end local s2s_sessions = array(iterators.values(module:shared"/*/s2s/sessions")); local total_count = #s2s_sessions; s2s_sessions:filter(match):sort(_sort_s2s); local shown_count = #s2s_sessions; for _, session in ipairs(s2s_sessions) do if group_by_host and currenthost ~= get_s2s_hosts(session) then currenthost = get_s2s_hosts(session); print("#",prosody.hosts[currenthost] or "Unknown host"); print(row()); end print(row(session)); end if total_count ~= shown_count then return true, ("%d out of %d s2s connections shown"):format(shown_count, total_count); end 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 local function print_subject(print, subject) for _, entry in ipairs(subject) do print( (" %s: %q"):format( entry.name or entry.oid, entry.value:gsub("[\r\n%z%c]", " ") ) ); end end -- As much as it pains me to use the 0-based depths that OpenSSL does, -- I think there's going to be more confusion among operators if we -- break from that. local function print_errors(print, errors) for depth, t in pairs(errors) do print( (" %d: %s"):format( depth-1, table.concat(t, "\n| ") ) ); end end 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 match_s2s_jid(session, domain) and session or nil; end; local cert_set = {}; for session in domain_sessions do local conn = session.conn; conn = conn and conn:socket(); if not conn.getpeerchain then if conn.dohandshake then error("This version of LuaSec does not support certificate viewing"); end else local cert = conn:getpeercertificate(); if cert then local certs = conn:getpeerchain(); local digest = cert:digest("sha1"); if not cert_set[digest] then local chain_valid, chain_errors = conn:getpeerverification(); cert_set[digest] = { { from = session.from_host, to = session.to_host, direction = session.direction }; chain_valid = chain_valid; chain_errors = chain_errors; certs = certs; }; else table.insert(cert_set[digest], { from = session.from_host, to = session.to_host, direction = session.direction }); end end end end local domain_certs = array.collect(values(cert_set)); -- Phew. We now have a array of unique certificates presented by domain. local n_certs = #domain_certs; if n_certs == 0 then return "No certificates found for "..domain; end local function _capitalize_and_colon(byte) return string.upper(byte)..":"; end local function pretty_fingerprint(hash) return hash:gsub("..", _capitalize_and_colon):sub(1, -2); end for cert_info in values(domain_certs) do local certs = cert_info.certs; local cert = certs[1]; print("---") print("Fingerprint (SHA1): "..pretty_fingerprint(cert:digest("sha1"))); print(""); local n_streams = #cert_info; print("Currently used on "..n_streams.." stream"..(n_streams==1 and "" or "s")..":"); for _, stream in ipairs(cert_info) do if stream.direction == "incoming" then print(" "..stream.to.." <- "..stream.from); else print(" "..stream.from.." -> "..stream.to); end end print(""); local chain_valid, errors = cert_info.chain_valid, cert_info.chain_errors; local valid_identity = cert_verify_identity(domain, "xmpp-server", cert); if chain_valid then print("Trusted certificate: Yes"); else print("Trusted certificate: No"); print_errors(print, errors); end print(""); print("Issuer: "); print_subject(print, cert:issuer()); print(""); print("Valid for "..domain..": "..(valid_identity and "Yes" or "No")); print("Subject:"); print_subject(print, cert:subject()); end print("---"); return ("Showing "..n_certs.." certificate" ..(n_certs==1 and "" or "s") .." 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"; local match_id; if from and not to then match_id, from = from, nil; elseif not to then return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'"; elseif from == to then return false, "Both from and to are the same... you can't do that :)"; 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 ((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; 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 host == "*" or match_s2s_jid(session, host) then session:close(build_reason(text, condition)); count = count + 1; end end if count == 0 then return false, "No sessions to close."; else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end end 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 host_type; for host, host_session in iterators.sorted_pairs(prosody.hosts, _sort_hosts) do i = i + 1; host_type = host_session.type; if host_type == "local" then print(host); else 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, host_type)); end end return true, i.." hosts"; end 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; local n_services, n_ports = 0, 0; for service, interfaces in iterators.sorted_pairs(services) do n_services = n_services + 1; local ports_list = {}; for interface, ports in pairs(interfaces) do for port in pairs(ports) do table.insert(ports_list, "["..interface.."]:"..port); end end n_ports = n_ports + #ports_list; print(service..": "..table.concat(ports_list, ", ")); end 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; local services = portmanager.get_active_services().data; for service, interfaces in pairs(services) do -- luacheck: ignore 213 for interface, ports in pairs(interfaces) do if not close_interface or close_interface == interface then if ports[close_port] then self.session.print("Closing ["..interface.."]:"..close_port.."..."); local ok, err = portmanager.close(interface, close_port) if not ok then self.session.print("Failed to close "..interface.." "..close_port..": "..err); else n_closed = n_closed + 1; end end end end end return true, "Closed "..n_closed.." ports"; end 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; __tostring = function (self) return "MUC room <"..self.room.jid..">"; end; }; local function check_muc(jid) local room_name, host = jid_split(jid); if not prosody.hosts[host] then return nil, "No such host: "..host; elseif not prosody.hosts[host].modules.muc then return nil, "Host '"..host.."' is not a MUC service"; end 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 return room_name, host; end if not room_name then return nil, host end if config ~= nil and type(config) ~= "table" then return nil, "Config must be a table"; end if prosody.hosts[host].modules.muc.get_room_from_jid(room_jid) then return nil, "Room exists already" end 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_obj, err = get_muc(room_jid); if not room_obj then 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 return nil, "Please supply the address of a local MUC component"; end local print = self.session.print; local c = 0; for room in host_session.modules.muc.each_room() do print(room.jid); c = c + 1; end return true, c.." rooms"; end 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 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 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 if not role then role = module:get_option_string("default_provisioned_role", "prosody:member"); end return promise.resolve(password or self.session.request_input("password")):next(function (password_) local ok, err = um.create_user_with_role(username, password_, host, role); if not ok then return promise.reject("Could not create user: "..err); end return ("Created %s with role '%s'"):format(jid, role); end); 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 return true, "User disabled"; else 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 return nil, "No such host: "..host; elseif not um.user_exists(username, host) then return nil, "No such user"; end local ok, err = um.delete_user(username, host); if ok then return true, "User deleted"; else return nil, "Could not delete user: "..err; 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 return nil, "No such host: "..host; elseif not um.user_exists(username, host) then return nil, "No such user"; end return promise.resolve(password or self.session.request_input("password")):next(function (password_) local ok, err = um.set_password(username, password_, host, nil); if ok then return "User password changed"; else return promise.reject("Could not change password for user: "..err); end end); end describe_command [[user:role(jid, host) - Show primary role for a user]] function def_env.user:role(jid, host) local username, userhost = jid_split(jid); if host == nil then host = userhost; 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 local primary_role = um.get_user_role(username, host); local secondary_roles = um.get_user_secondary_roles(username, host); local primary_role_desc = primary_role and primary_role.name or ""; print(primary_role and primary_role.name or ""); local n_secondary = 0; for role_name in pairs(secondary_roles or {}) do n_secondary = n_secondary + 1; print(role_name.." (secondary)"); end if n_secondary > 0 then return true, primary_role_desc.." (primary)"; end return true, primary_role_desc; end def_env.user.roles = def_env.user.role; describe_command [[user:set_role(jid, host, role) - Set primary role of a user (see 'help roles')]] -- user:set_role("someone@example.com", "example.com", "prosody:admin") -- user:set_role("someone@example.com", "prosody:admin") function def_env.user:set_role(jid, host, new_role) local username, userhost = jid_split(jid); if new_role == nil then host, new_role = userhost, host; end if not prosody.hosts[host] then return nil, "No such host: "..host; elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then return nil, "No such user"; end 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 hidden_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 local role, err = um.add_user_secondary_role(username, host, new_role); if not role then return nil, err; end return true, "Role added"; end hidden_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 local ok, err = um.remove_user_secondary_role(username, host, role_name); if not ok then return nil, err; end return true, "Role removed"; 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 return nil, "No host given"; elseif not prosody.hosts[host] then return nil, "No such host"; end local print = self.session.print; local total, matches = 0, 0; for user in um.users(host) do if not pat or user:match(pat) then print(user.."@"..host); matches = matches + 1; end total = total + 1; end return true, "Showing "..(pat and (matches.." of ") or "all " )..total.." users"; end def_env.xmpp = new_section("Commands for sending XMPP stanzas"); 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)); if not localhost then return nil, "Invalid sender hostname"; elseif not prosody.hosts[localhost] then return nil, "No such local host"; end if not remotehost then return nil, "Invalid destination hostname"; elseif prosody.hosts[remotehost] then return nil, "Both hosts are local"; end local iq = st.iq{ from=localhost, to=remotehost, type="get", id=new_id()} :tag("ping", {xmlns="urn:xmpp:ping"}); local time_start = time.now(); local print = self.session.print; local function onchange(what) return function(event) local s2s_session = event.session; if (s2s_session.from_host == localhost and s2s_session.to_host == remotehost) or (s2s_session.to_host == localhost and s2s_session.from_host == remotehost) then local dir = available_columns.dir.mapper(s2s_session.direction, s2s_session); print(("Session %s (%s%s%s) %s (%gs)"):format(s2s_session.id, localhost, dir, remotehost, what, time.now() - time_start)); elseif s2s_session.type == "s2sin_unauthed" and s2s_session.to_host == nil and s2s_session.from_host == nil then print(("Session %s %s (%gs)"):format(s2s_session.id, what, time.now() - time_start)); end end end local onconnected = onchange("connected"); local onauthenticated = onchange("authenticated"); local onestablished = onchange("established"); local ondestroyed = onchange("destroyed"); module:hook("s2s-connected", onconnected, 1); module:context(localhost):hook("s2s-authenticated", onauthenticated, 1); module:hook("s2sout-established", onestablished, 1); module:hook("s2sin-established", onestablished, 1); module:hook("s2s-destroyed", ondestroyed, 1); return module:context(localhost):send_iq(iq, nil, timeout):finally(function() module:unhook("s2s-connected", onconnected, 1); module:context(localhost):unhook("s2s-authenticated", onauthenticated); module:unhook("s2sout-established", onestablished); module:unhook("s2sin-established", onestablished); module:unhook("s2s-destroyed", ondestroyed); end):next(function(pong) 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 = 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; if not resolver then resolver = adns.resolver(); session.dns_resolver = resolver; end 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 = 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_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); if #http_apps > 0 then local http_host = module:context(host):get_option_string("http_host"); if host == "*" then print("Global HTTP endpoints available on all hosts:"); else print("HTTP endpoints on "..host..(http_host and (" (using "..http_host.."):") or ":")); end print(output_split()); for _, provider in ipairs(http_apps) do local mod = provider._provided_by; 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((internal=="" and output_simple or output_split){mod, external, internal}); end print(""); end end local default_host = module:get_option_string("http_default_host"); if not default_host then print("HTTP requests to unknown hosts will return 404 Not Found"); else print("HTTP requests to unknown hosts will be handled by "..default_host); end return true; end def_env.watch = new_section("Commands for watching live logs from the server"); describe_command [[watch:log() - Follow debug logs]] function def_env.watch:log() local writing = false; local sink = logger.add_simple_sink(function (source, level, message) if writing then return; end writing = true; self.session.print(source, level, message); writing = false; end); while self.session.is_connected() do async.sleep(3); end if not logger.remove_sink(sink) then module:log("warn", "Unable to remove watch:log() sink"); end end describe_command [[watch:stanzas(target, filter) - Watch live stanzas matching the specified target and filter]] local stanza_watchers = module:require("mod_debug_stanzas/watcher"); function def_env.watch:stanzas(target_spec, filter_spec) local function handler(event_type, stanza, session) if stanza then if event_type == "sent" then self.session.print(("\n"):format(session.id)); elseif event_type == "received" then self.session.print(("\n"):format(session.id)); else self.session.print(("\n"):format(event_type, session.id)); end self.session.print(stanza); elseif session then self.session.print("\n"); elseif event_type then self.session.print("\n"); end end stanza_watchers.add({ target_spec = { jid = target_spec; }; filter_spec = filter_spec and { with_jid = filter_spec; }; }, handler); while self.session.is_connected() do async.sleep(3); end stanza_watchers.remove(handler); end def_env.debug = new_section("Commands for debugging the server"); describe_command [[debug:logevents(host) - Enable logging of fired events on host]] function def_env.debug:logevents(host) 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 "prosody.net.http.server"._events; elseif not prosody.hosts[host] then return false, "Unknown host: "..host; else events_obj = prosody.hosts[host].events; end else events_obj = prosody.events; end 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"prosody.util.timer".add_task; local h, params = add_task.h, add_task.params; local function normalize_time(t) return t; end local function format_time(t) return os.date("%F %T", math.floor(normalize_time(t))); end if h then print("-- util.timer"); elseif server.timer then print("-- net.server.timer"); h = server.timer.add_task.timers; normalize_time = server.timer.to_absolute_time or normalize_time; end if h then local timers = {}; for i, id in ipairs(h.ids) do local t, cb = h.priorities[i], h.items[id]; if not params then local param = cb.param; if param then cb = param.callback; else cb = cb.timer_callback or cb; end elseif params[id] then cb = params[id].callback or cb; end table.insert(timers, { format_time(t), cb }); end table.sort(timers, function (a, b) return a[1] < b[1] end); for _, t in ipairs(timers) do print(t[1], t[2]) end end if server.event_base then local count = 0; for _, v in pairs(debug.getregistry()) do if type(v) == "function" and v.callback and v.callback == add_task._on_timer then count = count + 1; end end print(count .. " libevent callbacks"); end if h then local next_time = h:peek(); if next_time then return true, ("Next event at %s (in %.6fs)"):format(format_time(next_time), normalize_time(next_time) - time.now()); end end return true; end 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 describe_command [[debug:cert_index([path]) - Show Prosody's view of a directory of certs]] function def_env.debug:cert_index(path) local print = self.session.print; local cm = require "core.certmanager"; path = path or module:get_option("certificates", "certs"); local sink = logger.add_simple_sink(function (source, level, message) if source == "certmanager" then if level == "debug" or level == "info" then level = "II"; elseif level == "warn" or level == "error" then level = "EE"; end self.session.print(level..": "..message); end end); print("II: Scanning "..path.."..."); local index = {}; cm.index_certs(path, index) if not logger.remove_sink(sink) then module:log("warn", "Unable to remove log sink"); end local c, max_domain = 0, 8; for domain in pairs(index) do if #domain > max_domain then max_domain = #domain; end end print(""); local row = format_table({ { title = "Domain", width = max_domain }; { title = "Certificate", width = "100%" }; { title = "Service", width = 5 }; }, self.session.width); print(row()); print(("-"):rep(self.session.width or 80)); for domain, certs in it.sorted_pairs(index) do for cert_file, services in it.sorted_pairs(certs) do for service in it.sorted_pairs(services) do c = c + 1; print(row({ domain, cert_file, service })); end end end print(""); return true, ("Showing %d certificates in %s"):format(c, path); end def_env.stats = new_section("Commands to show internal statistics"); local short_units = { seconds = "s", bytes = "B", }; local stats_methods = {}; function stats_methods:render_single_fancy_histogram_ex(print, prefix, metric_family, metric, cumulative) local creation_timestamp, sum, count local buckets = {} local prev_bucket_count = 0 for suffix, extra_labels, value in metric:iter_samples() do if suffix == "_created" then creation_timestamp = value elseif suffix == "_sum" then sum = value elseif suffix == "_count" then count = value elseif extra_labels then local bucket_threshold = extra_labels["le"] local bucket_count if cumulative then bucket_count = value else bucket_count = value - prev_bucket_count prev_bucket_count = value end if bucket_threshold == "+Inf" then t_insert(buckets, {threshold = 1/0, count = bucket_count}) elseif bucket_threshold ~= nil then t_insert(buckets, {threshold = tonumber(bucket_threshold), count = bucket_count}) end end end if #buckets == 0 or not creation_timestamp or not sum or not count then print("[no data or not a histogram]") return false end local graph_width, graph_height, wscale = #buckets, 10, 1; if graph_width < 8 then wscale = 8 elseif graph_width < 16 then wscale = 4 elseif graph_width < 32 then wscale = 2 end local eighth_chars = " ▁▂▃▄▅▆▇█"; local max_bin_samples = 0 for _, bucket in ipairs(buckets) do if bucket.count > max_bin_samples then max_bin_samples = bucket.count end end print(""); print(prefix) print(("_"):rep(graph_width*wscale).." "..max_bin_samples); for row = graph_height, 1, -1 do local row_chars = {}; local min_eighths, max_eighths = 8, 0; for i = 1, #buckets do local char_eighths = math.ceil(math.max(math.min((graph_height/(max_bin_samples/buckets[i].count))-(row-1), 1), 0)*8); if char_eighths < min_eighths then min_eighths = char_eighths; end if char_eighths > max_eighths then max_eighths = char_eighths; end if char_eighths == 0 then row_chars[i] = ("-"):rep(wscale); else local char = eighth_chars:sub(char_eighths*3+1, char_eighths*3+3); row_chars[i] = char:rep(wscale); end end print(table.concat(row_chars).."|- "..string.format("%.8g", math.ceil((max_bin_samples/graph_height)*(row-0.5)))); end local legend_pat = string.format("%%%d.%dg", wscale-1, wscale-1) local row = {} for i = 1, #buckets do local threshold = buckets[i].threshold t_insert(row, legend_pat:format(threshold)) end t_insert(row, " " .. metric_family.unit) print(t_concat(row, "/")) return true end function stats_methods:render_single_fancy_histogram(print, prefix, metric_family, metric) return self:render_single_fancy_histogram_ex(print, prefix, metric_family, metric, false) end function stats_methods:render_single_fancy_histogram_cf(print, prefix, metric_family, metric) -- cf = cumulative frequency return self:render_single_fancy_histogram_ex(print, prefix, metric_family, metric, true) end function stats_methods:cfgraph() for _, stat_info in ipairs(self) do local family_name, metric_family = unpack(stat_info, 1, 2) local function print(s) table.insert(stat_info.output, s); end if not self:render_family(print, family_name, metric_family, self.render_single_fancy_histogram_cf) then return self end end return self; end function stats_methods:histogram() for _, stat_info in ipairs(self) do local family_name, metric_family = unpack(stat_info, 1, 2) local function print(s) table.insert(stat_info.output, s); end if not self:render_family(print, family_name, metric_family, self.render_single_fancy_histogram) then return self end end return self; end function stats_methods:render_single_counter(print, prefix, metric_family, metric) local created_timestamp, current_value for suffix, _, value in metric:iter_samples() do if suffix == "_created" then created_timestamp = value elseif suffix == "_total" then current_value = value end end if current_value and created_timestamp then local base_unit = short_units[metric_family.unit] or metric_family.unit local unit = base_unit .. "/s" local factor = 1 if base_unit == "s" then -- be smart! unit = "%" factor = 100 elseif base_unit == "" then unit = "events/s" end print(("%-50s %s"):format(prefix, format_number(factor * current_value / (self.now - created_timestamp), unit.." [avg]"))); end end function stats_methods:render_single_gauge(print, prefix, metric_family, metric) local current_value for _, _, value in metric:iter_samples() do current_value = value end if current_value then local unit = short_units[metric_family.unit] or metric_family.unit print(("%-50s %s"):format(prefix, format_number(current_value, unit))); end end function stats_methods:render_single_summary(print, prefix, metric_family, metric) local sum, count for suffix, _, value in metric:iter_samples() do if suffix == "_sum" then sum = value elseif suffix == "_count" then count = value end end if sum and count then local unit = short_units[metric_family.unit] or metric_family.unit if count == 0 then print(("%-50s %s"):format(prefix, "no obs.")); else print(("%-50s %s"):format(prefix, format_number(sum / count, unit.."/event [avg]"))); end end end function stats_methods:render_family(print, family_name, metric_family, render_func) local labelkeys = metric_family.label_keys if #labelkeys > 0 then print(family_name) for labelset, metric in metric_family:iter_metrics() do local labels = {} for i, k in ipairs(labelkeys) do local v = labelset[i] t_insert(labels, ("%s=%s"):format(k, v)) end local prefix = " "..t_concat(labels, " ") render_func(self, print, prefix, metric_family, metric) end else for _, metric in metric_family:iter_metrics() do render_func(self, print, family_name, metric_family, metric) end end end local function stats_tostring(stats) local print = stats.session.print; for _, stat_info in ipairs(stats) do if #stat_info.output > 0 then print("\n#"..stat_info[1]); print(""); for _, v in ipairs(stat_info.output) do print(v); end print(""); else local metric_family = stat_info[2] if metric_family.type_ == "counter" then stats:render_family(print, stat_info[1], metric_family, stats.render_single_counter) elseif metric_family.type_ == "gauge" or metric_family.type_ == "unknown" then stats:render_family(print, stat_info[1], metric_family, stats.render_single_gauge) elseif metric_family.type_ == "summary" or metric_family.type_ == "histogram" then stats:render_family(print, stat_info[1], metric_family, stats.render_single_summary) end end end return #stats.." statistics displayed"; end local stats_mt = {__index = stats_methods, __tostring = stats_tostring } local function new_stats_context(self) -- TODO: instead of now(), it might be better to take the time of the last -- interval, if the statistics backend is set to use periodic collection -- Otherwise we get strange stuff like average cpu usage decreasing until -- the next sample and so on. 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 "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 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 table.insert(displayed_stats, { family_name, metric_family, output = {} }) end end 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 = event.source and ("mod_"..event.source.name) or ""; if not schema.validate(command_metadata_schema, command) or type(command.handler) ~= "function" then module:log("warn", "Ignoring command added by %s: missing or invalid data", mod_name); return; end local handler = command.handler; if command_host then if type(command.host_selector) ~= "string" then module:log("warn", "Ignoring command %s:%s() added by %s - missing/invalid host_selector", command.section, command.name, mod_name); return; end local qualified_name = command.section..":"..command.name; local host_command_info = host_commands[qualified_name]; if not host_command_info then local selector_index; for i, arg in ipairs(command.args) do if arg.name == command.host_selector then selector_index = i + 1; -- +1 to account for 'self' break; end end if not selector_index then module:log("warn", "Command %s() host selector argument '%s' not found - not registering", qualified_name, command.host_selector); return; end host_command_info = { [false] = { host_selector = command.host_selector; handler = function (...) local selected_host = select(2, jid_split((select(selector_index, ...)))); if type(selected_host) ~= "string" then return nil, "Invalid or missing argument '"..command.host_selector.."'"; end if not prosody.hosts[selected_host] then return nil, "Unknown host: "..selected_host; end local host_handler = host_commands[qualified_name][selected_host]; if not host_handler then return nil, "This command is not available on "..selected_host; end return host_handler(...); end; }; }; host_commands[qualified_name] = host_command_info; end if host_command_info[command_host] then module:log("warn", "Command %s() is already registered - overwriting with %s", qualified_name, mod_name); end host_command_info[command_host] = handler; end local section_t = def_env[command.section]; if not section_t then section_t = {}; def_env[command.section] = section_t; end if command_host then section_t[command.name] = host_commands[command.section..":"..command.name][false].handler; else section_t[command.name] = command.handler; end local section_mt = getmetatable(section_t); if not section_mt then section_mt = {}; setmetatable(section_t, section_mt); end local section_help = section_mt.help; if not section_help then section_help = { desc = command.section_desc; commands = {}; }; section_mt.help = section_help; end if command.flags then if command.flags.stop_on_positional == nil then command.flags.stop_on_positional = false; end if command.flags.strict == nil then command.flags.strict = true; end end section_help.commands[command.name] = { desc = command.desc; full = command.help; args = array(command.args); flags = command.flags; module = command._provided_by; }; module:log("debug", "Shell command added by %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 ------------- function printbanner(session) local option = module:get_option_string("console_banner", "full"); if option == "full" or option == "graphic" then session.print [[ ____ \ / _ | _ \ _ __ ___ ___ _-_ __| |_ _ | |_) | '__/ _ \/ __|/ _ \ / _` | | | | | __/| | | (_) \__ \ |_| | (_| | |_| | |_| |_| \___/|___/\___/ \__,_|\__, | A study in simplicity |___/ ]] end if option == "short" or option == "full" then session.print("Welcome to the Prosody administration console. For a list of commands, type: help"); session.print("You may find more help on using this console in our online documentation at "); session.print("https://prosody.im/doc/console\n"); end if option ~= "short" and option ~= "full" and option ~= "graphic" then session.print(option); end end prosody-13.0.1/plugins/PaxHeaders/mod_admin_socket.lua0000644000000000000000000000011714773555365020063 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.583710502 prosody-13.0.1/plugins/mod_admin_socket.lua0000644000175000017500000000541214773555365022264 0ustar00prosodyprosody00000000000000module:set_global(); local have_unix, unix = pcall(require, "socket.unix"); if have_unix and type(unix) == "function" then -- COMPAT #1717 -- Before the introduction of datagram support, only the stream socket -- 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 accommodate both variants. unix = { stream = unix }; end if not have_unix or type(unix) ~= "table" then module:log_status("error", "LuaSocket unix socket support not available or incompatible, ensure it is up to date"); return; end local server = require "prosody.net.server"; local adminstream = require "prosody.util.adminstream"; local st = require "prosody.util.stanza"; local socket_path = module:get_option_path("admin_socket", "prosody.sock", "data"); local sessions = module:shared("sessions"); local function fire_admin_event(session, stanza) local event_data = { origin = session, stanza = stanza; }; local event_name; if stanza.attr.xmlns then event_name = "admin/"..stanza.attr.xmlns..":"..stanza.name; else event_name = "admin/"..stanza.name; end module:log("debug", "Firing %s", event_name); local ret = module:fire_event(event_name, event_data); if ret == nil then session.send(st.stanza("repl-result", { type = "error" }):text("No module handled this query. Is mod_admin_shell enabled?")); end return ret; end module:hook("server-stopping", function () for _, session in pairs(sessions) do session:close("system-shutdown"); end os.remove(socket_path); end); --- Unix domain socket management local conn, sock; local admin_server = adminstream.server(sessions, fire_admin_event); local listeners = admin_server.listeners; module:hook_object_event(admin_server.events, "disconnected", function (event) return module:fire_event("admin-disconnected", event); end); local function accept_connection() module:log("debug", "accepting..."); local client = sock:accept(); if not client then return; end server.wrapclient(client, "unix", 0, listeners, "*a"); end function module.load() sock = unix.stream(); sock:settimeout(0); os.remove(socket_path); local ok, err = sock:bind(socket_path); if not ok then module:log_status("error", "Unable to bind admin socket %s: %s", socket_path, err); return; end local ok, err = sock:listen(); if not ok then module:log_status("error", "Unable to listen on admin socket %s: %s", socket_path, err); return; end if server.wrapserver then conn = server.wrapserver(sock, socket_path, 0, listeners); else conn = server.watchfd(sock:getfd(), accept_connection); end end function module.unload() if conn then conn:close(); end if sock then sock:close(); end os.remove(socket_path); end prosody-13.0.1/plugins/PaxHeaders/mod_admin_telnet.lua0000644000000000000000000000011714773555365020066 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.583710502 prosody-13.0.1/plugins/mod_admin_telnet.lua0000644000175000017500000001101014773555365022256 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: ignore 212/self module:set_global(); module:depends("admin_shell"); local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" }; 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 }; local function printbanner(session) local option = module:get_option_string("console_banner", "full"); if option == "full" or option == "graphic" then session.print [[ ____ \ / _ | _ \ _ __ ___ ___ _-_ __| |_ _ | |_) | '__/ _ \/ __|/ _ \ / _` | | | | | __/| | | (_) \__ \ |_| | (_| | |_| | |_| |_| \___/|___/\___/ \__,_|\__, | A study in simplicity |___/ ]] end if option == "short" or option == "full" then session.print("Welcome to the Prosody administration console. For a list of commands, type: help"); session.print("You may find more help on using this console in our online documentation at "); session.print("https://prosody.im/doc/console\n"); end if option ~= "short" and option ~= "full" and option ~= "graphic" then session.print(option); end end console = {}; local runner_callbacks = {}; function runner_callbacks:ready() self.data.conn:resume(); end function runner_callbacks:waiting() self.data.conn:pause(); end function runner_callbacks:error(err) module:log("error", "Traceback[telnet]: %s", err); self.data.print("Fatal error while running command, it did not complete"); self.data.print("Error: "..tostring(err)); end function console:new_session(conn) local w = function(s) conn:write(s:gsub("\n", "\r\n")); end; local session = { conn = conn; send = function (t) if st.is_stanza(t) and (t.name == "repl-result" or t.name == "repl-output") then t = "| "..t:get_text().."\n"; end w(tostring(t)); end; print = function (...) local t = {}; for i=1,select("#", ...) do t[i] = tostring(select(i, ...)); end w("| "..table.concat(t, "\t").."\n"); end; serialize = tostring; disconnect = function () conn:close(); end; }; session.env = setmetatable({}, default_env_mt); session.thread = async.runner(function (line) console:process_line(session, line); session.send(string.char(0)); end, runner_callbacks, session); -- Load up environment with helper objects for name, t in pairs(def_env) do if type(t) == "table" then session.env[name] = setmetatable({ session = session }, { __index = t }); end end session.env.output:configure(); return session; end function console:process_line(session, line) line = line:gsub("\r?\n$", ""); if line == "bye" or line == "quit" or line == "exit" or line:byte() == 4 then session.print("See you!"); session:disconnect(); return; end return module:fire_event("admin/repl-input", { origin = session, stanza = st.stanza("repl-input"):text(line) }); end local sessions = {}; function module.save() return { sessions = sessions } end function module.restore(data) if data.sessions then for conn in pairs(data.sessions) do conn:setlistener(console_listener); local session = console:new_session(conn); sessions[conn] = session; end end end function console_listener.onconnect(conn) -- Handle new connection local session = console:new_session(conn); sessions[conn] = session; printbanner(session); session.send(string.char(0)); end function console_listener.onincoming(conn, data) local session = sessions[conn]; local partial = session.partial_data; if partial then data = partial..data; end for line in data:gmatch("[^\n]*[\n\004]") do if session.closed then return end session.thread:run(line); end session.partial_data = data:match("[^\n]+$"); end function console_listener.onreadtimeout(conn) local session = sessions[conn]; if session then session.send("\0"); return true; end end function console_listener.ondisconnect(conn, err) -- luacheck: ignore 212/err local session = sessions[conn]; if session then session.disconnect(); sessions[conn] = nil; end end function console_listener.ondetach(conn) sessions[conn] = nil; end module:provides("net", { name = "console"; listener = console_listener; default_port = 5582; private = true; }); prosody-13.0.1/plugins/PaxHeaders/mod_announce.lua0000644000000000000000000000011714773555365017231 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.583710502 prosody-13.0.1/plugins/mod_announce.lua0000644000175000017500000001276414773555365021442 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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; function send_to_online(message, host) host = host or module.host; local sessions; if host then sessions = { [host] = hosts[host] }; else sessions = hosts; end local c = 0; for hostname, host_session in pairs(sessions) do if host_session.sessions then message.attr.from = hostname; for username in pairs(host_session.sessions) do c = c + 1; message.attr.to = username.."@"..hostname; module:send(message); end end end 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 -based jabberd-style announcement sending function handle_announcement(event) local stanza = event.stanza; -- luacheck: ignore 211/node local node, host, resource = jid.split(stanza.attr.to); if resource ~= "announce/online" then return; -- Not an announcement end 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 module:log("info", "Sending server announcement to all online users"); local message = st.clone(stanza); message.attr.type = "headline"; message.attr.from = host; local c = send_to_online(message, host); module:log("info", "Announcement sent to %d online users", c); return true; end module:hook("message/host", handle_announcement); -- Ad-hoc command (XEP-0133) 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."; { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "subject", type = "text-single", label = "Subject" }; { name = "announcement", type = "text-multi", required = true, label = "Announcement" }; }; function announce_handler(_, data, state) if state then if data.action == "cancel" then return { status = "canceled" }; end 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(); if fields.subject and fields.subject ~= "" then message:text_tag("subject", fields.subject); end local count = send_to_online(message, data.to); module:log("info", "Announcement sent to %d online users", count); return { status = "completed", info = ("Announcement sent to %d online users"):format(count) }; else return { status = "executing", actions = {"next", "complete", default = "complete"}, form = announce_layout }, "executing"; end end module:depends "adhoc"; 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; }); prosody-13.0.1/plugins/PaxHeaders/mod_auth_anonymous.lua0000644000000000000000000000011714773555365020474 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.583710502 prosody-13.0.1/plugins/mod_auth_anonymous.lua0000644000175000017500000000400414773555365022671 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: ignore 212 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); -- define auth provider local provider = {}; function provider.test_password(username, password) return nil, "Password based auth not supported."; end function provider.get_password(username) return nil, "Password not available."; end function provider.set_password(username, password) return nil, "Password based auth not supported."; end function provider.user_exists(username) return nil, "Only anonymous users are supported."; -- FIXME check if anonymous user is connected? end function provider.create_user(username, password) return nil, "Account creation/modification not supported."; end function provider.get_sasl_handler() local anonymous_authentication_profile = { anonymous = function(sasl, username, realm) return true; -- for normal usage you should always return true here end }; return new_sasl(module.host, anonymous_authentication_profile); end function provider.users() return next, hosts[module.host].sessions, nil; end -- datamanager callback to disable writes local function dm_callback(username, host, datastore, data) if host == module.host then return false; end return username, host, datastore, data; end if not module:get_option_boolean("allow_anonymous_s2s", false) then module:hook("route/remote", function (event) return false; -- Block outgoing s2s from anonymous users end, 300); end function module.load() if not allow_storage then datamanager.add_callback(dm_callback); end end function module.unload() if not allow_storage then datamanager.remove_callback(dm_callback); end end module:provides("auth", provider); prosody-13.0.1/plugins/PaxHeaders/mod_auth_insecure.lua0000644000000000000000000000011714773555365020261 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.583710502 prosody-13.0.1/plugins/mod_auth_insecure.lua0000644000175000017500000000324714773555365022466 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: ignore 212 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" }; assert(module:get_option_string("insecure_open_authentication") == "Yes please, I know what I'm doing!"); function provider.test_password(username, password) return true; end function provider.set_password(username, password) local account = datamanager.load(username, host, "accounts"); password = saslprep(password); if not password then return nil, "Password fails SASLprep."; end if account then account.updated = os.time(); account.password = password; return datamanager.store(username, host, "accounts", account); end return nil, "Account not available."; end function provider.user_exists(username) return true; end function provider.create_user(username, password) local now = os.time(); return datamanager.store(username, host, "accounts", { created = now; updated = now; password = password }); end function provider.delete_user(username) return datamanager.store(username, host, "accounts", nil); end function provider.get_sasl_handler() local getpass_authentication_profile = { plain_test = function(sasl, username, password, realm) return true, true; end }; return new_sasl(module.host, getpass_authentication_profile); end module:add_item("auth-provider", provider); prosody-13.0.1/plugins/PaxHeaders/mod_auth_internal_hashed.lua0000644000000000000000000000011714773555365021574 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.587710518 prosody-13.0.1/plugins/mod_auth_internal_hashed.lua0000644000175000017500000001451614773555365024002 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2010 Jeff Mitchell -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local max = math.max; 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 "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_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_integer("default_iteration_count", 10000, 4096); local tokenauth = module:depends("tokenauth"); -- define auth provider 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."; end if credentials.password ~= nil and string.len(credentials.password) ~= 0 then if not secure_equals(saslprep(credentials.password), password) then return nil, "Auth failed. Provided password is incorrect."; end if provider.set_password(username, credentials.password) == nil then return nil, "Auth failed. Could not set hashed password from plaintext."; else return true; end end if credentials.iteration_count == nil or credentials.salt == nil or string.len(credentials.salt) == 0 then return nil, "Auth failed. Stored salt and iteration count information is not complete."; end local valid, stored_key, server_key = get_auth_db(password, credentials.salt, credentials.iteration_count); local stored_key_hex = to_hex(stored_key); local server_key_hex = to_hex(server_key); if valid and secure_equals(stored_key_hex, credentials.stored_key) and secure_equals(server_key_hex, credentials.server_key) then return true; else return nil, "Auth failed. Invalid username, password, or password hash information."; end end function provider.set_password(username, password) log("debug", "set_password for username '%s'", username); local account = accounts:get(username); if account then account.salt = generate_uuid(); account.iteration_count = max(account.iteration_count or 0, default_iteration_count); local valid, stored_key, server_key = get_auth_db(password, account.salt, account.iteration_count); if not valid then return valid, stored_key; end local stored_key_hex = to_hex(stored_key); local server_key_hex = to_hex(server_key); account.stored_key = stored_key_hex 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 log("debug", "account not found for username '%s'", username); return nil, "Auth failed. Invalid username"; end 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, { 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); if not valid then return valid, stored_key; end local stored_key_hex = to_hex(stored_key); 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, created = now, updated = now; }); end function provider.delete_user(username) return accounts:set(username, nil); end function provider.get_sasl_handler() local testpass_authentication_profile = { 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); if not credentials then return; end if credentials.password then if provider.set_password(username, credentials.password) == nil then return nil, "Auth failed. Could not set hashed password from plaintext."; end credentials = accounts:get(username); if not credentials then return; end end local stored_key, server_key = credentials.stored_key, credentials.server_key; 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, not credentials.disabled; end; oauthbearer = tokenauth.sasl_handler(provider, "oauth2", module:shared("tokenauth/oauthbearer_config")); }; return new_sasl(host, testpass_authentication_profile); end module:provides("auth", provider); prosody-13.0.1/plugins/PaxHeaders/mod_auth_internal_plain.lua0000644000000000000000000000011714773555365021443 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.587710518 prosody-13.0.1/plugins/mod_auth_internal_plain.lua0000644000175000017500000000572114773555365023647 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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; local accounts = module:open_store("accounts"); -- define auth provider 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."; end if secure_equals(password, saslprep(credentials.password)) then return true; else return nil, "Auth failed. Invalid username or password."; end end function provider.get_password(username) log("debug", "get_password for username '%s'", username); return (accounts:get(username) or {}).password; end function provider.set_password(username, password) log("debug", "set_password for username '%s'", username); password = saslprep(password); if not password then return nil, "Password fails SASLprep."; end 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 log("debug", "account not found for username '%s'", username); return nil, "Auth failed. Invalid username"; end return true; 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, { 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; created = now, updated = now; }); end function provider.delete_user(username) return accounts:set(username, nil); end function provider.get_sasl_handler() local getpass_authentication_profile = { plain = function(_, username, realm) local password = usermanager.get_password(username, realm); if not password then return "", nil; end return password, true; end }; return new_sasl(host, getpass_authentication_profile); end module:provides("auth", provider); prosody-13.0.1/plugins/PaxHeaders/mod_auth_ldap.lua0000644000000000000000000000011714773555365017364 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.587710518 prosody-13.0.1/plugins/mod_auth_ldap.lua0000644000175000017500000001066314773555365021571 0ustar00prosodyprosody00000000000000-- mod_auth_ldap local new_sasl = require "prosody.util.sasl".new; local lualdap = require "lualdap"; local function ldap_filter_escape(s) return (s:gsub("[*()\\%z]", function(c) return ("\\%02x"):format(c:byte()) end)); end -- Config options 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_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_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 function ldap_do_once(method, ...) if ld == nil then local err; ld, err = lualdap.open_simple(ldap_server, ldap_rootdn, ldap_password, ldap_tls); if not ld then return nil, err, "reconnect"; end end -- luacheck: ignore 411/success local success, iterator, invariant, initial = pcall(ld[method], ld, ...); if not success then ld = nil; return nil, iterator, "search"; end local success, dn, attr = pcall(iterator, invariant, initial); if not success then ld = nil; return success, dn, "iter"; end return dn, attr, "return"; end function ldap_do(method, retry_count, ...) local dn, attr, where; for _=1,1+retry_count do dn, attr, where = ldap_do_once(method, ...); if dn or not(attr) then break; end -- nothing or something found module:log("warn", "LDAP: %s %s (in %s)", tostring(dn), tostring(attr), where); -- otherwise retry end if not dn and attr then module:log("error", "LDAP: %s", tostring(attr)); end return dn, attr; end function get_user(username) module:log("debug", "get_user(%q)", username); return ldap_do("search", 2, { base = ldap_base; scope = ldap_scope; sizelimit = 1; filter = ldap_filter:gsub("%$(%a+)", { user = ldap_filter_escape(username); host = host; }); }); end local provider = {}; function provider.create_user(username, password) -- luacheck: ignore 212 return nil, "Account creation not available with LDAP."; end function provider.user_exists(username) return not not get_user(username); end function provider.set_password(username, password) local dn, attr = get_user(username); if not dn then return nil, attr end if attr.userPassword == password then return true end return ldap_do("modify", 2, dn, { '=', userPassword = password }); end if ldap_mode == "getpasswd" then function provider.get_password(username) local dn, attr = get_user(username); if dn and attr then return attr.userPassword; end end function provider.test_password(username, password) return provider.get_password(username) == password; end function provider.get_sasl_handler() return new_sasl(module.host, { plain = function(sasl, username) -- luacheck: ignore 212/sasl local password = provider.get_password(username); if not password then return "", nil; end return password, true; end }); end elseif ldap_mode == "bind" then local function test_password(userdn, password) local ok, err = lualdap.open_simple(ldap_server, userdn, password, ldap_tls); if not ok then module:log("debug", "ldap open_simple error: %s", err); end return not not ok; end function provider.test_password(username, password) local dn = get_user(username); if not dn then return end return test_password(dn, password) end function provider.get_sasl_handler() return new_sasl(module.host, { plain_test = function(sasl, username, password) -- luacheck: ignore 212/sasl return provider.test_password(username, password), true; end }); end else module:log("error", "Unsupported ldap_mode %s", tostring(ldap_mode)); end module:provides("auth", provider); prosody-13.0.1/plugins/PaxHeaders/mod_authz_internal.lua0000644000000000000000000000011714773555365020452 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.587710518 prosody-13.0.1/plugins/mod_authz_internal.lua0000644000175000017500000002536414773555365022663 0ustar00prosodyprosody00000000000000local 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 host_suffix = module:get_option_string("parent_host", (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", "prosody:guest"); public_user_role = module:get_option_string("public_user_role", "prosody:guest"); 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 -- 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 user an unknown role: "..tostring(role_name)); end local keys_update = { _default = role_name; -- Primary role cannot be secondary role [role_name] = role_map_store.remove; }; if role_name == default_user_role then -- Don't store default keys_update._default = role_map_store.remove; end local ok, err = role_map_store:set_keys(user, keys_update); if not ok then return nil, err; end return role; end function add_user_secondary_role(user, role_name) local role = role_registry[role_name]; if not role then return error("Cannot assign user an unknown role: "..tostring(role_name)); end local ok, err = role_map_store:set(user, role_name, true); if not ok then return nil, err; end return role; end function remove_user_secondary_role(user, role_name) return role_map_store:set(user, role_name, nil); end function get_user_secondary_roles(user) local stored_roles, err = role_store:get(user); if not stored_roles then if err then -- Unable to fetch role, fail return nil, err; end -- No role set return {}; end stored_roles._default = nil; for role_name in pairs(stored_roles) do stored_roles[role_name] = role_registry[role_name]; end return stored_roles; end function user_can_assume_role(user, role_name) local primary_role = get_user_role(user); if primary_role and primary_role.name == role_name then return true; end local secondary_roles = get_user_secondary_roles(user); if secondary_roles and secondary_roles[role_name] then return true; end return false; end -- This function is *expensive* function get_users_with_role(role_name) local function role_filter(username, default_role) --luacheck: ignore 212/username return default_role == role_name; end local primary_role_users = set.new(it.to_array(it.filter(role_filter, pairs(role_map_store:get_all("_default") or {})))); local secondary_role_users = set.new(it.to_array(it.keys(role_map_store:get_all(role_name) or {}))); local config_set; if role_name == "prosody:admin" then config_set = config_admin_jids; elseif role_name == "prosody:operator" then config_set = config_global_admin_jids; end if config_set then local config_admin_users = config_set / function (admin_jid) local j_node, j_host = jid_split(admin_jid); if j_host == host then return j_node; end end; return it.to_array(config_admin_users + primary_role_users + secondary_role_users); end return it.to_array(primary_role_users + secondary_role_users); end 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_role(jid, role_name) -- luacheck: ignore 212 return false, "not-implemented"; end function get_jids_with_role(role_name) -- Fetch role users from storage local storage_role_jids = array.map(get_users_with_role(role_name), function (username) return username.."@"..host; end); if role_name == "prosody:admin" then return it.to_array(config_admin_jids + set.new(storage_role_jids)); elseif role_name == "prosody:operator" then return it.to_array(config_global_admin_jids + set.new(storage_role_jids)); end return storage_role_jids; end function add_default_permission(role_name, action, policy) local role = role_registry[role_name]; if not role then module:log("warn", "Attempt to add default permission for unknown role: %s", role_name); return nil, "no-such-role"; end if policy == nil then policy = true; end module:log("debug", "Adding policy %s for permission %s on role %s", policy, action, role_name); return role:set_permission(action, policy); end function get_role_by_name(role_name) return assert(role_registry[role_name], role_name); end function get_all_roles() return role_registry; end -- COMPAT: Migrate from 0.12 role storage local function do_migration(migrate_host) local old_role_store = assert(module:context(migrate_host):open_store("roles")); local new_role_store = assert(module:context(migrate_host):open_store("account_roles")); local migrated, failed, skipped = 0, 0, 0; -- Iterate all users for username in assert(old_role_store:users()) do local old_roles = it.to_array(it.filter(function (k) return k:sub(1,1) ~= "_"; end, it.keys(old_role_store:get(username)))); if #old_roles == 1 then local ok, err = new_role_store:set(username, { _default = old_roles[1]; }); if ok then migrated = migrated + 1; else failed = failed + 1; print("EE: Failed to store new role info for '"..username.."': "..err); end else print("WW: User '"..username.."' has multiple roles and cannot be automatically migrated"); skipped = skipped + 1; end end return migrated, failed, skipped; end function module.command(arg) if arg[1] == "migrate" then table.remove(arg, 1); local migrate_host = arg[1]; if not migrate_host or not prosody.hosts[migrate_host] then print("EE: Please supply a valid host to migrate to the new role storage"); return 1; end -- Initialize storage layer require "prosody.core.storagemanager".initialize_host(migrate_host); print("II: Migrating roles..."); local migrated, failed, skipped = do_migration(migrate_host); print(("II: %d migrated, %d failed, %d skipped"):format(migrated, failed, skipped)); return (failed + skipped == 0) and 0 or 1; else print("EE: Unknown command: "..(arg[1] or "")); print(" Hint: try 'migrate'?"); end end prosody-13.0.1/plugins/PaxHeaders/mod_blocklist.lua0000644000000000000000000000011714773555365017411 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.591710534 prosody-13.0.1/plugins/mod_blocklist.lua0000644000175000017500000002703714773555365021621 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2009-2010 Matthew Wild -- Copyright (C) 2009-2010 Waqas Hussain -- Copyright (C) 2014-2015 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- This module implements XEP-0191: Blocking Command -- 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"prosody.util.stanza"; local st_error_reply = st.error_reply; 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; -- 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_integer("blocklist_cache_size", 256, 1); local blocklist_cache = require"prosody.util.cache".new(cache_size); local null_blocklist = {}; module:add_feature("urn:xmpp:blocking"); local function set_blocklist(username, blocklist) local ok, err = storage:set(username, blocklist); if not ok then return ok, err; end -- Successful save, update the cache 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 local default_list = legacy_data.lists[legacy_data.default]; if not default_list or not default_list.items then return; end local migrated_data = { [false] = { created = os.time(); migrated = "privacy" }}; module:log("info", "Migrating blocklist from mod_privacy storage for user '%s'", username); for _, item in ipairs(default_list.items) do if item.type == "jid" and item.action == "deny" then local jid = jid_prep(item.value); if not jid then module:log("warn", "Invalid JID in privacy store for user '%s' not migrated: %s", username, item.value); else migrated_data[jid] = true; end end end set_blocklist(username, migrated_data); 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 = blocklist_cache:get(username); if not blocklist then if not user_exists(username, module.host) then return null_blocklist; end blocklist = storage:get(username); if not blocklist then blocklist = migrate_privacy_list(username); end if not blocklist then blocklist = { [false] = { created = os.time(); }; }; end blocklist_cache:set(username, blocklist); end return blocklist; end 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 = get_blocklist(username); for jid in pairs(blocklist) do if jid then reply:tag("item", { jid = jid }):up(); end end origin.interested_blocklist = true; -- Gets notified about changes origin.send(reply); return true; end, -1); -- Add or remove some jid(s) from the blocklist -- We want this to be atomic and not do a partial update local function edit_blocklist(event) local now = os.time(); local origin, stanza = event.origin, event.stanza; local username = origin.username; local action = stanza.tags[1]; -- "block" or "unblock" local is_blocking = action.name == "block" and now or nil; -- nil if unblocking local new = {}; -- JIDs to block depending or unblock on action -- XEP-0191 sayeth: -- > When the user blocks communications with the contact, the user's -- > server MUST send unavailable presence information to the contact (but -- > only if the contact is allowed to receive presence notifications [...] -- So contacts we need to do that for are added to the set below. local send_unavailable = is_blocking and {}; local send_available = not is_blocking and {}; -- Because blocking someone currently also blocks the ability to reject -- subscription requests, we'll preemptively reject such local remove_pending = is_blocking and {}; for item in action:childtags("item") do local jid = jid_prep(item.attr.jid); if not jid then origin.send(st_error_reply(stanza, "modify", "jid-malformed")); return true; end item.attr.jid = jid; -- echo back prepped new[jid] = true; if is_blocking then if is_contact_subscribed(username, module.host, jid) then send_unavailable[jid] = true; elseif is_contact_pending_in(username, module.host, jid) then remove_pending[jid] = true; end elseif is_contact_subscribed(username, module.host, jid) then send_available[jid] = true; end end if is_blocking and not next(new) then -- element does not contain at least one child element origin.send(st_error_reply(stanza, "modify", "bad-request")); return true; end local blocklist = get_blocklist(username); local new_blocklist = { -- We set the [false] key to something as a signal not to migrate privacy lists [false] = blocklist[false] or { created = now; }; }; if type(blocklist[false]) == "table" then new_blocklist[false].modified = now; end if is_blocking or next(new) then for jid, t in pairs(blocklist) do if jid then new_blocklist[jid] = t; end end for jid in pairs(new) do new_blocklist[jid] = is_blocking; end -- else empty the blocklist end local ok, err = set_blocklist(username, new_blocklist); if ok then origin.send(st.reply(stanza)); else origin.send(st_error_reply(stanza, "wait", "internal-server-error", err)); return true; end if is_blocking then for jid in pairs(send_unavailable) do -- Check that this JID isn't already blocked, i.e. this is not a change if not blocklist[jid] then for _, session in pairs(sessions[username].sessions) do if session.presence then module:send(st.presence({ type = "unavailable", to = jid, from = session.full_jid })); end end end end if next(remove_pending) then local roster = load_roster(username, module.host); for jid in pairs(remove_pending) do roster[false].pending[jid] = nil; end save_roster(username, module.host, roster); -- Not much we can do about save failing here end else local user_bare = username .. "@" .. module.host; for jid in pairs(send_available) do module:send(st.presence({ type = "probe", to = user_bare, from = jid })); end end local blocklist_push = st.iq({ type = "set", id = "blocklist-push" }) :add_child(action); -- I am lazy for _, session in pairs(sessions[username].sessions) do if session.interested_blocklist then blocklist_push.attr.to = session.full_jid; session.send(blocklist_push); end end return true; end module:hook("iq-set/self/urn:xmpp:blocking:block", edit_blocklist, -1); 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 blocklist_cache:set(event.username, nil); end end); -- Buggy clients module:hook("iq-error/self/blocklist-push", function (event) local origin, stanza = event.origin, event.stanza; local _, condition, text = stanza:get_error(); local log = (origin.log or module._log); log("warn", "Client returned an error in response to notification from mod_%s: %s%s%s", module.name, condition, text and ": " or "", text or ""); return true; end); local function is_blocked(user, jid) 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]; end -- Event handlers for bouncing or dropping stanzas local function drop_stanza(event) local stanza = event.stanza; local attr = stanza.attr; local to, from = attr.to, attr.from; to = to and jid_split(to); if to and from then 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 local function bounce_stanza(event) local origin, stanza = event.origin, event.stanza; if drop_stanza(event) then origin.send(st_error_reply(stanza, "cancel", "service-unavailable")); return true; end end local function bounce_iq(event) local type = event.stanza.attr.type; if type == "set" or type == "get" then return bounce_stanza(event); end return drop_stanza(event); -- result or error end local function bounce_message(event) local stanza = event.stanza; local type = stanza.attr.type; if type == "chat" or not type or type == "normal" then if full_sessions[stanza.attr.to] then -- See #690 return drop_stanza(event); end return bounce_stanza(event); end return drop_stanza(event); -- drop headlines, groupchats etc end local function drop_outgoing(event) local origin, stanza = event.origin, event.stanza; local username = origin.username or jid_split(stanza.attr.from); if not username then return end local to = stanza.attr.to; if to then return is_blocked(username, to); end -- nil 'to' means a self event, don't bock those end local function bounce_outgoing(event) local origin, stanza = event.origin, event.stanza; local type = stanza.attr.type; if type == "error" or stanza.name == "iq" and type == "result" then return drop_outgoing(event); end if drop_outgoing(event) then origin.send(st_error_reply(stanza, "cancel", "not-acceptable", "You have blocked this JID") :tag("blocked", { xmlns = "urn:xmpp:blocking:errors" })); return true; end end -- Hook all the events! local prio_in, prio_out = 100, 100; module:hook("presence/bare", drop_stanza, prio_in); module:hook("presence/full", drop_stanza, prio_in); if module:get_option_boolean("bounce_blocked_messages", false) then module:hook("message/bare", bounce_message, prio_in); module:hook("message/full", bounce_message, prio_in); else module:hook("message/bare", drop_stanza, prio_in); module:hook("message/full", drop_stanza, prio_in); end module:hook("iq/bare", bounce_iq, prio_in); module:hook("iq/full", bounce_iq, prio_in); module:hook("pre-message/bare", bounce_outgoing, prio_out); module:hook("pre-message/full", bounce_outgoing, prio_out); module:hook("pre-message/host", bounce_outgoing, prio_out); module:hook("pre-presence/bare", bounce_outgoing, -1); module:hook("pre-presence/host", bounce_outgoing, -1); module:hook("pre-presence/full", bounce_outgoing, prio_out); module:hook("pre-iq/bare", bounce_outgoing, prio_out); module:hook("pre-iq/full", bounce_outgoing, prio_out); module:hook("pre-iq/host", bounce_outgoing, prio_out); prosody-13.0.1/plugins/PaxHeaders/mod_bookmarks.lua0000644000000000000000000000011714773555365017413 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.591710534 prosody-13.0.1/plugins/mod_bookmarks.lua0000644000175000017500000004222314773555365021615 0ustar00prosodyprosody00000000000000local 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 "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"); local namespace = "urn:xmpp:bookmarks:1"; local namespace_old = "urn:xmpp:bookmarks:0"; local namespace_private = "jabber:iq:private"; local namespace_legacy = "storage:bookmarks"; local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; local default_options = { ["persist_items"] = true; ["max_items"] = "max"; ["send_last_published_item"] = "never"; ["access_model"] = "whitelist"; }; module:hook("account-disco-info", function (event) -- This Time it’s Serious! event.reply:tag("feature", { var = namespace.."#compat" }):up(); event.reply:tag("feature", { var = namespace.."#compat-pep" }):up(); -- COMPAT XEP-0411 event.reply:tag("feature", { var = "urn:xmpp:bookmarks-conversion:0" }):up(); end); -- This must be declared on the domain JID, not the account JID. Note that -- this isn’t defined in the XEP. module:add_feature(namespace_private); local function generate_legacy_storage(items) local storage = st.stanza("storage", { xmlns = namespace_legacy }); for _, item_id in ipairs(items) do local item = items[item_id]; local bookmark = item:get_child("conference", namespace); if not bookmark then module:log("warn", "Invalid bookmark published: expected {%s}conference, got {%s}%s", namespace, item.tags[1] and item.tags[1].attr.xmlns, item.tags[1] and item.tags[1].name); end local conference = st.stanza("conference", { jid = item.attr.id, name = bookmark and bookmark.attr.name, autojoin = bookmark and bookmark.attr.autojoin, }); local nick = bookmark and bookmark:get_child_text("nick"); if nick ~= nil then conference:text_tag("nick", nick):up(); end local password = bookmark and bookmark:get_child_text("password"); if password ~= nil then conference:text_tag("password", password):up(); end storage:add_child(conference); end return storage; end local function on_retrieve_legacy_pep(event) local stanza, session = event.stanza, event.origin; local pubsub = stanza:get_child("pubsub", "http://jabber.org/protocol/pubsub"); if pubsub == nil then return; end local items = pubsub:get_child("items"); if items == nil then return; end local node = items.attr.node; if node ~= namespace_legacy then return; end local username = session.username; local jid = username.."@"..session.host; local service = mod_pep.get_pep_service(username); local ok, ret = service:get_items(namespace, session.full_jid); if not ok then if ret == "item-not-found" then module:log("debug", "Got no PEP bookmarks item for %s, returning empty private bookmarks", jid); else module:log("error", "Failed to retrieve PEP bookmarks of %s: %s", jid, ret); end session.send(st.error_reply(stanza, "cancel", ret, "Failed to retrieve bookmarks from PEP")); return true; end local storage = generate_legacy_storage(ret); module:log("debug", "Sending back legacy PEP for %s: %s", jid, storage); session.send(st.reply(stanza) :tag("pubsub", {xmlns = "http://jabber.org/protocol/pubsub"}) :tag("items", {node = namespace_legacy}) :tag("item", {id = "current"}) :add_child(storage)); return true; end local function on_retrieve_private_xml(event) local stanza, session = event.stanza, event.origin; local query = stanza:get_child("query", namespace_private); if query == nil then return; end local bookmarks = query:get_child("storage", namespace_legacy); if bookmarks == nil then return; end module:log("debug", "Getting private bookmarks: %s", bookmarks); local username = session.username; local jid = username.."@"..session.host; local service = mod_pep.get_pep_service(username); local ok, ret = service:get_items(namespace, session.full_jid); if not ok then if ret == "item-not-found" then module:log("debug", "Got no PEP bookmarks item for %s, returning empty private bookmarks", jid); session.send(st.reply(stanza):add_child(query)); else module:log("error", "Failed to retrieve PEP bookmarks of %s: %s", jid, ret); session.send(st.error_reply(stanza, "cancel", ret, "Failed to retrieve bookmarks from PEP")); end return true; end local storage = generate_legacy_storage(ret); module:log("debug", "Sending back private for %s: %s", jid, storage); session.send(st.reply(stanza):query(namespace_private):add_child(storage)); return true; end local function compare_bookmark2(a, b) if a == nil or b == nil then return false; end local a_conference = a:get_child("conference", namespace); local b_conference = b:get_child("conference", namespace); local a_nick = a_conference:get_child_text("nick"); local b_nick = b_conference:get_child_text("nick"); local a_password = a_conference:get_child_text("password"); local b_password = b_conference:get_child_text("password"); return (a.attr.id == b.attr.id and a_conference.attr.name == b_conference.attr.name and a_conference.attr.autojoin == b_conference.attr.autojoin and a_nick == b_nick and a_password == b_password); end local function publish_to_pep(jid, bookmarks, synchronise) local service = mod_pep.get_pep_service(jid_split(jid)); if #bookmarks.tags == 0 then if synchronise then -- If we set zero legacy bookmarks, purge the bookmarks 2 node. module:log("debug", "No bookmark in the set, purging instead."); local ok, err = service:purge(namespace, jid, true); -- It's okay if no node exists when purging, user has -- no bookmarks anyway. if not ok and err ~= "item-not-found" then module:log("error", "Failed to clear items from bookmarks 2 node: %s", err); return ok, err; end end return true; end -- Retrieve the current bookmarks2. module:log("debug", "Retrieving the current bookmarks 2."); local has_bookmarks2, ret = service:get_items(namespace, jid); local bookmarks2; if not has_bookmarks2 and ret == "item-not-found" then module:log("debug", "Got item-not-found, assuming it was empty until now, creating."); local ok, err = service:create(namespace, jid, default_options); if not ok then module:log("error", "Creating bookmarks 2 node failed: %s", err); return ok, err; end bookmarks2 = {}; elseif not has_bookmarks2 then module:log("debug", "Got %s error, aborting.", ret); return false, ret; else module:log("debug", "Got existing bookmarks2."); bookmarks2 = ret; local ok, err = service:get_node_config(namespace, jid); if not ok then module:log("error", "Retrieving bookmarks 2 node config failed: %s", err); return ok, err; end local options = err; for key, value in pairs(default_options) do if options[key] and options[key] ~= value then module:log("warn", "Overriding bookmarks 2 configuration for %s, from %s to %s", jid, options[key], value); options[key] = value; end end local ok, err = service:set_node_config(namespace, jid, options); if not ok then module:log("error", "Setting bookmarks 2 node config failed: %s", err); return ok, err; end end -- Get a list of all items we may want to remove. local to_remove = {}; for i in ipairs(bookmarks2) do to_remove[bookmarks2[i]] = true; end for bookmark in bookmarks:childtags("conference", namespace_legacy) do -- Create the new conference element by copying everything from the legacy one. local conference = st.stanza("conference", { xmlns = namespace, name = bookmark.attr.name, autojoin = bookmark.attr.autojoin, }); local nick = bookmark:get_child_text("nick"); if nick ~= nil then conference:text_tag("nick", nick):up(); end local password = bookmark:get_child_text("password"); if password ~= nil then conference:text_tag("password", password):up(); end -- Create its wrapper. local item = st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = bookmark.attr.jid }) :add_child(conference); -- Then publish it only if it’s a new one or updating a previous one. if compare_bookmark2(item, bookmarks2[bookmark.attr.jid]) then module:log("debug", "Item %s identical to the previous one, skipping.", item.attr.id); to_remove[bookmark.attr.jid] = nil; else if bookmarks2[bookmark.attr.jid] == nil then module:log("debug", "Item %s not existing previously, publishing.", item.attr.id); else module:log("debug", "Item %s different from the previous one, publishing.", item.attr.id); to_remove[bookmark.attr.jid] = nil; end local ok, err = service:publish(namespace, jid, bookmark.attr.jid, item, default_options); if not ok then module:log("error", "Publishing item %s failed: %s", item.attr.id, err); return ok, err; end end end -- Now handle retracting items that have been removed. if synchronise then for id in pairs(to_remove) do module:log("debug", "Item %s removed from bookmarks.", id); local ok, err = service:retract(namespace, jid, id, st.stanza("retract", { id = id })); if not ok then module:log("error", "Retracting item %s failed: %s", id, err); return ok, err; end end end return true; end -- Synchronise legacy PEP to PEP. local function on_publish_legacy_pep(event) local stanza, session = event.stanza, event.origin; local pubsub = stanza:get_child("pubsub", "http://jabber.org/protocol/pubsub"); if pubsub == nil then return; end local publish = pubsub:get_child("publish"); if publish == nil then return end if publish.attr.node == namespace_old then session.send(st.error_reply(stanza, "modify", "not-allowed", "Your client does XEP-0402 version 0.3.0 but 0.4.0+ is required")); return true; end if publish.attr.node ~= namespace_legacy then return; end local item = publish:get_child("item"); if item == nil then return; end -- Here we ignore the item id, it’ll be generated as 'current' anyway. local bookmarks = item:get_child("storage", namespace_legacy); if bookmarks == nil then return; end -- We also ignore the publish-options. module:log("debug", "Legacy PEP bookmarks set by client, publishing to PEP."); local ok, err = publish_to_pep(session.full_jid, bookmarks, true); if not ok then module:log("error", "Failed to sync legacy bookmarks to PEP for %s@%s: %s", session.username, session.host, err); session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP")); return true; end session.send(st.reply(stanza)); return true; end -- Synchronise Private XML to PEP. local function on_publish_private_xml(event) local stanza, session = event.stanza, event.origin; local query = stanza:get_child("query", namespace_private); if query == nil then return; end local bookmarks = query:get_child("storage", namespace_legacy); if bookmarks == nil then return; end module:log("debug", "Private bookmarks set by client, publishing to PEP."); local ok, err = publish_to_pep(session.full_jid, bookmarks, true); if not ok then module:log("error", "Failed to sync private XML bookmarks to PEP for %s@%s: %s", session.username, session.host, err); session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP")); return true; end session.send(st.reply(stanza)); return true; end local function migrate_legacy_bookmarks(event) local session = event.session; local username = session.username; local service = mod_pep.get_pep_service(username); local jid = username.."@"..session.host; local ok, ret = service:get_items(namespace_legacy, session.full_jid); if ok and ret[1] then module:log("debug", "Legacy PEP bookmarks found for %s, migrating.", jid); local failed = false; for _, item_id in ipairs(ret) do local item = ret[item_id]; if item.attr.id ~= "current" then module:log("warn", "Legacy PEP bookmarks for %s isn’t using 'current' as its id: %s", jid, item.attr.id); end local bookmarks = item:get_child("storage", namespace_legacy); module:log("debug", "Got legacy PEP bookmarks of %s: %s", jid, bookmarks); local ok, err = publish_to_pep(session.full_jid, bookmarks, false); if not ok then module:log("error", "Failed to store legacy PEP bookmarks to bookmarks 2 for %s, aborting migration: %s", jid, err); failed = true; break; end end if not failed then module:log("debug", "Successfully migrated legacy PEP bookmarks of %s to bookmarks 2, clearing items.", jid); local ok, err = service:purge(namespace_legacy, jid, false); if not ok then module:log("error", "Failed to delete legacy PEP bookmarks for %s: %s", jid, err); end end end local ok, current_legacy_config = service:get_node_config(namespace_legacy, jid); if not ok or current_legacy_config["access_model"] ~= "whitelist" then -- The legacy node must exist in order for the access model to apply to the -- XEP-0411 COMPAT broadcasts (which bypass the pubsub service entirely), -- so create or reconfigure it to be useless. -- -- FIXME It would be handy to have a publish model that prevents the owner -- from publishing, but the affiliation takes priority local config = { ["persist_items"] = false; ["max_items"] = 1; ["send_last_published_item"] = "never"; ["access_model"] = "whitelist"; }; local ok, err; if ret == "item-not-found" then ok, err = service:create(namespace_legacy, jid, config); else ok, err = service:set_node_config(namespace_legacy, jid, config); end if not ok then module:log("error", "Setting legacy bookmarks node config failed: %s", err); return ok, err; end end local data, err = private_storage:get(username, "storage:storage:bookmarks"); if not data then module:log("debug", "No existing legacy bookmarks for %s, migration already done: %s", jid, err); local ok, ret2 = service:get_items(namespace, session.full_jid); if not ok or not ret2 then module:log("debug", "Additionally, no bookmarks 2 were existing for %s, assuming empty.", jid); module:fire_event("bookmarks/empty", { session = session }); end return; end local bookmarks = st.deserialize(data); module:log("debug", "Got legacy bookmarks of %s: %s", jid, bookmarks); module:log("debug", "Going to store legacy bookmarks to bookmarks 2 %s.", jid); local ok, err = publish_to_pep(session.full_jid, bookmarks, false); if not ok then module:log("error", "Failed to store legacy bookmarks to bookmarks 2 for %s, aborting migration: %s", jid, err); return; end module:log("debug", "Stored legacy bookmarks to bookmarks 2 for %s.", jid); local ok, err = private_storage:set(username, "storage:storage:bookmarks", nil); if not ok then module:log("error", "Failed to remove legacy bookmarks of %s: %s", jid, err); return; end module:log("debug", "Removed legacy bookmarks of %s, migration done!", jid); end module:hook("iq/bare/jabber:iq:private:query", function (event) if event.stanza.attr.type == "get" then return on_retrieve_private_xml(event); else return on_publish_private_xml(event); end end, 1); module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function (event) if event.stanza.attr.type == "get" then return on_retrieve_legacy_pep(event); else return on_publish_legacy_pep(event); end end, 1); if module:get_option_boolean("upgrade_legacy_bookmarks", true) then module:hook("resource-bind", migrate_legacy_bookmarks); end -- COMPAT XEP-0411 Broadcast as per XEP-0048 + PEP local function legacy_broadcast(event) local service = event.service; local ok, bookmarks = service:get_items(namespace, event.actor); if bookmarks == "item-not-found" then ok, bookmarks = true, {} end if not ok then return end local legacy_bookmarks_item = st.stanza("item", { xmlns = xmlns_pubsub; id = "current" }) :add_child(generate_legacy_storage(bookmarks)); service:broadcast("items", namespace_legacy, { --[[ no subscribers ]] }, legacy_bookmarks_item, event.actor); end local function broadcast_legacy_removal(event) if event.node ~= namespace then return end return legacy_broadcast(event); end module:hook("presence/initial", function (event) -- Broadcasts to all clients with legacy+notify, not just the one coming online. -- Upgrade to XEP-0402 to avoid it local service = mod_pep.get_pep_service(event.origin.username); legacy_broadcast({ service = service, actor = event.origin.full_jid }); end); module:handle_items("pep-service", function (event) local service = event.item.service; module:hook_object_event(service.events, "item-published/" .. namespace, legacy_broadcast); module:hook_object_event(service.events, "item-retracted", broadcast_legacy_removal); module:hook_object_event(service.events, "node-purged", broadcast_legacy_removal); module:hook_object_event(service.events, "node-deleted", broadcast_legacy_removal); end, function (event) local service = event.item.service; module:unhook_object_event(service.events, "item-published/" .. namespace, legacy_broadcast); module:unhook_object_event(service.events, "item-retracted", broadcast_legacy_removal); module:unhook_object_event(service.events, "node-purged", broadcast_legacy_removal); module:unhook_object_event(service.events, "node-deleted", broadcast_legacy_removal); end, true); prosody-13.0.1/plugins/PaxHeaders/mod_bosh.lua0000644000000000000000000000011714773555365016356 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.591710534 prosody-13.0.1/plugins/mod_bosh.lua0000644000175000017500000005010014773555365020551 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- module:set_global(); 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 "prosody.util.uuid".generate; local core_process_stanza = prosody.core_process_stanza; local st = require "prosody.util.stanza"; local logger = require "prosody.util.logger"; local log = module._log; 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"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"; local xmlns_bosh = "http://jabber.org/protocol/httpbind"; -- (hard-coded into a literal in session.send) local stream_callbacks = { stream_ns = xmlns_bosh, stream_tag = "body", default_ns = "jabber:client" }; -- These constants are implicitly assumed within the code, and cannot be changed 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_period("bosh_max_inactivity", 60); -- The minimum amount of time between requests with no payload 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_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_integer("c2s_stanza_size_limit", 1024*256, 10000); if cross_domain ~= nil then module:log("info", "The 'cross_domain_bosh' option has been deprecated"); end local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; -- All sessions, and sessions that have no requests open local sessions = module:shared("sessions"); local measure_active = module:measure("active_sessions", "amount"); local measure_inactive = module:measure("inactive_sessions", "amount"); local report_bad_host = module:measure("bad_host", "rate"); local report_bad_sid = module:measure("bad_sid", "rate"); local report_new_sid = module:measure("new_sid", "rate"); local report_timeout = module:measure("timeout", "rate"); module:hook("stats-update", function () local active = 0; local inactive = 0; for _, session in pairs(sessions) do if #session.requests > 0 then active = active + 1; else inactive = inactive + 1; end end measure_active(active); measure_inactive(inactive); end); -- Used to respond to idle sessions (those with waiting requests) function on_destroy_request(request) log("debug", "Request destroyed: %s", request); local session = sessions[request.context.sid]; if session then local requests = session.requests; for i, r in ipairs(requests) do if r == request then t_remove(requests, i); break; end end -- If this session now has no requests open, mark it as inactive local max_inactive = session.bosh_max_inactive; if max_inactive and #requests == 0 then if session.inactive_timer then session.inactive_timer:stop(); end session.inactive_timer = module:add_timer(max_inactive, session_timeout, session, request.context, "BOSH client silent for over "..max_inactive.." seconds"); (session.log or log)("debug", "BOSH session marked as inactive (for %ds)", max_inactive); end if session.bosh_wait_timer then session.bosh_wait_timer:stop(); session.bosh_wait_timer = nil; end end end function session_timeout(now, session, context, reason) -- luacheck: ignore 212/now if not session.destroyed then report_timeout(); sessions[context.sid] = nil; sm_destroy_session(session, reason); end end function handle_POST(event) log("debug", "Handling new request %s: %s\n----------", event.request, event.request.body); local request, response = event.request, event.response; response.on_destroy = on_destroy_request; local body = request.body; local context = { request = request, response = response, notopen = true }; local stream = new_xmpp_stream(context, stream_callbacks, stanza_size_limit); response.context = context; local headers = response.headers; headers.content_type = "text/xml; charset=utf-8"; -- stream:feed() calls the stream_callbacks, so all stanzas in -- the body are processed in this next line before it returns. -- In particular, the streamopened() stream callback is where -- much of the session logic happens, because it's where we first -- get to see the 'sid' of this request. local ok, err = stream:feed(body); if not ok then module:log("warn", "Error parsing BOSH payload; %s", err) local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams, condition = "bad-request" }); return tostring(close_reply); end -- Stanzas (if any) in the request have now been processed, and -- we take care of the high-level BOSH logic here, including -- giving a response or putting the request "on hold". local session = sessions[context.sid]; if session then -- Session was marked as inactive, since we have -- a request open now, unmark it if session.inactive_timer and #session.requests > 0 then session.inactive_timer:stop(); session.inactive_timer = nil; end if session.bosh_wait_timer then session.bosh_wait_timer:stop(); session.bosh_wait_timer = nil; end local r = session.requests; log("debug", "Session %s has %d out of %d requests open", context.sid, #r, BOSH_HOLD); log("debug", "and there are %d things in the send_buffer:", #session.send_buffer); if #r > BOSH_HOLD then -- We are holding too many requests, send what's in the buffer, log("debug", "We are holding too many requests, so..."); if #session.send_buffer > 0 then log("debug", "...sending what is in the buffer") session.send(t_concat(session.send_buffer)); session.send_buffer = {}; else -- or an empty response log("debug", "...sending an empty response"); session.send(""); end elseif #session.send_buffer > 0 then log("debug", "Session has data in the send buffer, will send now.."); local resp = t_concat(session.send_buffer); session.send_buffer = {}; session.send(resp); end if not response.finished then -- We're keeping this request open, to respond later log("debug", "Have nothing to say, so leaving request unanswered for now"); end if session.bosh_terminate then session.log("debug", "Closing session with %d requests open", #session.requests); session:close(); return nil; else if session.bosh_wait and #session.requests > 0 then session.bosh_wait_timer = module:add_timer(session.bosh_wait, after_bosh_wait, session.requests[1], session) end return true; -- Inform http server we shall reply later end elseif response.finished or context.ignore_request then if response.finished then module:log("debug", "Response finished"); end if context.ignore_request then module:log("debug", "Ignoring this request"); end -- A response has been sent already, or we're ignoring this request -- (e.g. so a different instance of the module can handle it) return; end module:log("warn", "Unable to associate request with a session (incomplete request?)"); report_bad_sid(); local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams, condition = "item-not-found" }); return tostring(close_reply) .. "\n"; end function after_bosh_wait(now, request, session) -- luacheck: ignore 212 if request.conn then session.send(""); end end local function bosh_reset_stream(session) session.notopen = true; end local stream_xmlns_attr = { xmlns = "urn:ietf:params:xml:ns:xmpp-streams" }; local function bosh_close_stream(session, reason) (session.log or log)("info", "BOSH client disconnected: %s", (reason and reason.condition or reason) or "session close"); local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams }); if reason then close_reply.attr.condition = "remote-stream-error"; if type(reason) == "string" then -- assume stream error close_reply:tag("stream:error") :tag(reason, {xmlns = xmlns_xmpp_streams}); elseif st.is_stanza(reason) then close_reply = reason; elseif type(reason) == "table" then if reason.condition then close_reply:tag("stream:error") :tag(reason.condition, stream_xmlns_attr):up(); if reason.text then close_reply:tag("text", stream_xmlns_attr):text(reason.text):up(); end if reason.extra then close_reply:add_child(reason.extra); end end end log("info", "Disconnecting client, is: %s", close_reply); end local response_body = tostring(close_reply); for _, held_request in ipairs(session.requests) do held_request:send(response_body); end sessions[session.sid] = nil; sm_destroy_session(session); end local runner_callbacks = { }; -- Handle the tag in the request payload. function stream_callbacks.streamopened(context, attr) local request, response = context.request, context.response; local sid, rid = attr.sid, tonumber(attr.rid); log("debug", "BOSH body open (sid: %s)", sid or ""); context.rid = rid; if not sid then -- New session request context.notopen = nil; -- Signals that we accept this opening tag if not attr.to then log("debug", "BOSH client tried to connect without specifying a host"); report_bad_host(); local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams, condition = "improper-addressing" }); response:send(tostring(close_reply)); return; end local to_host = nameprep(attr.to); if not to_host then log("debug", "BOSH client tried to connect to invalid host: %s", attr.to); report_bad_host(); local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams, condition = "improper-addressing" }); response:send(tostring(close_reply)); return; end if not prosody.hosts[to_host] then log("debug", "BOSH client tried to connect to non-existent host: %s", attr.to); report_bad_host(); local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams, condition = "improper-addressing" }); response:send(tostring(close_reply)); return; end if prosody.hosts[to_host].type ~= "local" then log("debug", "BOSH client tried to connect to %s host: %s", prosody.hosts[to_host].type, attr.to); report_bad_host(); local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams, condition = "improper-addressing" }); response:send(tostring(close_reply)); return; end local wait = tonumber(attr.wait); if not rid or (not attr.wait or not wait or wait < 0 or wait % 1 ~= 0) then log("debug", "BOSH client sent invalid rid or wait attributes: rid=%s, wait=%s", attr.rid, attr.wait); local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams, condition = "bad-request" }); response:send(tostring(close_reply)); return; end wait = math_min(wait, bosh_max_wait); -- New session sid = new_uuid(); -- TODO use util.session local session = { 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(); requests = { }, send_buffer = {}, reset_stream = bosh_reset_stream, close = bosh_close_stream, dispatch_stanza = core_process_stanza, notopen = true, log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure, ip = request.ip; }; sessions[sid] = session; session.thread = runner(function (stanza) session:dispatch_stanza(stanza); end, runner_callbacks, session); local filter = initialize_filters(session); session.log("debug", "BOSH session created for request from %s", session.ip); log("info", "New BOSH session, assigned it sid '%s'", sid); report_new_sid(); module:fire_event("bosh-session", { session = session, request = request }); -- Send creation response local creating_session = true; local r = session.requests; function session.send(s) -- We need to ensure that outgoing stanzas have the jabber:client xmlns if s.attr and not s.attr.xmlns then s = st.clone(s); s.attr.xmlns = "jabber:client"; end s = filter("stanzas/out", s); --log("debug", "Sending BOSH data: %s", s); if not s then return true end t_insert(session.send_buffer, tostring(s)); local oldest_request = r[1]; if oldest_request and not session.bosh_processing then log("debug", "We have an open request, so sending on that"); local body_attr = { xmlns = "http://jabber.org/protocol/httpbind", ["xmlns:stream"] = "http://etherx.jabber.org/streams"; type = session.bosh_terminate and "terminate" or nil; sid = sid; }; if creating_session then creating_session = nil; body_attr.requests = tostring(BOSH_MAX_REQUESTS); body_attr.hold = tostring(BOSH_HOLD); body_attr.inactivity = tostring(bosh_max_inactivity); body_attr.polling = tostring(bosh_max_polling); body_attr.wait = tostring(session.bosh_wait); body_attr.authid = sid; body_attr.secure = "true"; body_attr.ver = '1.6'; body_attr.from = session.host; body_attr["xmlns:xmpp"] = "urn:xmpp:xbosh"; body_attr["xmpp:version"] = "1.0"; end local response_xml = st.stanza("body", body_attr):top_tag()..t_concat(session.send_buffer)..""; session.bosh_responses[oldest_request.context.rid] = response_xml; oldest_request:send(response_xml); session.send_buffer = {}; end return true; end request.sid = sid; end local session = sessions[sid]; if not session then -- Unknown sid log("info", "Client tried to use sid '%s' which we don't know about", sid); report_bad_sid(); response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" }))); context.notopen = nil; return; end session.conn = request.conn; if session.rid then local diff = rid - session.rid; -- Diff should be 1 for a healthy request session.log("debug", "rid: %d, sess: %s, diff: %d", rid, session.rid, diff) if diff ~= 1 then context.sid = sid; context.notopen = nil; if diff == 2 then -- Missed a request -- Hold request, but don't process it (ouch!) session.log("debug", "rid skipped: %d, deferring this request", rid-1) context.defer = true; session.bosh_deferred = { context = context, sid = sid, rid = rid, terminate = attr.type == "terminate" }; return; end -- Set a marker to indicate that stanzas in this request should NOT be processed -- (these stanzas will already be in the XML parser's buffer) context.ignore = true; if session.bosh_responses[rid] then -- Re-send past response, ignore stanzas in this request session.log("debug", "rid repeated within window, replaying old response"); response:send(session.bosh_responses[rid]); return; elseif diff == 0 then session.log("debug", "current rid repeated, ignoring stanzas"); t_insert(session.requests, response); context.sid = sid; return; end -- Session broken, destroy it session.log("debug", "rid out of range: %d (diff %d)", rid, diff); response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" }))); return; end session.rid = rid; end if attr.type == "terminate" then -- Client wants to end this session, which we'll do -- after processing any stanzas in this request session.bosh_terminate = true; end context.notopen = nil; -- Signals that we accept this opening tag t_insert(session.requests, response); context.sid = sid; session.bosh_processing = true; -- Used to suppress replies until processing of this request is done if session.notopen then local features = st.stanza("stream:features"); module:context(session.host):fire_event("stream-features", { origin = session, features = features, stream = attr }); session.send(features); session.notopen = nil; end end local function handleerr(err) log("error", "Traceback[bosh]: %s", traceback(err, 2)); end function runner_callbacks:error(err) -- luacheck: ignore 212/self return handleerr(err); end function stream_callbacks.handlestanza(context, stanza) if context.ignore then return; end log("debug", "BOSH stanza received: %s\n", stanza:top_tag()); local session = sessions[context.sid]; if session then if stanza.attr.xmlns == xmlns_bosh then stanza.attr.xmlns = nil; end if context.defer and session.bosh_deferred then log("debug", "Deferring this stanza"); t_insert(session.bosh_deferred, stanza); else stanza = session.filter("stanzas/in", stanza); session.thread:run(stanza); end else log("debug", "No session for this stanza! (sid: %s)", context.sid or "none!"); end end function stream_callbacks.streamclosed(context) local session = sessions[context.sid]; if session then if not context.defer and session.bosh_deferred then -- Handle deferred stanzas now local deferred_stanzas = session.bosh_deferred; local deferred_context = deferred_stanzas.context; session.bosh_deferred = nil; log("debug", "Handling deferred stanzas from rid %d", deferred_stanzas.rid); session.rid = deferred_stanzas.rid; t_insert(session.requests, deferred_context.response); for _, stanza in ipairs(deferred_stanzas) do stream_callbacks.handlestanza(deferred_context, stanza); end if deferred_stanzas.terminate then session.bosh_terminate = true; end end session.bosh_processing = false; if #session.send_buffer > 0 then session.send(""); end end end function stream_callbacks.error(context, error) if not context.sid then log("debug", "Error parsing BOSH request payload; %s", error); local response = context.response; local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", ["xmlns:stream"] = xmlns_streams, condition = "bad-request" }); response:send(tostring(close_reply)); return; end local session = sessions[context.sid]; (session and session.log or log)("warn", "Error parsing BOSH request payload; %s", error); if error == "stream-error" then -- Remote stream error, we close normally session:close(); else session:close({ condition = "bad-format", text = "Error processing stream" }); end end local function GET_response(event) return module:fire_event("http-message", { response = event.response; --- title = "Prosody BOSH endpoint"; message = "It works! Now point your BOSH client to this URL to connect to Prosody."; warning = not (consider_bosh_secure or event.request.secure) and "This endpoint is not considered secure!" or nil; --

For more information see Prosody: Setting up BOSH.

}) or "This is the Prosody BOSH endpoint."; end function module.add_host(module) module:depends("http"); module:provides("http", { default_path = "/http-bind"; cors = { enabled = true; }; route = { ["GET"] = GET_response; ["GET /"] = GET_response; ["POST"] = handle_POST; ["POST /"] = handle_POST; }; }); if module.host ~= "*" then module:depends("http_altconnect", true); end end if require"prosody.core.modulemanager".get_modules_for_host("*"):contains(module.name) then module:add_host(); end prosody-13.0.1/plugins/PaxHeaders/mod_c2s.lua0000644000000000000000000000011714773555365016112 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.595710551 prosody-13.0.1/plugins/mod_c2s.lua0000644000175000017500000004123414773555365020315 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- module:set_global(); 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 "prosody.util.uuid".generate; local async = require "prosody.util.async"; local runner = async.runner; local tostring, type = tostring, type; local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; local log = module._log; local c2s_timeout = module:get_option_period("c2s_timeout", "5 minutes"); local stream_close_timeout = module:get_option_period("c2s_close_timeout", 5); local opt_keepalives = module:get_option_boolean("c2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true)); local stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256,10000); local 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"}); local sessions = module:shared("sessions"); local core_process_stanza = prosody.core_process_stanza; 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", "", "Encrypted connections", {"protocol"; "cipher"} ); module:hook("stats-update", function () -- for push backends, avoid sending out updates for each increment of -- the metric below. statsmanager.cork() measure_connections:clear() for _, session in pairs(sessions) do local host = session.host or "" local type_ = session.type or "other" -- we want to expose both v4 and v6 counters in all cases to make -- queries smoother local is_ipv6 = session.ip and session.ip:match(":") and 1 or 0 local is_ipv4 = 1 - is_ipv6 measure_connections:with_labels(host, type_, "ipv4"):add(is_ipv4) measure_connections:with_labels(host, type_, "ipv6"):add(is_ipv6) end statsmanager.uncork() end); --- Stream events handlers 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({ event = "streamopened", attr = attr }); end 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" }; return; end local host = nameprep(attr.to); if not host then session:close{ condition = "improper-addressing", text = "A valid 'to' attribute is required on stream headers" }; return; end if not session.host then session.host = host; elseif session.host ~= host then session:close{ condition = "not-authorized", text = "The 'to' attribute must remain the same across stream restarts" }; return; end session.version = tonumber(attr.version) or 0; session.streamid = uuid_generate(); (session.log or log)("debug", "Client sent opening to %s", session.host); if not hosts[session.host] or not hosts[session.host].modules.c2s then -- We don't serve this host... session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)}; return; end session:open_stream(host, attr.from); -- Opening the stream can cause the stream to be closed if session.destroyed then return end (session.log or log)("debug", "Sent reply to client"); session.notopen = nil; -- If session.secure is *false* (not nil) then it means we /were/ encrypting -- since we now have a new stream header, session is secured if session.secure == false then session.secure = true; session.encrypted = true; 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; m_tls_params:with_labels(info.protocol, info.cipher):add(1) else (session.log or log)("info", "Stream encrypted"); end end local features = st.stanza("stream:features"); hosts[session.host].events.fire_event("stream-features", { origin = session, features = features, stream = attr }); if features.tags[1] or session.full_jid then if stanza_size_limit or advertised_idle_timeout then 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 -- Here SASL should be offered (session.log or log)("warn", "No stream features to offer on secure session. Check authentication settings."); else -- Normally STARTTLS would be offered (session.log or log)("warn", "No stream features to offer on insecure session. Check encryption and security settings."); end session:close{ condition = "undefined-condition", text = "No stream features to proceed with" }; end end function stream_callbacks.streamclosed(session, attr) -- run _streamclosed in async context session.thread:run({ event = "streamclosed", attr = attr }); end function session_events.streamclosed(session) session.log("debug", "Received "); 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}"))); session:close("invalid-namespace"); elseif error == "parse-error" then (session.log or log)("debug", "Client XML parse error: %s", data); session:close("not-well-formed"); elseif error == "stream-error" then local condition, text = "undefined-condition"; for child in data:childtags(nil, xmlns_xmpp_streams) do if child.name ~= "text" then condition = child.name; else text = child:get_text(); end if condition ~= "undefined-condition" and text then break; end end text = condition .. (text and (" ("..text..")") or ""); session.log("info", "Session closed by remote with error: %s", text); session:close(nil, text); end end function stream_callbacks.handlestanza(session, stanza) stanza = session.filter("stanzas/in", stanza); session.thread:run(stanza); end --- Session methods local function session_close(session, reason) local log = session.log or log; local close_event_payload = { session = session, reason = reason }; module:context(session.host):fire_event("pre-session-close", close_event_payload); reason = close_event_payload.reason; if session.conn then if session.notopen then session:open_stream(); end if reason then -- nil == no err, initiated by us, false == initiated by client local stream_error = st.stanza("stream:error"); if type(reason) == "string" then -- assume stream error stream_error:tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' }); elseif st.is_stanza(reason) then stream_error = reason; elseif type(reason) == "table" then if reason.condition then stream_error:tag(reason.condition, stream_xmlns_attr):up(); if reason.text then stream_error:tag("text", stream_xmlns_attr):text(reason.text):up(); end if reason.extra then stream_error:add_child(reason.extra); end end end stream_error = tostring(stream_error); log("debug", "Disconnecting client, is: %s", stream_error); session.send(stream_error); end session.send(""); function session.send() return false; end local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason; session.log("debug", "c2s stream for %s closed: %s", session.full_jid or session.ip or "", reason_text or "session closed"); -- Authenticated incoming stream may still be sending us stanzas, so wait for from remote local conn = session.conn; if reason_text == nil and not session.notopen and session.type == "c2s" then -- Grace time to process data from authenticated cleanly-closed stream add_task(stream_close_timeout, function () if not session.destroyed then session.log("warn", "Failed to receive a stream close response, closing connection anyway..."); sm_destroy_session(session, reason_text); if conn then conn:close(); end end end); else sm_destroy_session(session, reason_text); if conn then conn:close(); end end else local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason; sm_destroy_session(session, reason_text); end end -- Close all user sessions with the specified reason. If leave_resource is -- true, the resource named by event.resource will not be closed. 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 module:log("debug", "Disconnecting %s sessions of %s@%s (%s)", not leave_resource and "all" or "other", username, host, reason.text); local user = hosts[host].sessions[username]; if user and user.sessions then for r, session in pairs(user.sessions) do if not leave_resource or r ~= resource then session:close(reason); end end end end end module:hook_global("user-password-changed", disconnect_user_sessions({ condition = "reset", text = "Password changed" }, true), 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 self.data.conn:resume(); else (self.data.log or log)("debug", "Session has no connection to resume"); end end function runner_callbacks:waiting() if self.data.conn then self.data.conn:pause(); else (self.data.log or log)("debug", "Session has no connection to pause while waiting"); end end function runner_callbacks:error(err) (self.data.log or log)("error", "Traceback[c2s]: %s", err); end --- Port listener function listener.onconnect(conn) local session = sm_new_session(conn); session.log("info", "Client connected"); -- Client is using Direct TLS or legacy SSL (otherwise mod_tls sets this flag) if conn:ssl() then session.secure = true; session.encrypted = true; session.ssl_ctx = conn:sslctx(); -- Check if TLS compression is used 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; m_tls_params:with_labels(info.protocol, info.cipher):add(1) else (session.log or log)("info", "Stream encrypted"); end end if opt_keepalives then conn:setoption("keepalive", opt_keepalives); end session.close = session_close; local stream = new_xmpp_stream(session, stream_callbacks, stanza_size_limit); session.stream = stream; session.notopen = true; function session.reset_stream() session.notopen = true; session.stream:reset(); end 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); local filter = session.filter; function session.data(data) -- Parse the data, which will store stanzas in session.pending_stanzas if data then data = filter("bytes/in", data); if data then local ok, err = stream:feed(data); if not ok then log("debug", "Received invalid XML (%s) %d bytes: %q", err, #data, data:sub(1, 300)); if err == "stanza-too-large" then session:close({ condition = "policy-violation", text = "XML stanza is too big", extra = st.stanza("stanza-too-big", { xmlns = 'urn:xmpp:errors' }), }); else session:close("not-well-formed"); end end end end end 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 session.dispatch_stanza = stream_callbacks.handlestanza; sessions[conn] = session; end function listener.onincoming(conn, data) local session = sessions[conn]; if session then session.data(data); end end function listener.ondisconnect(conn, err) local session = sessions[conn]; if session then (session.log or log)("info", "Client disconnected: %s", err or "connection closed"); sm_destroy_session(session, err); session.conn = nil; sessions[conn] = nil; end module:fire_event("c2s-closed", { session = session; conn = conn }); end function listener.onreadtimeout(conn) local session = sessions[conn]; if session then return (hosts[session.host] or prosody).events.fire_event("c2s-read-timeout", { session = session }); end end function listener.ondrain(conn) local session = sessions[conn]; if session then return (hosts[session.host] or prosody).events.fire_event("c2s-ondrain", { session = session }); end end function listener.onpredrain(conn) local session = sessions[conn]; if session then return (hosts[session.host] or prosody).events.fire_event("c2s-pre-ondrain", { session = session }); end end local function keepalive(event) local session = event.session; if not session.notopen then return event.session.send(' '); end end function listener.associate_session(conn, session) sessions[conn] = session; end function module.add_host(module) module:hook("c2s-read-timeout", keepalive, -1); end module:hook("c2s-read-timeout", keepalive, -1); module:hook("server-stopping", function(event) -- luacheck: ignore 212/event -- Close ports local pm = require "prosody.core.portmanager"; for _, netservice in pairs(module.items["net-provider"]) do pm.unregister_service(netservice.name, netservice); end end, -80); module:hook("server-stopping", function(event) local wait, done = async.waiter(1, true); module:hook("c2s-closed", function () if next(sessions) == nil then done(); end end) -- Close sessions local reason = event.reason; for _, session in pairs(sessions) do session:close{ condition = "system-shutdown", text = reason }; end -- Wait for them to close properly if they haven't already if next(sessions) ~= nil then add_task(stream_close_timeout+1, function () done() end); module:log("info", "Waiting for sessions to close"); wait(); end end, -100); module:provides("net", { name = "c2s"; listener = listener; default_port = 5222; encryption = "starttls"; multiplex = { protocol = "xmpp-client"; pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:client%1.*>"; }; }); module:provides("net", { name = "c2s_direct_tls"; listener = listener; encryption = "ssl"; multiplex = { pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:client%1.*>"; }; }); -- COMPAT module:provides("net", { name = "legacy_ssl"; listener = listener; encryption = "ssl"; multiplex = { pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:client%1.*>"; }; }); prosody-13.0.1/plugins/PaxHeaders/mod_carbons.lua0000644000000000000000000000011714773555365017052 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.595710551 prosody-13.0.1/plugins/mod_carbons.lua0000644000175000017500000001267614773555365021265 0ustar00prosodyprosody00000000000000-- XEP-0280: Message Carbons implementation for Prosody -- Copyright (C) 2011-2016 Kim Alvefur -- -- This file is MIT/X11 licensed. 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; module:add_feature("urn:xmpp:carbons:rules:0"); local function is_bare(jid) return not jid_resource(jid); end local function toggle_carbons(event) local origin, stanza = event.origin, event.stanza; local state = stanza.tags[1].name; module:log("debug", "%s %sd carbons", origin.full_jid, state); origin.want_carbons = state == "enable" and stanza.tags[1].attr.xmlns; origin.send(st.reply(stanza)); return true; end module:hook("iq-set/self/"..xmlns_carbons..":disable", toggle_carbons); module:hook("iq-set/self/"..xmlns_carbons..":enable", toggle_carbons); local function should_copy(stanza, c2s, user_bare) --> boolean, reason: string local st_type = stanza.attr.type or "normal"; if stanza:get_child("private", xmlns_carbons) then return false, "private"; end if stanza:get_child("no-copy", "urn:xmpp:hints") then return false, "hint"; end if not c2s and stanza.attr.to ~= user_bare and stanza:get_child("x", "http://jabber.org/protocol/muc#user") then -- MUC PMs are normally sent to full JIDs return false, "muc-pm"; end if st_type == "chat" then return true, "type"; end if st_type == "normal" and stanza:get_child("body") then return true, "type"; end -- Normal outgoing chat messages are sent to=bare JID. This clause should -- match the error bounces from those, which would have from=bare JID and -- be incoming (not c2s). if st_type == "error" and not c2s and is_bare(stanza.attr.from) then return true, "bounce"; end if stanza:get_child(nil, "urn:xmpp:jingle-message:0") or stanza:get_child(nil, "urn:xmpp:jingle-message:1") then -- XXX Experimental XEP return true, "jingle call"; end if stanza:get_child_with_attr("stanza-id", "urn:xmpp:sid:0", "by", user_bare) then return true, "archived"; end return false, "default"; end module:hook("carbons-should-copy", function (event) local should, why = should_copy(event.stanza); event.reason = why; return should; end, -1) local function message_handler(event, c2s) local origin, stanza = event.origin, event.stanza; local orig_type = stanza.attr.type or "normal"; local orig_from = stanza.attr.from; local bare_from = jid_bare(orig_from); local orig_to = stanza.attr.to; local bare_to = jid_bare(orig_to); -- Stanza sent by a local client local bare_jid = bare_from; -- JID of the local user local target_session = origin; local top_priority = false; local user_sessions = bare_sessions[bare_from]; -- Stanza about to be delivered to a local client if not c2s then bare_jid = bare_to; target_session = full_sessions[orig_to]; user_sessions = bare_sessions[bare_jid]; if not target_session and user_sessions then -- The top resources will already receive this message per normal routing rules, -- so we are going to skip them in order to avoid sending duplicated messages. local top_resources = user_sessions.top_resources; top_priority = top_resources and top_resources[1].priority end end if not user_sessions then module:log("debug", "Skip carbons for offline user"); return -- No use in sending carbons to an offline user end local event_payload = { stanza = stanza; session = origin }; local should = module:fire_event("carbons-should-copy", event_payload); local why = event_payload.reason; if not should then module:log("debug", "Not copying stanza: %s (%s)", stanza:top_tag(), why); if why == "private" and not c2s then stanza:maptags(function(tag) if not ( tag.attr.xmlns == xmlns_carbons and tag.name == "private" ) then return tag; end end); end return; end local carbon; user_sessions = user_sessions and user_sessions.sessions; for _, session in pairs(user_sessions) do -- Carbons are sent to resources that have enabled it if session.want_carbons -- but not the resource that sent the message, or the one that it's directed to and session ~= target_session -- and isn't among the top resources that would receive the message per standard routing rules and (c2s or session.priority ~= top_priority) then if not carbon then -- Create the carbon copy and wrap it as per the Stanza Forwarding XEP local copy = st.clone(stanza); if c2s and not orig_to then stanza.attr.to = bare_from; end copy.attr.xmlns = "jabber:client"; carbon = st.message{ from = bare_jid, type = orig_type, } :tag(c2s and "sent" or "received", { xmlns = xmlns_carbons }) :tag("forwarded", { xmlns = xmlns_forward }) :add_child(copy):reset(); end carbon.attr.to = session.full_jid; module:log("debug", "Sending carbon to %s", session.full_jid); session.send(carbon); end end end local function c2s_message_handler(event) return message_handler(event, true) end -- Stanzas sent by local clients module:hook("pre-message/host", c2s_message_handler, -0.5); module:hook("pre-message/bare", c2s_message_handler, -0.5); module:hook("pre-message/full", c2s_message_handler, -0.5); -- Stanzas to local clients module:hook("message/bare", message_handler, -0.5); module:hook("message/full", message_handler, -0.5); module:add_feature(xmlns_carbons); prosody-13.0.1/plugins/PaxHeaders/mod_cloud_notify.lua0000644000000000000000000000011714773555365020121 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.595710551 prosody-13.0.1/plugins/mod_cloud_notify.lua0000644000175000017500000006556114773555365022335 0ustar00prosodyprosody00000000000000-- XEP-0357: Push (aka: My mobile OS vendor won't let me have persistent TCP connections) -- Copyright (C) 2015-2016 Kim Alvefur -- Copyright (C) 2017-2019 Thilo Molitor -- -- This file is MIT/X11 licensed. local os_time = os.time; local st = require"prosody.util.stanza"; local jid = require"prosody.util.jid"; local dataform = require"prosody.util.dataforms".new; local hashes = require"prosody.util.hashes"; local random = require"prosody.util.random"; local cache = require"prosody.util.cache"; local watchdog = require "prosody.util.watchdog"; local xmlns_push = "urn:xmpp:push:0"; -- configuration local include_body = module:get_option_boolean("push_notification_with_body", false); local include_sender = module:get_option_boolean("push_notification_with_sender", false); local max_push_errors = module:get_option_number("push_max_errors", 16); local max_push_devices = module:get_option_number("push_max_devices", 5); local dummy_body = module:get_option_string("push_notification_important_body", "New Message!"); local extended_hibernation_timeout = module:get_option_number("push_max_hibernation_timeout", 72*3600); -- use same timeout like ejabberd local host_sessions = prosody.hosts[module.host].sessions; local push_errors = module:shared("push_errors"); local id2node = {}; local id2identifier = {}; -- For keeping state across reloads while caching reads -- This uses util.cache for caching the most recent devices and removing all old devices when max_push_devices is reached local push_store = (function() local store = module:open_store(); local push_services = {}; local api = {}; --luacheck: ignore 212/self function api:get(user) if not push_services[user] then local loaded, err = store:get(user); if not loaded and err then module:log("warn", "Error reading push notification storage for user '%s': %s", user, tostring(err)); push_services[user] = cache.new(max_push_devices):table(); return push_services[user], false; end if loaded then push_services[user] = cache.new(max_push_devices):table(); -- copy over plain table loaded from disk into our cache for k, v in pairs(loaded) do push_services[user][k] = v; end else push_services[user] = cache.new(max_push_devices):table(); end end return push_services[user], true; end function api:flush_to_disk(user) local plain_table = {}; for k, v in pairs(push_services[user]) do plain_table[k] = v; end local ok, err = store:set(user, plain_table); if not ok then module:log("error", "Error writing push notification storage for user '%s': %s", user, tostring(err)); return false; end return true; end function api:set_identifier(user, push_identifier, data) local services = self:get(user); services[push_identifier] = data; end return api; end)(); -- Forward declarations, as both functions need to reference each other local handle_push_success, handle_push_error; function handle_push_error(event) local stanza = event.stanza; local error_type, condition, error_text = stanza:get_error(); local node = id2node[stanza.attr.id]; local identifier = id2identifier[stanza.attr.id]; if node == nil then module:log("warn", "Received push error with unrecognised id: %s", stanza.attr.id); return false; -- unknown stanza? Ignore for now! end local from = stanza.attr.from; local user_push_services = push_store:get(node); local found, changed = false, false; for push_identifier, _ in pairs(user_push_services) do if push_identifier == identifier then found = true; if user_push_services[push_identifier] and user_push_services[push_identifier].jid == from and error_type ~= "wait" then push_errors[push_identifier] = push_errors[push_identifier] + 1; module:log("info", "Got error <%s:%s:%s> for identifier '%s': " .."error count for this identifier is now at %s", error_type, condition, error_text or "", push_identifier, tostring(push_errors[push_identifier])); if push_errors[push_identifier] >= max_push_errors then module:log("warn", "Disabling push notifications for identifier '%s'", push_identifier); -- remove push settings from sessions if host_sessions[node] then for _, session in pairs(host_sessions[node].sessions) do if session.push_identifier == push_identifier then session.push_identifier = nil; session.push_settings = nil; session.first_hibernated_push = nil; -- check for prosody 0.12 mod_smacks if session.hibernating_watchdog and session.original_smacks_callback and session.original_smacks_timeout then -- restore old smacks watchdog session.hibernating_watchdog:cancel(); session.hibernating_watchdog = watchdog.new(session.original_smacks_timeout, session.original_smacks_callback); end end end end -- save changed global config changed = true; user_push_services[push_identifier] = nil push_errors[push_identifier] = nil; -- unhook iq handlers for this identifier (if possible) module:unhook("iq-error/host/"..stanza.attr.id, handle_push_error); module:unhook("iq-result/host/"..stanza.attr.id, handle_push_success); id2node[stanza.attr.id] = nil; id2identifier[stanza.attr.id] = nil; end elseif user_push_services[push_identifier] and user_push_services[push_identifier].jid == from and error_type == "wait" then module:log("debug", "Got error <%s:%s:%s> for identifier '%s': " .."NOT increasing error count for this identifier", error_type, condition, error_text or "", push_identifier); else module:log("debug", "Unhandled push error <%s:%s:%s> from %s for identifier '%s'", error_type, condition, error_text or "", from, push_identifier ); end end end if changed then push_store:flush_to_disk(node); elseif not found then module:log("warn", "Unable to find matching registration for push error <%s:%s:%s> from %s", error_type, condition, error_text or "", from); end return true; end function handle_push_success(event) local stanza = event.stanza; local node = id2node[stanza.attr.id]; local identifier = id2identifier[stanza.attr.id]; if node == nil then return false; end -- unknown stanza? Ignore for now! local from = stanza.attr.from; local user_push_services = push_store:get(node); for push_identifier, _ in pairs(user_push_services) do if push_identifier == identifier then if user_push_services[push_identifier] and user_push_services[push_identifier].jid == from and push_errors[push_identifier] > 0 then push_errors[push_identifier] = 0; -- unhook iq handlers for this identifier (if possible) module:unhook("iq-error/host/"..stanza.attr.id, handle_push_error); module:unhook("iq-result/host/"..stanza.attr.id, handle_push_success); id2node[stanza.attr.id] = nil; id2identifier[stanza.attr.id] = nil; module:log("debug", "Push succeeded, error count for identifier '%s' is now at %s again", push_identifier, tostring(push_errors[push_identifier]) ); end end end return true; end -- http://xmpp.org/extensions/xep-0357.html#disco local function account_dico_info(event) (event.reply or event.stanza):tag("feature", {var=xmlns_push}):up(); end module:hook("account-disco-info", account_dico_info); -- http://xmpp.org/extensions/xep-0357.html#enabling local function push_enable(event) local origin, stanza = event.origin, event.stanza; local enable = stanza.tags[1]; origin.log("debug", "Attempting to enable push notifications"); -- MUST contain a 'jid' attribute of the XMPP Push Service being enabled local push_jid = enable.attr.jid; -- SHOULD contain a 'node' attribute local push_node = enable.attr.node; -- CAN contain a 'include_payload' attribute local include_payload = enable.attr.include_payload; if not push_jid then origin.log("debug", "Push notification enable request missing the 'jid' field"); origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing jid")); return true; end if push_jid == stanza.attr.from then origin.log("debug", "Push notification enable request 'jid' field identical to our own"); origin.send(st.error_reply(stanza, "modify", "bad-request", "JID must be different from ours")); return true; end local publish_options = enable:get_child("x", "jabber:x:data"); if not publish_options then -- Could be intentional origin.log("debug", "No publish options in request"); end local push_identifier = push_jid .. "<" .. (push_node or ""); local push_service = { jid = push_jid; node = push_node; include_payload = include_payload; options = publish_options and st.preserialize(publish_options); timestamp = os_time(); client_id = origin.client_id; resource = not origin.client_id and origin.resource or nil; language = stanza.attr["xml:lang"]; }; local allow_registration = module:fire_event("cloud_notify/registration", { origin = origin, stanza = stanza, push_info = push_service; }); if allow_registration == false then return true; -- Assume error reply already sent end push_store:set_identifier(origin.username, push_identifier, push_service); local ok = push_store:flush_to_disk(origin.username); if not ok then origin.send(st.error_reply(stanza, "wait", "internal-server-error")); else origin.push_identifier = push_identifier; origin.push_settings = push_service; origin.first_hibernated_push = nil; origin.log("info", "Push notifications enabled for %s (%s)", tostring(stanza.attr.from), tostring(origin.push_identifier)); origin.send(st.reply(stanza)); end return true; end module:hook("iq-set/self/"..xmlns_push..":enable", push_enable); -- http://xmpp.org/extensions/xep-0357.html#disabling local function push_disable(event) local origin, stanza = event.origin, event.stanza; local push_jid = stanza.tags[1].attr.jid; -- MUST include a 'jid' attribute local push_node = stanza.tags[1].attr.node; -- A 'node' attribute MAY be included if not push_jid then origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing jid")); return true; end local user_push_services = push_store:get(origin.username); for key, push_info in pairs(user_push_services) do if push_info.jid == push_jid and (not push_node or push_info.node == push_node) then origin.log("info", "Push notifications disabled (%s)", tostring(key)); if origin.push_identifier == key then origin.push_identifier = nil; origin.push_settings = nil; origin.first_hibernated_push = nil; -- check for prosody 0.12 mod_smacks if origin.hibernating_watchdog and origin.original_smacks_callback and origin.original_smacks_timeout then -- restore old smacks watchdog origin.hibernating_watchdog:cancel(); origin.hibernating_watchdog = watchdog.new(origin.original_smacks_timeout, origin.original_smacks_callback); end end user_push_services[key] = nil; push_errors[key] = nil; for stanza_id, identifier in pairs(id2identifier) do if identifier == key then module:unhook("iq-error/host/"..stanza_id, handle_push_error); module:unhook("iq-result/host/"..stanza_id, handle_push_success); id2node[stanza_id] = nil; id2identifier[stanza_id] = nil; end end end end local ok = push_store:flush_to_disk(origin.username); if not ok then origin.send(st.error_reply(stanza, "wait", "internal-server-error")); else origin.send(st.reply(stanza)); end return true; end module:hook("iq-set/self/"..xmlns_push..":disable", push_disable); -- urgent stanzas should be delivered without delay local function is_urgent(stanza) -- TODO if stanza.name == "message" then if stanza:get_child("propose", "urn:xmpp:jingle-message:0") then return true, "jingle call"; end end end -- is this push a high priority one (this is needed for ios apps not using voip pushes) local function is_important(stanza) local st_name = stanza and stanza.name or nil; if not st_name then return false; end -- nonzas are never important here if st_name == "presence" then return false; -- same for presences elseif st_name == "message" then -- unpack carbon copied message stanzas local carbon = stanza:find("{urn:xmpp:carbons:2}/{urn:xmpp:forward:0}/{jabber:client}message"); local stanza_direction = carbon and stanza:child_with_name("sent") and "out" or "in"; if carbon then stanza = carbon; end local st_type = stanza.attr.type; -- headline message are always not important if st_type == "headline" then return false; end -- carbon copied outgoing messages are not important if carbon and stanza_direction == "out" then return false; end -- We can't check for body contents in encrypted messages, so let's treat them as important -- Some clients don't even set a body or an empty body for encrypted messages -- check omemo https://xmpp.org/extensions/inbox/omemo.html if stanza:get_child("encrypted", "eu.siacs.conversations.axolotl") or stanza:get_child("encrypted", "urn:xmpp:omemo:0") then return true; end -- check xep27 pgp https://xmpp.org/extensions/xep-0027.html if stanza:get_child("x", "jabber:x:encrypted") then return true; end -- check xep373 pgp (OX) https://xmpp.org/extensions/xep-0373.html if stanza:get_child("openpgp", "urn:xmpp:openpgp:0") then return true; end -- XEP-0353: Jingle Message Initiation (incoming call request) if stanza:get_child("propose", "urn:xmpp:jingle-message:0") then return true; end local body = stanza:get_child_text("body"); -- groupchat subjects are not important here if st_type == "groupchat" and stanza:get_child_text("subject") then return false; end -- empty bodies are not important return body ~= nil and body ~= ""; end return false; -- this stanza wasn't one of the above cases --> it is not important, too end local push_form = dataform { { name = "FORM_TYPE"; type = "hidden"; value = "urn:xmpp:push:summary"; }; { name = "message-count"; type = "text-single"; }; { name = "pending-subscription-count"; type = "text-single"; }; { name = "last-message-sender"; type = "jid-single"; }; { name = "last-message-body"; type = "text-single"; }; }; -- http://xmpp.org/extensions/xep-0357.html#publishing local function handle_notify_request(stanza, node, user_push_services, log_push_decline) local pushes = 0; if not #user_push_services then return pushes end for push_identifier, push_info in pairs(user_push_services) do local send_push = true; -- only send push to this node when not already done for this stanza or if no stanza is given at all if stanza then if not stanza._push_notify then stanza._push_notify = {}; end if stanza._push_notify[push_identifier] then if log_push_decline then module:log("debug", "Already sent push notification for %s@%s to %s (%s)", node, module.host, push_info.jid, tostring(push_info.node)); end send_push = false; end stanza._push_notify[push_identifier] = true; end if send_push then -- construct push stanza local stanza_id = hashes.sha256(random.bytes(8), true); local push_notification_payload = st.stanza("notification", { xmlns = xmlns_push }); local form_data = { -- hardcode to 1 because other numbers are just meaningless (the XEP does not specify *what exactly* to count) ["message-count"] = "1"; }; if stanza and include_sender then form_data["last-message-sender"] = stanza.attr.from; end if stanza and include_body then form_data["last-message-body"] = stanza:get_child_text("body"); elseif stanza and dummy_body and is_important(stanza) then form_data["last-message-body"] = tostring(dummy_body); end push_notification_payload:add_child(push_form:form(form_data)); local push_publish = st.iq({ to = push_info.jid, from = module.host, type = "set", id = stanza_id }) :tag("pubsub", { xmlns = "http://jabber.org/protocol/pubsub" }) :tag("publish", { node = push_info.node }) :tag("item") :add_child(push_notification_payload) :up() :up(); if push_info.options then push_publish:tag("publish-options"):add_child(st.deserialize(push_info.options)); end -- send out push module:log("debug", "Sending %s push notification for %s@%s to %s (%s)", form_data["last-message-body"] and "important" or "unimportant", node, module.host, push_info.jid, tostring(push_info.node) ); -- module:log("debug", "PUSH STANZA: %s", tostring(push_publish)); local push_event = { notification_stanza = push_publish; notification_payload = push_notification_payload; original_stanza = stanza; username = node; push_info = push_info; push_summary = form_data; important = not not form_data["last-message-body"]; }; if module:fire_event("cloud_notify/push", push_event) then module:log("debug", "Push was blocked by event handler: %s", push_event.reason or "Unknown reason"); else -- handle push errors for this node if push_errors[push_identifier] == nil then push_errors[push_identifier] = 0; end module:hook("iq-error/host/"..stanza_id, handle_push_error); module:hook("iq-result/host/"..stanza_id, handle_push_success); id2node[stanza_id] = node; id2identifier[stanza_id] = push_identifier; module:send(push_publish); pushes = pushes + 1; end end end return pushes; end -- small helper function to extract relevant push settings local function get_push_settings(stanza, session) local to = stanza.attr.to; local node = to and jid.split(to) or session.username; local user_push_services = push_store:get(node); return node, user_push_services; end -- publish on offline message module:hook("message/offline/handle", function(event) local node, user_push_services = get_push_settings(event.stanza, event.origin); module:log("debug", "Invoking cloud handle_notify_request() for offline stanza"); handle_notify_request(event.stanza, node, user_push_services, true); end, 1); -- publish on bare groupchat -- this picks up MUC messages when there are no devices connected module:hook("message/bare/groupchat", function(event) module:log("debug", "Invoking cloud handle_notify_request() for bare groupchat stanza"); local node, user_push_services = get_push_settings(event.stanza, event.origin); handle_notify_request(event.stanza, node, user_push_services, true); end, 1); local function process_stanza_queue(queue, session, queue_type) if not session.push_identifier then return; end local user_push_services = {[session.push_identifier] = session.push_settings}; local notified = { unimportant = false; important = false } for i=1, #queue do local stanza = queue[i]; -- fast ignore of already pushed stanzas if stanza and not (stanza._push_notify and stanza._push_notify[session.push_identifier]) then local node = get_push_settings(stanza, session); local stanza_type = "unimportant"; if dummy_body and is_important(stanza) then stanza_type = "important"; end if not notified[stanza_type] then -- only notify if we didn't try to push for this stanza type already -- session.log("debug", "Invoking cloud handle_notify_request() for smacks queued stanza: %d", i); if handle_notify_request(stanza, node, user_push_services, false) ~= 0 then if session.hibernating and not session.first_hibernated_push then -- if important stanzas are treated differently (pushed with last-message-body field set to dummy string) -- if the message was important (e.g. had a last-message-body field) OR if we treat all pushes equally, -- then record the time of first push in the session for the smack module which will extend its hibernation -- timeout based on the value of session.first_hibernated_push if not dummy_body or (dummy_body and is_important(stanza)) then session.first_hibernated_push = os_time(); -- check for prosody 0.12 mod_smacks if session.hibernating_watchdog and session.original_smacks_callback and session.original_smacks_timeout then -- restore old smacks watchdog (--> the start of our original timeout will be delayed until first push) session.hibernating_watchdog:cancel(); session.hibernating_watchdog = watchdog.new(session.original_smacks_timeout, session.original_smacks_callback); end end end session.log("debug", "Cloud handle_notify_request() > 0, not notifying for other %s queued stanzas of type %s", queue_type, stanza_type); notified[stanza_type] = true end end end if notified.unimportant and notified.important then break; end -- stop processing the queue if all push types are exhausted end end -- publish on unacked smacks message (use timer to send out push for all stanzas submitted in a row only once) local function process_stanza(session, stanza) if session.push_identifier then session.log("debug", "adding new stanza to push_queue"); if not session.push_queue then session.push_queue = {}; end local queue = session.push_queue; queue[#queue+1] = st.clone(stanza); if not session.awaiting_push_timer then -- timer not already running --> start new timer session.log("debug", "Invoking cloud handle_notify_request() for newly smacks queued stanza (in a moment)"); session.awaiting_push_timer = module:add_timer(1.0, function () session.log("debug", "Invoking cloud handle_notify_request() for newly smacks queued stanzas (now in timer)"); process_stanza_queue(session.push_queue, session, "push"); session.push_queue = {}; -- clean up queue after push session.awaiting_push_timer = nil; end); end end return stanza; end local function process_smacks_stanza(event) local session = event.origin; local stanza = event.stanza; if not session.push_identifier then session.log("debug", "NOT invoking cloud handle_notify_request() for newly smacks queued stanza (session.push_identifier is not set: %s)", session.push_identifier ); else process_stanza(session, stanza) end end -- smacks hibernation is started local function hibernate_session(event) local session = event.origin; local queue = event.queue; session.first_hibernated_push = nil; if session.push_identifier and session.hibernating_watchdog then -- check for prosody 0.12 mod_smacks -- save old watchdog callback and timeout session.original_smacks_callback = session.hibernating_watchdog.callback; session.original_smacks_timeout = session.hibernating_watchdog.timeout; -- cancel old watchdog and create a new watchdog with extended timeout session.hibernating_watchdog:cancel(); session.hibernating_watchdog = watchdog.new(extended_hibernation_timeout, function() session.log("debug", "Push-extended smacks watchdog triggered"); if session.original_smacks_callback then session.log("debug", "Calling original smacks watchdog handler"); session.original_smacks_callback(); end end); end -- process unacked stanzas process_stanza_queue(queue, session, "smacks"); end -- smacks hibernation is ended local function restore_session(event) local session = event.resumed; if session then -- older smacks module versions send only the "intermediate" session in event.session and no session.resumed one if session.awaiting_push_timer then session.awaiting_push_timer:stop(); session.awaiting_push_timer = nil; end session.first_hibernated_push = nil; -- the extended smacks watchdog will be canceled by the smacks module, no need to anything here end end -- smacks ack is delayed local function ack_delayed(event) local session = event.origin; local queue = event.queue; local stanza = event.stanza; if not session.push_identifier then return; end if stanza then process_stanza(session, stanza); return; end -- don't iterate through smacks queue if we know which stanza triggered this for i=1, #queue do local queued_stanza = queue[i]; -- process unacked stanzas (handle_notify_request() will only send push requests for new stanzas) process_stanza(session, queued_stanza); end end -- archive message added local function archive_message_added(event) -- event is: { origin = origin, stanza = stanza, for_user = store_user, id = id } -- only notify for new mam messages when at least one device is online if not event.for_user or not host_sessions[event.for_user] then return; end local stanza = event.stanza; local user_session = host_sessions[event.for_user].sessions; local to = stanza.attr.to; to = to and jid.split(to) or event.origin.username; -- only notify if the stanza destination is the mam user we store it for if event.for_user == to then local user_push_services = push_store:get(to); -- Urgent stanzas are time-sensitive (e.g. calls) and should -- be pushed immediately to avoid getting stuck in the smacks -- queue in case of dead connections, for example local is_urgent_stanza, urgent_reason = is_urgent(event.stanza); local notify_push_services; if is_urgent_stanza then module:log("debug", "Urgent push for %s (%s)", to, urgent_reason); notify_push_services = user_push_services; else -- only notify nodes with no active sessions (smacks is counted as active and handled separate) notify_push_services = {}; for identifier, push_info in pairs(user_push_services) do local identifier_found = nil; for _, session in pairs(user_session) do if session.push_identifier == identifier then identifier_found = session; break; end end if identifier_found then identifier_found.log("debug", "Not cloud notifying '%s' of new MAM stanza (session still alive)", identifier); else notify_push_services[identifier] = push_info; end end end handle_notify_request(event.stanza, to, notify_push_services, true); end end module:hook("smacks-hibernation-start", hibernate_session); module:hook("smacks-hibernation-end", restore_session); module:hook("smacks-ack-delayed", ack_delayed); module:hook("smacks-hibernation-stanza-queued", process_smacks_stanza); module:hook("archive-message-added", archive_message_added); local function send_ping(event) local user = event.user; local push_services = event.push_services or push_store:get(user); module:log("debug", "Handling event 'cloud-notify-ping' for user '%s'", user); local retval = handle_notify_request(nil, user, push_services, true); module:log("debug", "handle_notify_request() returned %s", tostring(retval)); end -- can be used by other modules to ping one or more (or all) push endpoints module:hook("cloud-notify-ping", send_ping); module:log("info", "Module loaded"); function module.unload() module:log("info", "Unloading module"); -- cleanup some settings, reloading this module can cause process_smacks_stanza() to stop working otherwise for user, _ in pairs(host_sessions) do for _, session in pairs(host_sessions[user].sessions) do if session.awaiting_push_timer then session.awaiting_push_timer:stop(); end session.awaiting_push_timer = nil; session.push_queue = nil; session.first_hibernated_push = nil; -- check for prosody 0.12 mod_smacks if session.hibernating_watchdog and session.original_smacks_callback and session.original_smacks_timeout then -- restore old smacks watchdog session.hibernating_watchdog:cancel(); session.hibernating_watchdog = watchdog.new(session.original_smacks_timeout, session.original_smacks_callback); end end end module:log("info", "Module unloaded"); end prosody-13.0.1/plugins/PaxHeaders/mod_component.lua0000644000000000000000000000011714773555365017425 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.595710551 prosody-13.0.1/plugins/mod_component.lua0000644000175000017500000003052414773555365021630 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- module:set_global(); local t_concat = table.concat; local tostring, type = tostring, type; local xpcall = require "prosody.util.xpcall".xpcall; local traceback = debug.traceback; local logger = require "prosody.util.logger"; local sha1 = require "prosody.util.hashes".sha1; local st = require "prosody.util.stanza"; 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; 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_integer("component_stanza_size_limit", module:get_option_integer("s2s_stanza_size_limit", 1024 * 512, 10000), 10000); local sessions = module:shared("sessions"); local function keepalive(event) local session = event.session; if not session.notopen then return event.session.send(' '); end end function module.add_host(module) if module:get_host_type() ~= "component" then error("Don't load mod_component manually, it should be for a component, please see https://prosody.im/doc/components", 0); end local env = module.environment; env.connected = false; env.session = false; local send; local function on_destroy(session, err) --luacheck: ignore 212/err module:set_status("warn", err and ("Disconnected: "..err) or "Disconnected"); env.connected = false; env.session = false; send = nil; session.on_destroy = nil; end -- Handle authentication attempts by component local function handle_component_auth(event) local session, stanza = event.origin, event.stanza; if session.type ~= "component_unauthed" then return; end if (not session.host) or #stanza.tags > 0 then (session.log or log)("warn", "Invalid component handshake for host: %s", session.host); session:close("not-authorized"); return true; end local secret = module:get_option_string("component_secret"); if not secret then (session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.host); session:close("not-authorized"); return true; end local supplied_token = t_concat(stanza); local calculated_token = sha1(session.streamid..secret, true); if supplied_token:lower() ~= calculated_token:lower() then module:log("info", "Component authentication failed for %s", session.host); session:close{ condition = "not-authorized", text = "Given token does not match calculated token" }; return true; end if env.connected then 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 module:log("error", "Second component attempted to connect, denying connection"); session:close{ condition = "conflict", text = "Component already connected" }; return true; end end env.connected = true; env.session = session; send = session.send; session.on_destroy = on_destroy; session.component_validate_from = module:get_option_boolean("validate_from_addresses", true); session.type = "component"; module:log("info", "External component successfully authenticated"); session.send(st.stanza("handshake")); module:fire_event("component-authenticated", { session = session }); module:set_status("info", "Connected"); return true; end module:hook("stanza/jabber:component:accept:handshake", handle_component_auth, -1); -- Handle stanzas addressed to this component local function handle_stanza(event) local stanza = event.stanza; if send then stanza.attr.xmlns = nil; send(stanza); else if stanza.name == "iq" and stanza.attr.type == "get" and stanza.attr.to == module.host then local query = stanza.tags[1]; local node = query.attr.node; if query.name == "query" and query.attr.xmlns == "http://jabber.org/protocol/disco#info" and (not node or node == "") then local name = module:get_option_string("name"); if name then local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info" }) :tag("identity", { category = "component", type = "generic", name = module:get_option_string("name", "Prosody") }):up() :tag("feature", { var = "http://jabber.org/protocol/disco#info" }):up(); event.origin.send(reply); return true; end end end module:log("warn", "Component not connected, bouncing error for: %s", stanza:top_tag()); if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then event.origin.send(st.error_reply(stanza, "wait", "remote-server-timeout", "Component unavailable", module.host) :tag("not-connected", { xmlns = "xmpp:prosody.im/protocol/component" })); end end return true; end module:hook("iq/bare", handle_stanza, -1); module:hook("message/bare", handle_stanza, -1); module:hook("presence/bare", handle_stanza, -1); module:hook("iq/full", handle_stanza, -1); module:hook("message/full", handle_stanza, -1); module:hook("presence/full", handle_stanza, -1); module:hook("iq/host", handle_stanza, -1); module:hook("message/host", handle_stanza, -1); module:hook("presence/host", handle_stanza, -1); module:hook("component-read-timeout", keepalive, -1); end module:hook("component-read-timeout", keepalive, -1); --- Network and stream part --- local xmlns_component = 'jabber:component:accept'; local listener = {}; --- Callbacks/data for xmppstream to handle streams for us --- local stream_callbacks = { default_ns = xmlns_component }; local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; function stream_callbacks.error(session, error, data) if session.destroyed then return; end module:log("warn", "Error processing component stream: %s", error); if error == "no-stream" then session:close("invalid-namespace"); elseif error == "parse-error" then session.log("warn", "External component %s XML parse error: %s", session.host, data); session:close("not-well-formed"); elseif error == "stream-error" then local condition, text = "undefined-condition"; for child in data:childtags(nil, xmlns_xmpp_streams) do if child.name ~= "text" then condition = child.name; else text = child:get_text(); end if condition ~= "undefined-condition" and text then break; end end text = condition .. (text and (" ("..text..")") or ""); session.log("info", "Session closed by remote with error: %s", text); session:close(nil, text); end end function stream_callbacks.streamopened(session, attr) if not attr.to then session:close{ condition = "improper-addressing", text = "A 'to' attribute is required on stream headers" }; return; end if not hosts[attr.to] or not hosts[attr.to].modules.component then session:close{ condition = "host-unknown", text = tostring(attr.to).." does not match any configured external components" }; return; end session.host = attr.to; session.streamid = uuid_gen(); session.notopen = nil; -- Return stream header session:open_stream(); end function stream_callbacks.streamclosed(session) session.log("debug", "Received "); session:close(); end local function handleerr(err) log("error", "Traceback[component]: %s", traceback(err, 2)); end function stream_callbacks.handlestanza(session, stanza) -- Namespaces are icky. if not stanza.attr.xmlns and stanza.name == "handshake" then stanza.attr.xmlns = xmlns_component; end if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then local from = stanza.attr.from; 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 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"); if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then session.send(st.error_reply(stanza, "modify", "bad-request", "Components MUST specify a 'to' address on stanzas")); end return; end end if stanza then return xpcall(core_process_stanza, handleerr, session, stanza); end end --- Closing a component connection local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; local function session_close(session, reason) if session.destroyed then return; end if session.conn then if session.notopen then session.send(""); session.send(st.stanza("stream:stream", default_stream_attr):top_tag()); end if reason then if type(reason) == "string" then -- assume stream error module:log("info", "Disconnecting component, is: %s", reason); session.send(st.stanza("stream:error"):tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' })); elseif st.is_stanza(reason) then module:log("info", "Disconnecting component, is: %s", reason); session.send(reason); elseif type(reason) == "table" then if reason.condition then local stanza = st.stanza("stream:error"):tag(reason.condition, stream_xmlns_attr):up(); if reason.text then stanza:tag("text", stream_xmlns_attr):text(reason.text):up(); end if reason.extra then stanza:add_child(reason.extra); end module:log("info", "Disconnecting component, is: %s", stanza); session.send(stanza); end end end session.send(""); session.conn:close(); listener.ondisconnect(session.conn, "stream error"); end end --- Component connlistener function listener.onconnect(conn) local _send = conn.write; local session = { type = "component_unauthed", conn = conn, send = function (data) return _send(conn, tostring(data)); end }; -- Logging functions -- local conn_name = "jcp"..tostring(session):match("[a-f0-9]+$"); session.log = logger.init(conn_name); session.close = session_close; if opt_keepalives then conn:setoption("keepalive", opt_keepalives); end session.log("info", "Incoming Jabber component connection"); local stream = new_xmpp_stream(session, stream_callbacks, stanza_size_limit); session.stream = stream; session.notopen = true; function session.reset_stream() session.notopen = true; session.stream:reset(); end function session.data(_, data) local ok, err = stream:feed(data); if ok then return; end log("debug", "Received invalid XML (%s) %d bytes: %q", err, #data, data:sub(1, 300)); session:close("not-well-formed"); end session.dispatch_stanza = stream_callbacks.handlestanza; sessions[conn] = session; end function listener.onincoming(conn, data) local session = sessions[conn]; session.data(conn, data); end function listener.ondisconnect(conn, err) local session = sessions[conn]; if session then (session.log or log)("info", "component disconnected: %s (%s)", session.host, err); if session.host then module:context(session.host):fire_event("component-disconnected", { session = session, reason = err }); end if session.on_destroy then session:on_destroy(err); end sessions[conn] = nil; for k in pairs(session) do if k ~= "log" and k ~= "close" then session[k] = nil; end end session.destroyed = true; end end function listener.ondetach(conn) sessions[conn] = nil; end function listener.onreadtimeout(conn) local session = sessions[conn]; if session then return (hosts[session.host] or prosody).events.fire_event("component-read-timeout", { session = session }); end end module:provides("net", { name = "component"; private = true; listener = listener; default_port = 5347; multiplex = { pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:component:accept%1.*>"; }; }); prosody-13.0.1/plugins/PaxHeaders/mod_cron.lua0000644000000000000000000000011714773555365016364 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.595710551 prosody-13.0.1/plugins/mod_cron.lua0000644000175000017500000000637314773555365020574 0ustar00prosodyprosody00000000000000module: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 active_hosts = {} if prosody.process_type == "prosodyctl" then return; -- Yes, it happens... end function module.add_host(host_module) local last_run_times = host_module:open_store("cron", "map"); active_hosts[host_module.host] = true; 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 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) 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, 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(cron_initial_delay, function() 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")) do task_runner:run(task); end end 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("prosody.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; }); prosody-13.0.1/plugins/PaxHeaders/mod_csi.lua0000644000000000000000000000011714773555365016201 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.599710566 prosody-13.0.1/plugins/mod_csi.lua0000644000175000017500000000273514773555365020407 0ustar00prosodyprosody00000000000000local st = require "prosody.util.stanza"; local xmlns_csi = "urn:xmpp:csi:0"; local csi_feature = st.stanza("csi", { xmlns = xmlns_csi }); 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 then event.features:add_child(csi_feature); end end); 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 end; end module:hook("stanza/"..xmlns_csi..":active", refire_event("csi-client-active")); module:hook("stanza/"..xmlns_csi..":inactive", refire_event("csi-client-inactive")); 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 count:with_labels("active"):set(active); count:with_labels("inactive"):set(inactive); count:with_labels("flushing"):set(flushing); end); prosody-13.0.1/plugins/PaxHeaders/mod_csi_simple.lua0000644000000000000000000000011714773555365017552 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.599710566 prosody-13.0.1/plugins/mod_csi_simple.lua0000644000175000017500000002256014773555365021756 0ustar00prosodyprosody00000000000000-- Copyright (C) 2016-2020 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- module:depends"csi" 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_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", { }); function is_important(stanza) --> boolean, reason: string if stanza == " " then return true, "whitespace keepalive"; elseif type(stanza) == "string" then return true, "raw data"; elseif not st.is_stanza(stanza) then -- This should probably never happen return true, type(stanza); end if stanza.attr.xmlns ~= nil then -- stream errors, stream management etc return true, "nonza"; end local st_name = stanza.name; if not st_name then return false; end local st_type = stanza.attr.type; if st_name == "presence" then if st_type == nil or st_type == "unavailable" or st_type == "error" then return false, "presence update"; end -- TODO Some MUC awareness, e.g. check for the 'this relates to you' status code return true, "subscription request"; elseif st_name == "message" then if st_type == "headline" then -- Headline messages are ephemeral by definition return false, "headline"; end if st_type == "error" then return true, "delivery failure"; end if stanza:get_child("sent", "urn:xmpp:carbons:2") then return true, "carbon"; end local forwarded = stanza:find("{urn:xmpp:carbons:2}received/{urn:xmpp:forward:0}/{jabber:client}message"); if forwarded then stanza = forwarded; end if stanza:get_child("body") then return true, "body"; end if stanza:get_child("subject") then -- Last step of a MUC join return true, "subject"; end if stanza:get_child("encryption", "urn:xmpp:eme:0") then -- Since we can't know what an encrypted message contains, we assume it's important -- XXX Experimental XEP return true, "encrypted"; end if stanza:get_child("x", "jabber:x:conference") or stanza:find("{http://jabber.org/protocol/muc#user}x/invite") then return true, "invite"; end if stanza:get_child(nil, "urn:xmpp:jingle-message:0") or stanza:get_child(nil, "urn:xmpp:jingle-message:1") then -- XXX Experimental XEP return true, "jingle call"; end for important in important_payloads do if stanza:find(important) then return true; end end return false; elseif st_name == "iq" then return true; end end module:hook("csi-is-stanza-important", function (event) local important, why = is_important(event.stanza); event.reason = why; return important; end, -1); local function should_flush(stanza, session, ctr) --> boolean, reason: string if ctr >= queue_size then return true, "queue size limit reached"; end local event = { stanza = stanza, session = session }; local ret = module:fire_event("csi-is-stanza-important", event) return ret, event.reason; end local function with_timestamp(stanza, from) if st.is_stanza(stanza) and stanza.attr.xmlns == nil and stanza.name ~= "iq" then stanza = st.clone(stanza); stanza:add_direct_child(st.stanza("delay", {xmlns = "urn:xmpp:delay", from = from, stamp = dt.datetime()})); end return stanza; end local measure_buffer_hold = module:measure("buffer_hold", "times", { buckets = { 0.1; 1; 5; 10; 15; 30; 60; 120; 180; 300; 600; 900 } }); local flush_reasons = module:metric( "counter", "flushes", "", "CSI queue flushes", { "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 session.csi_counter = ctr + 1; return stanza; end local flush, why = should_flush(stanza, session, ctr); if flush then if session.csi_measure_buffer_hold then session.csi_measure_buffer_hold(); 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 }); session.conn:resume_writes(); else session.log("debug", "Holding buffer (%s; queue size is %d)", why or "unimportant", session.csi_counter); stanza = with_timestamp(stanza, jid.join(session.username, session.host)) end session.csi_counter = ctr + 1; return stanza; end local function flush_buffer(data, session) local ctr = session.csi_counter or 0; if ctr == 0 or session.state ~= "inactive" then return data end 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(); session.csi_measure_buffer_hold = nil; end session.conn:resume_writes(); return data; end function enable_optimizations(session) if session.conn and session.conn.pause_writes then session.conn:pause_writes(); session.csi_measure_buffer_hold = measure_buffer_hold(); session.csi_counter = 0; if session.csi_resume then timer.stop(session.csi_resume); session.csi_resume = nil; end filters.add_filter(session, "stanzas/out", manage_buffer); filters.add_filter(session, "bytes/in", flush_buffer); else session.log("warn", "Session connection does not support write pausing"); end end function disable_optimizations(session) filters.remove_filter(session, "stanzas/out", manage_buffer); filters.remove_filter(session, "bytes/in", flush_buffer); session.csi_counter = nil; if session.csi_resume then timer.stop(session.csi_resume); session.csi_resume = nil; end if session.csi_measure_buffer_hold then session.csi_measure_buffer_hold(); session.csi_measure_buffer_hold = nil; end if session.conn and session.conn.resume_writes then session.conn:resume_writes(); end end module:hook("csi-client-inactive", function (event) local session = event.origin; enable_optimizations(session); end); module:hook("csi-client-active", function (event) local session = event.origin; disable_optimizations(session); end); module:hook("pre-resource-unbind", function (event) local session = event.session; disable_optimizations(session); end, 1); local function resume_optimizations(_, _, session) if (session.state == "flushing" or session.state == "inactive") and session.conn and session.conn.pause_writes then session.state = "inactive"; session.conn:pause_writes(); session.csi_measure_buffer_hold = measure_buffer_hold(); session.log("debug", "Buffer flushed, resuming inactive mode (queue size was %d)", session.csi_counter); session.csi_counter = 0; end session.csi_resume = nil; end module:hook("c2s-ondrain", function (event) local session = event.session; if (session.state == "flushing" or session.state == "inactive") and session.conn and session.conn.pause_writes then -- After flushing, remain in pseudo-flushing state for a moment to allow -- some followup traffic, iq replies, smacks acks to be sent without having -- to go back and forth between inactive and flush mode. if not session.csi_resume then session.csi_resume = timer.add_task(resume_delay, resume_optimizations, session); end -- Should further noise in this short grace period push back the delay? -- Probably not great if the session can be kept in pseudo-active mode -- indefinitely. end end); function module.load() for _, user_session in pairs(prosody.hosts[module.host].sessions) do for _, session in pairs(user_session.sessions) do if session.state == "inactive" then enable_optimizations(session); end end end end function module.unload() for _, user_session in pairs(prosody.hosts[module.host].sessions) do for _, session in pairs(user_session.sessions) do if session.state and session.state ~= "active" then disable_optimizations(session); end end end end function module.command(arg) if arg[1] ~= "test" then print("Usage: "..module.name.." test < test-stream.xml") print(""); print("Provide a series of stanzas to test against importance algorithm"); return 1; end -- luacheck: ignore 212/self 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) local important, because = is_important(stanza); print("--"); print(stanza:indent(nil, " ")); -- :pretty_print() maybe? if important then print((because or "unspecified reason").. " -> important"); else print((because or "unspecified reason").. " -> unimportant"); end end local input_stream = xmppstream.new(input_session, stream_callbacks); input_stream:reset(); input_stream:feed(st.stanza("stream", { xmlns = "jabber:client" }):top_tag()); input_session.notopen = nil; for line in io.lines() do input_stream:feed(line); end end prosody-13.0.1/plugins/PaxHeaders/mod_debug_reset.lua0000644000000000000000000000011714773555365017713 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.599710566 prosody-13.0.1/plugins/mod_debug_reset.lua0000644000175000017500000000176614773555365022124 0ustar00prosodyprosody00000000000000-- 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; }; prosody-13.0.1/plugins/PaxHeaders/mod_debug_sql.lua0000644000000000000000000000011714773555365017370 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.599710566 prosody-13.0.1/plugins/mod_debug_sql.lua0000644000175000017500000000070014773555365021564 0ustar00prosodyprosody00000000000000-- Enables SQL query logging -- -- luacheck: ignore 213/uri module:set_global(); local engines = module:shared("/*/sql/connections"); for uri, engine in pairs(engines) do engine:debug(true); end setmetatable(engines, { __newindex = function (t, uri, engine) engine:debug(true); rawset(t, uri, engine); end }); function module.unload() setmetatable(engines, nil); for uri, engine in pairs(engines) do engine:debug(false); end end prosody-13.0.1/plugins/PaxHeaders/mod_debug_stanzas0000644000000000000000000000013114773555365017470 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.599710566 30 ctime=1743706869.599710566 prosody-13.0.1/plugins/mod_debug_stanzas/0000755000175000017500000000000014773555365021750 5ustar00prosodyprosody00000000000000prosody-13.0.1/plugins/mod_debug_stanzas/PaxHeaders/watcher.lib.lua0000644000000000000000000000011714773555365022456 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.599710566 prosody-13.0.1/plugins/mod_debug_stanzas/watcher.lib.lua0000644000175000017500000001500514773555365024656 0ustar00prosodyprosody00000000000000local 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; }; prosody-13.0.1/plugins/PaxHeaders/mod_dialback.lua0000644000000000000000000000011714773555365017155 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.603710582 prosody-13.0.1/plugins/mod_dialback.lua0000644000175000017500000001604214773555365021357 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local hosts = _G.hosts; local log = module._log; 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"; local dialback_requests = setmetatable({}, { __mode = 'v' }); local dialback_secret = sha256_hash(module:get_option_string("dialback_secret", uuid_gen()), true); function module.save() return { dialback_secret = dialback_secret }; end function module.restore(state) dialback_secret = state.dialback_secret; end function generate_dialback(id, to, from) return sha256_hmac(dialback_secret, to .. ' ' .. from .. ' ' .. id, true); end function initiate_dialback(session) -- generate dialback key session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host); session.sends2s(st.stanza("db:result", { from = session.from_host, to = session.to_host }):text(session.dialback_key)); session.log("debug", "sent dialback key on outgoing s2s stream"); end function verify_dialback(id, to, from, key) return secure_equals(key, generate_dialback(id, to, from)); end module:hook("stanza/jabber:server:dialback:verify", function(event) local origin, stanza = event.origin, event.stanza; if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then -- We are being asked to verify the key, to ensure it was generated by us origin.log("debug", "verifying that dialback key is ours..."); local attr = stanza.attr; if attr.type then module:log("warn", "Ignoring incoming session from %s claiming a dialback key for %s is %s", origin.from_host or "(unknown)", attr.from or "(unknown)", attr.type); return true; end -- COMPAT: Grr, ejabberd breaks this one too?? it is black and white in XEP-220 example 34 --if attr.from ~= origin.to_host then error("invalid-from"); end local type; if verify_dialback(attr.id, attr.from, attr.to, stanza[1]) then type = "valid" else type = "invalid" origin.log("warn", "Asked to verify a dialback key that was incorrect. An imposter is claiming to be %s?", attr.to); end origin.log("debug", "verified dialback key... it is %s", type); origin.sends2s(st.stanza("db:verify", { from = attr.to, to = attr.from, id = attr.id, type = type }):text(stanza[1])); return true; end end); module:hook("stanza/jabber:server:dialback:result", function(event) local origin, stanza = event.origin, event.stanza; if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then -- They want to be identified through dialback -- We need to check the key with the Authoritative server local attr = stanza.attr; if not attr.to or not attr.from then origin.log("debug", "Missing Dialback addressing (from=%q, to=%q)", attr.from, attr.to); origin:close("improper-addressing"); return true; end local to, from = nameprep(attr.to), nameprep(attr.from); if not hosts[to] then -- Not a host that we serve origin.log("warn", "%s tried to connect to %s, which we don't serve", from, to); origin:close("host-unknown"); return true; elseif not from then origin:close("improper-addressing"); return true; end origin.hosts[from] = { dialback_key = stanza[1] }; dialback_requests[from.."/"..origin.streamid] = origin; -- COMPAT: ejabberd, gmail and perhaps others do not always set 'to' and 'from' -- on streams. We fill in the session's to/from here instead. if not origin.from_host then origin.from_host = from; end if not origin.to_host then origin.to_host = to; end origin.log("debug", "asking %s if key %s belongs to them", from, stanza[1]); module:fire_event("route/remote", { from_host = to, to_host = from; stanza = st.stanza("db:verify", { from = to, to = from, id = origin.streamid }):text(stanza[1]); }); return true; end end); module:hook("stanza/jabber:server:dialback:verify", function(event) local origin, stanza = event.origin, event.stanza; if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then local attr = stanza.attr; local dialback_verifying = dialback_requests[attr.from.."/"..(attr.id or "")]; if dialback_verifying and attr.from == origin.to_host then local valid; if attr.type == "valid" then module:fire_event("s2s-authenticated", { session = dialback_verifying, host = attr.from, mechanism = "dialback" }); valid = "valid"; else -- Warn the original connection that is was not verified successfully log("warn", "authoritative server for %s denied the key", attr.from or "(unknown)"); valid = "invalid"; end if dialback_verifying.destroyed then log("warn", "Incoming s2s session %s was closed in the meantime, so we can't notify it of the dialback result", tostring(dialback_verifying):match("%w+$")); else dialback_verifying.sends2s( st.stanza("db:result", { from = attr.to, to = attr.from, id = attr.id, type = valid }) :text(dialback_verifying.hosts[attr.from].dialback_key)); end dialback_requests[attr.from.."/"..(attr.id or "")] = nil; end return true; end end); module:hook("stanza/jabber:server:dialback:result", function(event) local origin, stanza = event.origin, event.stanza; if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then -- Remote server is telling us whether we passed dialback local attr = stanza.attr; if not hosts[attr.to] then origin:close("host-unknown"); return true; elseif hosts[attr.to].s2sout[attr.from] ~= origin then -- This isn't right origin:close("invalid-id"); return true; end if stanza.attr.type == "valid" then module:fire_event("s2s-authenticated", { session = origin, host = attr.from, mechanism = "dialback" }); else origin:close("not-authorized", "dialback authentication failed"); end return true; end end); module:hook_tag("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza) -- luacheck: ignore 212/stanza if origin.external_auth == "failed" then module:log("debug", "SASL EXTERNAL failed, falling back to dialback"); initiate_dialback(origin); return true; end end, 100); module:hook_tag(xmlns_stream, "features", function (origin, stanza) -- luacheck: ignore 212/stanza if not origin.external_auth or origin.external_auth == "failed" then module:log("debug", "Initiating dialback..."); initiate_dialback(origin); return true; end end, 100); module:hook("s2sout-authenticate-legacy", function (event) module:log("debug", "Initiating dialback..."); initiate_dialback(event.origin); return true; end, 100); -- Offer dialback to incoming hosts module:hook("s2s-stream-features", function (data) data.features:tag("dialback", { xmlns='urn:xmpp:features:dialback' }):up(); end); prosody-13.0.1/plugins/PaxHeaders/mod_disco.lua0000644000000000000000000000011714773555365016524 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.603710582 prosody-13.0.1/plugins/mod_disco.lua0000644000175000017500000002320614773555365020726 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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); local disco_items = module:get_option_array("disco_items", {}) do -- validate disco_items for _, item in ipairs(disco_items) do local err; if type(item) ~= "table" then err = "item is not a table"; elseif type(item[1]) ~= "string" then err = "item jid is not a string"; elseif item[2] and type(item[2]) ~= "string" then err = "item name is not a string"; end if err then module:log("error", "option disco_items is malformed: %s", err); disco_items = {}; -- TODO clean up data instead of removing it? break; end end end if module:get_host_type() == "local" then module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the nonexisting mod_router end module:add_feature("http://jabber.org/protocol/disco#info"); module:add_feature("http://jabber.org/protocol/disco#items"); -- Generate and cache disco result and caps hash local _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash; local function build_server_disco_info() local query = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" }); local done = {}; for _,identity in ipairs(module:get_host_items("identity")) do local identity_s = identity.category.."\0"..identity.type; if not done[identity_s] then query:tag("identity", identity):up(); done[identity_s] = true; end end for _,feature in ipairs(module:get_host_items("feature")) do if not done[feature] then query:tag("feature", {var=feature}):up(); done[feature] = true; end end for _,extension in ipairs(module:get_host_items("extension")) do if not done[extension] then query:add_child(extension); done[extension] = true; end end _cached_server_disco_info = query; _cached_server_caps_hash = calculate_hash(query); _cached_server_caps_feature = st.stanza("c", { xmlns = "http://jabber.org/protocol/caps"; hash = "sha-1"; node = "http://prosody.im"; ver = _cached_server_caps_hash; }); end local function clear_disco_cache() _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash = nil, nil, nil; end local function get_server_disco_info() if not _cached_server_disco_info then build_server_disco_info(); end return _cached_server_disco_info; end local function get_server_caps_feature() if not _cached_server_caps_feature then build_server_disco_info(); end return _cached_server_caps_feature; end local function get_server_caps_hash() if not _cached_server_caps_hash then build_server_disco_info(); end return _cached_server_caps_hash; end module:hook("item-added/identity", clear_disco_cache); module:hook("item-added/feature", clear_disco_cache); module:hook("item-added/extension", clear_disco_cache); module:hook("item-removed/identity", clear_disco_cache); module:hook("item-removed/feature", clear_disco_cache); module:hook("item-removed/extension", clear_disco_cache); -- Handle disco requests to the server module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function(event) local origin, stanza = event.origin, event.stanza; local node = stanza.tags[1].attr.node; if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node}); local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; local ret = module:fire_event("host-disco-info-node", node_event); if ret ~= nil then return ret; end if node_event.exists then origin.send(reply); else origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); end return true; end local reply_query = get_server_disco_info(); reply_query.attr.node = node; local reply = st.reply(stanza):add_child(reply_query); origin.send(reply); return true; end); module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function(event) local origin, stanza = event.origin, event.stanza; local node = stanza.tags[1].attr.node; if node and node ~= "" then local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node}); local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; local ret = module:fire_event("host-disco-items-node", node_event); if ret ~= nil then return ret; end if node_event.exists then origin.send(reply); else origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); end return true; end local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); local ret = module:fire_event("host-disco-items", { origin = origin, stanza = stanza, reply = reply }); if ret ~= nil then return ret; end for jid, name in pairs(get_children(module.host)) do reply:tag("item", {jid = jid, name = name~=true and name or nil}):up(); end for _, item in ipairs(disco_items) do reply:tag("item", {jid=item[1], name=item[2]}):up(); end origin.send(reply); return true; end); -- Handle caps stream feature module:hook("stream-features", function (event) if event.origin.type == "c2s" or event.origin.type == "c2s_unbound" then event.features:add_child(get_server_caps_feature()); end end); module:hook("s2s-stream-features", function (event) if event.origin.type == "s2sin" then event.features:add_child(get_server_caps_feature()); 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 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(); reply:tag("feature", { var = "http://jabber.org/protocol/disco#items" }):up(); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; local ret = module:fire_event("account-disco-info-node", node_event); if ret ~= nil then return ret; end if node_event.exists then origin.send(reply); else origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); end return true; 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 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(); else reply:tag('identity', {category='account', type='registered'}):up(); end reply:tag("feature", { var = "http://jabber.org/protocol/disco#info" }):up(); reply:tag("feature", { var = "http://jabber.org/protocol/disco#items" }):up(); module:fire_event("account-disco-info", { origin = origin, reply = reply }); origin.send(reply); return true; end end); module:hook("iq-get/bare/http://jabber.org/protocol/disco#items: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; if not stanza.attr.to 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#items', node=node}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; local ret = module:fire_event("account-disco-items-node", node_event); if ret ~= nil then return ret; end if node_event.exists then origin.send(reply); else origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); end return true; end local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items'}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account module:fire_event("account-disco-items", { origin = origin, stanza = stanza, reply = reply }); origin.send(reply); return true; end end); prosody-13.0.1/plugins/PaxHeaders/mod_external_services.lua0000644000000000000000000000011714773555365021150 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.603710582 prosody-13.0.1/plugins/mod_external_services.lua0000644000175000017500000001625314773555365023356 0ustar00prosodyprosody00000000000000 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_integer("external_service_port", nil, 1, 65535); local default_secret = module:get_option_string("external_service_secret"); 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://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 ttl = item.ttl; end local expires = srv.expires or os.time() + ttl; local username; if type(item.username) == "string" then username = string.format("%d:%s", expires, item.username); else username = string.format("%d", expires); end srv.username = username; srv.password = base64.encode(hashes.hmac_sha1(secret, srv.username)); end local algorithms = { turn = behave_turn_rest_credentials; turns = behave_turn_rest_credentials; } -- filter config into well-defined service records local function prepare(item) if type(item) ~= "table" then module:log("error", "Service definition is not a table: %q", item); return nil; end local srv = { type = nil; transport = nil; host = default_host; port = default_port; username = nil; password = nil; restricted = nil; expires = nil; }; if type(item.type) == "string" then srv.type = item.type; else module:log("error", "Service missing mandatory 'type' field: %q", item); return nil; end if type(item.transport) == "string" then srv.transport = item.transport; else module:log("warn", "Service missing recommended 'transport' field: %q", item); end if type(item.host) == "string" then srv.host = item.host; end if type(item.port) == "number" then srv.port = item.port; elseif not srv.port then module:log("warn", "Service missing recommended 'port' field: %q", item); end if type(item.username) == "string" then srv.username = item.username; end if type(item.password) == "string" then srv.password = item.password; srv.restricted = true; end if item.restricted == true then srv.restricted = true; end if type(item.expires) == "number" then srv.expires = item.expires; elseif type(item.ttl) == "number" then srv.expires = os.time() + item.ttl; end if (item.secret == true and default_secret) or type(item.secret) == "string" then local secret_cb = item.credentials_cb or algorithms[item.algorithm] or algorithms[srv.type]; local secret = item.secret; if secret == true then secret = default_secret; end if secret_cb then secret_cb(srv, item, secret); srv.restricted = true; end end return srv; end function module.load() -- Trigger errors on startup local extras = module:get_host_items("external_service"); local services = ( configured_services + extras ) / prepare; if #services == 0 then module:set_status("warn", "No services configured or all had errors"); end end module:handle_items("external_service", function(added) if prepare(added.item) then module:set_status("core", "OK"); end end, module.load); -- Ensure only valid items are added in events local services_mt = { __index = getmetatable(array()).__index; __newindex = function (self, i, v) rawset(self, i, assert(prepare(v), "Invalid service entry added")); end; } function get_services() local extras = module:get_host_items("external_service"); local services = ( configured_services + extras ) / prepare; setmetatable(services, services_mt); return services; end function services_xml(services, name, namespace) local reply = st.stanza(name or "services", { xmlns = namespace or "urn:xmpp:extdisco:2" }); for _, srv in ipairs(services) do reply:tag("service", { type = srv.type; transport = srv.transport; host = srv.host; port = srv.port and string.format("%d", srv.port) or nil; username = srv.username; password = srv.password; expires = srv.expires and dt.datetime(srv.expires) or nil; restricted = srv.restricted and "1" or nil; }):up(); end return reply; end local function handle_services(event) local origin, stanza = event.origin, event.stanza; local action = stanza.tags[1]; local user_bare = jid.bare(stanza.attr.from); local user_host = jid.host(user_bare); if not ((access:empty() and origin.type == "c2s") or access:contains(user_bare) or access:contains(user_host)) then origin.send(st.error_reply(stanza, "auth", "forbidden")); return true; end local services = get_services(); local requested_type = action.attr.type; if requested_type then services:filter(function(item) return item.type == requested_type; end); end module:fire_event("external_service/services", { origin = origin; stanza = stanza; requested_type = requested_type; services = services; }); local reply = st.reply(stanza):add_child(services_xml(services, action.name, action.attr.xmlns)); origin.send(reply); return true; end local function handle_credentials(event) local origin, stanza = event.origin, event.stanza; local action = stanza.tags[1]; if origin.type ~= "c2s" then origin.send(st.error_reply(stanza, "auth", "forbidden")); return true; end local services = get_services(); services:filter(function (item) return item.restricted; end) local requested_credentials = set.new(); for service in action:childtags("service") do if not service.attr.type or not service.attr.host then origin.send(st.error_reply(stanza, "modify", "bad-request", "The 'port' and 'type' attributes are required.")); return true; end requested_credentials:add(string.format("%s:%s:%d", service.attr.type, service.attr.host, tonumber(service.attr.port) or 0)); end module:fire_event("external_service/credentials", { origin = origin; stanza = stanza; requested_credentials = requested_credentials; services = services; }); services:filter(function (srv) local port_key = string.format("%s:%s:%d", srv.type, srv.host, srv.port or 0); local portless_key = string.format("%s:%s:%d", srv.type, srv.host, 0); return requested_credentials:contains(port_key) or requested_credentials:contains(portless_key); end); local reply = st.reply(stanza):add_child(services_xml(services, action.name, action.attr.xmlns)); origin.send(reply); return true; end -- XEP-0215 v0.7 module:add_feature("urn:xmpp:extdisco:2"); module:hook("iq-get/host/urn:xmpp:extdisco:2:services", handle_services); module:hook("iq-get/host/urn:xmpp:extdisco:2:credentials", handle_credentials); -- COMPAT XEP-0215 v0.6 -- Those still on the old version gets to deal with undefined attributes until they upgrade. module:add_feature("urn:xmpp:extdisco:1"); module:hook("iq-get/host/urn:xmpp:extdisco:1:services", handle_services); module:hook("iq-get/host/urn:xmpp:extdisco:1:credentials", handle_credentials); prosody-13.0.1/plugins/PaxHeaders/mod_flags.lua0000644000000000000000000000011714773555365016517 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.603710582 prosody-13.0.1/plugins/mod_flags.lua0000644000175000017500000000734214773555365020724 0ustar00prosodyprosody00000000000000local jid_node = require "prosody.util.jid".node; local flags = module:open_store("account_flags", "keyval+"); -- API function add_flag(username, flag, comment) local flag_data = { when = os.time(); comment = comment; }; local ok, err = flags:set_key(username, flag, flag_data); if not ok then return nil, err; end module:fire_event("user-flag-added/"..flag, { user = username; flag = flag; data = flag_data; }); return true; end function remove_flag(username, flag) local ok, err = flags:set_key(username, flag, nil); if not ok then return nil, err; end module:fire_event("user-flag-removed/"..flag, { user = username; flag = flag; }); return true; end function has_flag(username, flag) -- luacheck: ignore 131/has_flag local ok, err = flags:get_key(username, flag); if not ok and err then error("Failed to check flags for user: "..err); end return not not ok; end function get_flag_info(username, flag) -- luacheck: ignore 131/get_flag_info return flags:get_key(username, flag); end -- Shell commands local function get_username(jid) return (assert(jid_node(jid), "please supply a valid user JID")); end module:add_item("shell-command", { section = "flags"; section_desc = "View and manage flags on user accounts"; name = "list"; desc = "List flags for the given user account"; args = { { name = "jid", type = "string" }; }; host_selector = "jid"; handler = function(self, jid) --luacheck: ignore 212/self local c = 0; local user_flags, err = flags:get(get_username(jid)); if not user_flags and err then return false, "Unable to list flags: "..err; end if user_flags then local print = self.session.print; for flag_name, flag_data in pairs(user_flags) do print(flag_name, os.date("%Y-%m-%d %R", flag_data.when), flag_data.comment); c = c + 1; end end return true, ("%d flags listed"):format(c); end; }); module:add_item("shell-command", { section = "flags"; section_desc = "View and manage flags on user accounts"; name = "add"; desc = "Add a flag to the given user account, with optional comment"; args = { { name = "jid", type = "string" }; { name = "flag", type = "string" }; { name = "comment", type = "string" }; }; host_selector = "jid"; handler = function(self, jid, flag, comment) --luacheck: ignore 212/self local username = get_username(jid); local ok, err = add_flag(username, flag, comment); if not ok then return false, "Failed to add flag: "..err; end return true, "Flag added"; end; }); module:add_item("shell-command", { section = "flags"; section_desc = "View and manage flags on user accounts"; name = "remove"; desc = "Remove a flag from the given user account"; args = { { name = "jid", type = "string" }; { name = "flag", type = "string" }; }; host_selector = "jid"; handler = function(self, jid, flag) --luacheck: ignore 212/self local username = get_username(jid); local ok, err = remove_flag(username, flag); if not ok then return false, "Failed to remove flag: "..err; end return true, "Flag removed"; end; }); module:add_item("shell-command", { section = "flags"; section_desc = "View and manage flags on user accounts"; name = "find"; desc = "Find all user accounts with a given flag on the specified host"; args = { { name = "host", type = "string" }; { name = "flag", type = "string" }; }; host_selector = "host"; handler = function(self, host, flag) --luacheck: ignore 212/self 212/host local users_with_flag = flags:get_key_from_all(flag); local print = self.session.print; local c = 0; for user, flag_data in pairs(users_with_flag) do print(user, os.date("%Y-%m-%d %R", flag_data.when), flag_data.comment); c = c + 1; end return true, ("%d accounts listed"):format(c); end; }); prosody-13.0.1/plugins/PaxHeaders/mod_groups.lua0000644000000000000000000000011714773555365016742 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.603710582 prosody-13.0.1/plugins/mod_groups.lua0000644000175000017500000000733514773555365021151 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local groups; local members; local datamanager = require "prosody.util.datamanager"; local jid_prep = require "prosody.util.jid".prep; local module_host = module:get_host(); function inject_roster_contacts(event) local username, host= event.username, event.host; --module:log("debug", "Injecting group members to roster"); local bare_jid = username.."@"..host; if not members[bare_jid] and not members[false] then return; end -- Not a member of any groups local roster = event.roster; local function import_jids_to_roster(group_name) for jid in pairs(groups[group_name]) do -- Add them to roster --module:log("debug", "processing jid %s in group %s", jid, group_name); if jid ~= bare_jid then if not roster[jid] then roster[jid] = {}; end roster[jid].subscription = "both"; if groups[group_name][jid] then roster[jid].name = groups[group_name][jid]; end if not roster[jid].groups then roster[jid].groups = { [group_name] = true }; end roster[jid].groups[group_name] = true; roster[jid].persist = false; end end end -- Find groups this JID is a member of if members[bare_jid] then for _, group_name in ipairs(members[bare_jid]) do --module:log("debug", "Importing group %s", group_name); import_jids_to_roster(group_name); end end -- Import public groups if members[false] then for _, group_name in ipairs(members[false]) do --module:log("debug", "Importing group %s", group_name); import_jids_to_roster(group_name); end end if roster[false] then roster[false].version = true; end end function remove_virtual_contacts(username, host, datastore, data) if host == module_host and datastore == "roster" then local new_roster = {}; for jid, contact in pairs(data) do if contact.persist ~= false then new_roster[jid] = contact; end end if new_roster[false] then new_roster[false].version = nil; -- Version is void end return username, host, datastore, new_roster; end return username, host, datastore, data; end function module.load() local groups_file = module:get_option_path("groups_file", nil, "config"); if not groups_file then return; end module:hook("roster-load", inject_roster_contacts); datamanager.add_callback(remove_virtual_contacts); groups = { default = {} }; members = { }; local curr_group = "default"; for line in io.lines(groups_file) do if line:match("^%s*%[.-%]%s*$") then curr_group = line:match("^%s*%[(.-)%]%s*$"); if curr_group:match("^%+") then curr_group = curr_group:gsub("^%+", ""); if not members[false] then members[false] = {}; end members[false][#members[false]+1] = curr_group; -- Is a public group end module:log("debug", "New group: %s", curr_group); groups[curr_group] = groups[curr_group] or {}; else -- Add JID local entryjid, name = line:match("([^=]*)=?(.*)"); module:log("debug", "entryjid = '%s', name = '%s'", entryjid, name); local jid; jid = jid_prep(entryjid:match("%S+")); if jid then module:log("debug", "New member of %s: %s", curr_group, jid); groups[curr_group][jid] = name or false; members[jid] = members[jid] or {}; members[jid][#members[jid]+1] = curr_group; elseif entryjid:match("%S") then module:log("warn", "Invalid JID: %q", entryjid); end end end module:log("info", "Groups loaded successfully"); end function module.unload() datamanager.remove_callback(remove_virtual_contacts); end -- Public for other modules to access function group_contains(group_name, jid) return groups[group_name][jid]; end prosody-13.0.1/plugins/PaxHeaders/mod_http.lua0000644000000000000000000000011714773555365016402 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.607710598 prosody-13.0.1/plugins/mod_http.lua0000644000175000017500000003557214773555365020615 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2012 Matthew Wild -- Copyright (C) 2008-2012 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- module:set_global(); pcall(function () module:depends("http_errors"); end); 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 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 "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 "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", 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", {}); local opt_methods = module:get_option_set("access_control_allow_methods", { "GET", "OPTIONS" }); local opt_headers = module:get_option_set("access_control_allow_headers", { "Content-Type" }); local opt_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_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) local method, path = key:match("^(%S+)%s+(.+)$"); if not method then -- No path specified, default to "" (base path) method, path = key, ""; end if method:sub(1,1) == "/" then return nil; end if app_path == "/" and path:sub(1,1) == "/" then app_path = ""; end if host == "*" then return method:upper().." "..app_path..path; else return method:upper().." "..host..app_path..path; end end local function get_base_path(host_module, app_name, default_app_path) return (normalize_path(host_module:get_option("http_paths", {})[app_name] -- Host or module:get_option("http_paths", {})[app_name] -- Global or default_app_path)) -- Default :gsub("%$(%w+)", { host = host_module.host }); end local function redir_handler(event) event.response.headers.location = event.request.path.."/"; if event.request.url.query then event.response.headers.location = event.response.headers.location .. "?" .. event.request.url.query end return 301; 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, 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 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; port = tonumber(external_url.port) or ports_by_scheme[external_url.scheme]; path = normalize_path(external_url.path or "/", true) .. (get_base_path(module, app_name, default_path or "/" .. app_name):sub(2)); } if ports_by_scheme[url.scheme] == url.port then url.port = nil end 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 for port, service in pairs(ports) do -- luacheck: ignore 512 local url = { scheme = service[1].service.name; host = module:get_option_string("http_host", module.global and module:get_option_string("http_default_host", interface) 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 end if prosody.process_type == "prosody" then module:log("warn", "No http ports enabled, can't generate an external URL"); end 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 = 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 response.headers.access_control_allow_credentials = "true"; end end function module.add_host(module) local host = module.host; if host ~= "*" then host = module:get_option_string("http_host", host); end local apps = {}; module.environment.apps = apps; local function http_app_added(event) local app_name = event.item.name; local default_app_path = event.item.default_path or "/"..app_name; local app_path = get_base_path(module, app_name, default_app_path); if not app_name then -- TODO: Link to docs module:log("error", "HTTP app has no 'name', add one or use module:provides('http', app)"); return; end apps[app_name] = apps[app_name] or {}; local app_handlers = apps[app_name]; local app_methods = opt_methods; local app_headers = opt_headers; local app_credentials = opt_credentials; local app_origins; if opt_origins and not (opt_origins:empty() or opt_origins:contains("*")) then app_origins = opt_origins._items; end local function cors_handler(event_data) local request, response = event_data.request, event_data.response; apply_cors_headers(response, app_methods, app_headers, opt_max_age, app_credentials, app_origins, request.headers.origin); end local function options_handler(event_data) cors_handler(event_data); return ""; end local cors = cors_overrides[app_name] or event.item.cors; if cors then if cors.enabled == true then if cors.credentials ~= nil then app_credentials = cors.credentials; end if cors.headers then for header, enable in pairs(cors.headers) do if enable and not app_headers:contains(header) then app_headers = app_headers + set.new { header }; elseif not enable and app_headers:contains(header) then app_headers = app_headers - set.new { header }; end end end if cors.origins then if cors.origins == "*" or cors.origins[1] == "*" then app_origins = nil; else app_origins = set.new(cors.origins)._items; end end elseif cors.enabled == false then cors = nil; end else cors = opt_default_cors; end local streaming = event.item.streaming_uploads; if not event.item.route then -- TODO: Link to docs module:log("error", "HTTP app %q provides no 'route', add one to handle HTTP requests", app_name); return; end for key, handler in pairs(event.item.route) do local event_name = get_http_event(host, app_path, key); if event_name then local method = event_name:match("^%S+"); if not app_methods:contains(method) then app_methods = app_methods + set.new{ method }; end local options_event_name = event_name:gsub("^%S+", "OPTIONS"); if type(handler) ~= "function" then local data = handler; handler = function () return data; end elseif event_name:sub(-2, -1) == "/*" then local base_path_len = #event_name:match("/.+$"); local _handler = handler; handler = function (_event) local path = _event.request.path:sub(base_path_len); return _handler(_event, path); end; module:hook_object_event(server, event_name:sub(1, -3), redir_handler, -1); elseif event_name:sub(-1, -1) == "/" then module:hook_object_event(server, event_name:sub(1, -2), redir_handler, -1); end if not streaming then -- COMPAT Modules not compatible with streaming uploads behave as before. local _handler = handler; function handler(event) -- luacheck: ignore 432/event if event.request.body ~= false then return _handler(event); end end end if not app_handlers[event_name] then app_handlers[event_name] = { main = handler; cors = cors and cors_handler; options = cors and options_handler; }; module:hook_object_event(server, event_name, handler); if cors then module:hook_object_event(server, event_name, cors_handler, 1); module:hook_object_event(server, options_event_name, options_handler, -1); end else module:log("warn", "App %s added handler twice for '%s', ignoring", app_name, event_name); end else module:log("error", "Invalid route in %s, %q. See https://prosody.im/doc/developers/http#routes", app_name, key); end end local services = portmanager.get_active_services(); if services:get("https") or services:get("http") then module:log("info", "Serving '%s' at %s", app_name, module:http_url(app_name, app_path)); elseif prosody.process_type == "prosody" then module:log("error", "Not listening on any ports, '%s' will be unreachable", app_name); end end local function http_app_removed(event) local app_handlers = apps[event.item.name]; apps[event.item.name] = nil; for event_name, handlers in pairs(app_handlers) do module:unhook_object_event(server, event_name, handlers.main); if handlers.cors then module:unhook_object_event(server, event_name, handlers.cors); end if event_name:sub(-2, -1) == "/*" then module:unhook_object_event(server, event_name:sub(1, -3), redir_handler, -1); elseif event_name:sub(-1, -1) == "/" then module:unhook_object_event(server, event_name:sub(1, -2), redir_handler, -1); end if handlers.options then local options_event_name = event_name:gsub("^%S+", "OPTIONS"); module:unhook_object_event(server, options_event_name, handlers.options); end end end module:handle_items("http-provider", http_app_added, http_app_removed); if host ~= "*" then server.add_host(host); function module.unload() server.remove_host(host); end end end 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 local parsed_ip = new_ip(ip) for trusted_proxy in trusted_proxies do if match_ip(parsed_ip, parse_cidr(trusted_proxy)) then return true; end end return false 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 -- This logic looks weird at first, but it makes sense. -- The for loop will take the last non-trusted-proxy IP from `forwarded_for`. -- We append the original request IP to the header. Then, since the last IP wins, there are two cases: -- Case a) The original request IP is *not* in trusted proxies, in which case the X-Forwarded-For header will, effectively, be ineffective; the original request IP will win because it overrides any other IP in the header. -- Case b) The original request IP is in trusted proxies. In that case, the if branch in the for loop will skip the last IP, causing it to be ignored. The second-to-last IP will be taken instead. -- Case c) If the second-to-last IP is also a trusted proxy, it will also be ignored, iteratively, up to the last IP which isn’t in trusted proxies. -- Case d) If all IPs are in trusted proxies, something went obviously wrong and the logic never overwrites `ip`, leaving it at the original request IP. forwarded_for = forwarded_for..", "..ip; for forwarded_ip in forwarded_for:gmatch("[^%s,]+") do if not is_trusted_proxy(forwarded_ip) then ip = forwarded_ip; end end end secure = secure or request.headers.x_forwarded_proto == "https"; return ip, secure; end end module:wrap_object_event(server._events, false, function (handlers, event_name, event_data) local request = event_data.request; if request and is_trusted_proxy(request.ip) then -- Not included in eg http-error events request.ip, request.secure = get_forwarded_connection_info(request); end return handlers(event_name, event_data); end); module:provides("net", { name = "http"; listener = server.listener; private = true; default_port = 5280; multiplex = { pattern = "^[A-Z]"; }; }); module:provides("net", { name = "https"; listener = server.listener; default_port = 5281; encryption = "ssl"; multiplex = { protocol = "http/1.1"; pattern = "^[A-Z]"; }; }); prosody-13.0.1/plugins/PaxHeaders/mod_http_altconnect.lua0000644000000000000000000000011714773555365020614 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.607710598 prosody-13.0.1/plugins/mod_http_altconnect.lua0000644000175000017500000000337614773555365023024 0ustar00prosodyprosody00000000000000-- mod_http_altconnect -- XEP-0156: Discovering Alternative XMPP Connection Methods module:depends"http"; local mm = require "prosody.core.modulemanager"; local json = require"prosody.util.json"; local st = require"prosody.util.stanza"; local array = require"prosody.util.array"; local advertise_bosh = module:get_option_boolean("advertise_bosh", true); local advertise_websocket = module:get_option_boolean("advertise_websocket", true); local function get_supported() local uris = array(); if advertise_bosh and (mm.is_loaded(module.host, "bosh") or mm.is_loaded("*", "bosh")) then uris:push({ rel = "urn:xmpp:alt-connections:xbosh", href = module:http_url("bosh", "/http-bind") }); end if advertise_websocket and (mm.is_loaded(module.host, "websocket") or mm.is_loaded("*", "websocket")) then uris:push({ rel = "urn:xmpp:alt-connections:websocket", href = module:http_url("websocket", "xmpp-websocket"):gsub("^http", "ws") }); end return uris; end local function GET_xml(event) local response = event.response; local xrd = st.stanza("XRD", { xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0' }); local uris = get_supported(); for _, method in ipairs(uris) do xrd:tag("Link", method):up(); end response.headers.content_type = "application/xrd+xml" response.headers.access_control_allow_origin = "*"; return '' .. tostring(xrd); end local function GET_json(event) local response = event.response; local jrd = { links = get_supported() }; response.headers.content_type = "application/json" response.headers.access_control_allow_origin = "*"; return json.encode(jrd); end; module:provides("http", { default_path = "/.well-known"; route = { ["GET /host-meta"] = GET_xml; ["GET /host-meta.json"] = GET_json; }; }); prosody-13.0.1/plugins/PaxHeaders/mod_http_errors.lua0000644000000000000000000000011714773555365017776 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.607710598 prosody-13.0.1/plugins/mod_http_errors.lua0000644000175000017500000000756114773555365022206 0ustar00prosodyprosody00000000000000module:set_global(); 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); local default_message = { module:get_option_string("http_errors_default_message", "That's all I know.") }; local default_messages = { [400] = { "What kind of request do you call that??" }; [403] = { "You're not allowed to do that." }; [404] = { "Whatever you were looking for is not here. %"; "Where did you put it?", "It's behind you.", "Keep looking." }; [500] = { "% Check your error log for more info."; "Gremlins.", "It broke.", "Don't look at me." }; ["/"] = { "A study in simplicity."; "Better catch it!"; "Don't just stand there, go after it!"; "Well, say something, before it runs too far!"; "Welcome to the world of XMPP!"; "You can do anything in XMPP!"; -- "The only limit is XML."; "You can do anything with Prosody!"; -- the only limit is memory? }; }; local messages = setmetatable(module:get_option("http_errors_messages", {}), { __index = default_messages }); local html = [[ {title}

{icon?{icon_raw!?}} {title}

{message}

{warning&

⚠ {warning?} ⚠

} {extra&

{extra?}

} ]]; local function get_page(code, extra) local message = messages[code]; if always_serve or message then message = message or default_message; return render(html, { title = rawget(codes, code) or ("Code "..tostring(code)); message = message[1]:gsub("%%", function () return message[math.random(2, math.max(#message,2))]; end); extra = extra; }); end end -- Main error page handler module:hook_object_event(server, "http-error", function (event) if event.response then event.response.headers.content_type = "text/html; charset=utf-8"; end return get_page(event.code, (show_private and event.private_message) or event.message or (event.error and event.error.text)); end); -- Way to use the template for other things so to give a consistent appearance module:hook("http-message", function (event) if event.response then event.response.headers.content_type = "text/html; charset=utf-8"; end return render(html, event); end, -1); local icon = [[ ]]; -- Something nicer shown instead of 404 at the root path, if nothing else handles this path module:hook_object_event(server, "http-error", function (event) local request, response = event.request, event.response; if request and response and request.path == "/" and response.status_code == 404 then response.status_code = 200; response.headers.content_type = "text/html; charset=utf-8"; local message = messages["/"]; return render(html, { icon_raw = icon, title = "Prosody is running!"; message = message[math.random(#message)]; }); end end, 1); prosody-13.0.1/plugins/PaxHeaders/mod_http_file_share.lua0000644000000000000000000000011714773555365020563 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.607710598 prosody-13.0.1/plugins/mod_http_file_share.lua0000644000175000017500000005241614773555365022772 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2021 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- XEP-0363: HTTP File Upload -- Again, from the top! local t_insert = table.insert; local jid = require "prosody.util.jid"; local st = require "prosody.util.stanza"; local url = require "socket.url"; 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); local unlimited = math.huge; local namespace = "urn:xmpp:http:upload:0"; module:depends("disco"); module:add_identity("store", "file", module:get_option_string("name", "HTTP File Upload")); module:add_feature(namespace); local uploads = module:open_store("uploads", "archive"); local persist_stats = module:open_store("upload_stats", "map"); -- id, , time, owner 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_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_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 module:add_extension(dataform { { name = "FORM_TYPE", type = "hidden", value = namespace }, { name = "max-file-size", type = "text-single", datatype = "xs:integer" }, }:form({ ["max-file-size"] = file_size_limit }, "result")); local upload_errors = errors.init(module.name, namespace, { access = { type = "auth"; condition = "forbidden" }; filename = { type = "modify"; condition = "bad-request"; text = "Invalid filename" }; filetype = { type = "modify"; condition = "not-acceptable"; text = "File type not allowed" }; filesize = { code = 413; type = "modify"; condition = "not-acceptable"; text = "File too large"; extra = { tag = st.stanza("file-too-large", { xmlns = namespace }):tag("max-file-size"):text(tostring(file_size_limit)); }; }; filesizefmt = { type = "modify"; condition = "bad-request"; text = "File size must be positive integer"; }; quota = { type = "wait"; condition = "resource-constraint"; text = "Daily quota reached"; }; outofdisk = { type = "wait"; condition = "resource-constraint"; text = "Server global storage quota reached" }; authzmalformed = { code = 401; type = "auth"; condition = "not-authorized"; text = "Missing or malformed Authorization header"; }; unauthz = { code = 403; type = "auth"; condition = "forbidden"; text = "Unauthorized or invalid token" }; invalidslot = { code = 400; type = "modify"; condition = "bad-request"; text = "Invalid upload slot, must not contain '/'"; }; alreadycompleted = { code = 409; type = "cancel"; condition = "conflict"; text = "Upload already completed" }; writefail = { code = 500; type = "wait"; condition = "internal-server-error" } }); local upload_cache = cache.new(1024); local quota_cache = cache.new(1024); local total_storage_usage = unknown; local measure_upload_cache_size = module:measure("upload_cache", "amount"); local measure_quota_cache_size = module:measure("quota_cache", "amount"); local measure_total_storage_usage = module:measure("total_storage", "amount", { unit = "bytes" }); do local total, err = persist_stats:get(nil, "total"); if not err then total_storage_usage = tonumber(total) or 0; end end module:hook_global("stats-update", function () measure_upload_cache_size(upload_cache:count()); measure_quota_cache_size(quota_cache:count()); measure_total_storage_usage(total_storage_usage); end); local buckets = {}; for n = 10, 40, 2 do local exp = math.floor(2 ^ n); table.insert(buckets, exp); if exp >= file_size_limit then break end end local measure_uploads = module:measure("upload", "sizes", {buckets = buckets}); -- Convenience wrapper for logging file sizes local function B(bytes) if bytes ~= bytes then return "unknown" elseif bytes == unlimited then return "unlimited"; end return hi.format(bytes, "B", "b"); end local function get_filename(slot, create) return dm.getpath(slot, module.host, module.name, "bin", create) end function get_daily_quota(uploader) local now = os.time(); local max_age = now - 86400; local cached = quota_cache:get(uploader); if cached and cached.time > max_age then return cached.size; end local iter, err = uploads:find(nil, {with = uploader; start = max_age }); if not iter then return iter, err; end local total_bytes = 0; local oldest_upload = now; for _, slot, when in iter do local size = tonumber(slot.attr.size); if size then total_bytes = total_bytes + size; end if when < oldest_upload then oldest_upload = when; end end -- If there were no uploads then we end up caching [now, 0], which is fine -- since we increase the size on new uploads quota_cache:set(uploader, { time = oldest_upload, size = total_bytes }); return total_bytes; end function may_upload(uploader, filename, filesize, filetype) -- > boolean, error local uploader_host = jid.host(uploader); if not (module:may(":upload", uploader) or access:contains(uploader) or access:contains(uploader_host)) then return false, upload_errors.new("access"); end if not filename or filename:find"/" then -- On Linux, only '/' and '\0' are invalid in filenames and NUL can't be in XML return false, upload_errors.new("filename"); end if not filesize or filesize < 0 or filesize % 1 ~= 0 then return false, upload_errors.new("filesizefmt"); end if filesize > file_size_limit then return false, upload_errors.new("filesize"); end if total_storage_usage + filesize > total_storage_limit then module:log("warn", "Global storage quota reached, at %s / %s!", B(total_storage_usage), B(total_storage_limit)); return false, upload_errors.new("outofdisk"); end local uploader_quota = get_daily_quota(uploader); if uploader_quota + filesize > daily_quota then return false, upload_errors.new("quota"); end if not ( file_types:empty() or file_types:contains(filetype) or file_types:contains(filetype:gsub("/.*", "/*")) ) then return false, upload_errors.new("filetype"); end return true; end function get_authz(slot, uploader, filename, filesize, filetype) return create_jwt({ -- token properties sub = uploader; -- slot properties slot = slot; expires = expiry < math.huge and (os.time()+expiry) or nil; -- file properties filename = filename; filesize = filesize; filetype = filetype; }); end function get_url(slot, filename) local base_url = external_base_url or module:http_url(); local slot_url = url.parse(base_url); slot_url.path = url.parse_path(slot_url.path or "/"); t_insert(slot_url.path, slot); if filename then t_insert(slot_url.path, filename); slot_url.path.is_directory = false; else slot_url.path.is_directory = true; end slot_url.path = url.build_path(slot_url.path); return url.build(slot_url); end function handle_slot_request(event) local stanza, origin = event.stanza, event.origin; local request = st.clone(stanza.tags[1], true); local filename = request.attr.filename; local filesize = tonumber(request.attr.size); local filetype = request.attr["content-type"] or "application/octet-stream"; local uploader = jid.bare(stanza.attr.from); local may, why_not = may_upload(uploader, filename, filesize, filetype); if not may then origin.send(st.error_reply(stanza, why_not)); return true; end module:log("info", "Issuing upload slot to %s for %s", uploader, B(filesize)); local slot, storage_err = errors.coerce(uploads:append(nil, nil, request, os.time(), uploader)) if not slot then origin.send(st.error_reply(stanza, storage_err)); return true; end total_storage_usage = total_storage_usage + filesize; persist_stats:set(nil, "total", total_storage_usage); module:log("debug", "Total storage usage: %s / %s", B(total_storage_usage), B(total_storage_limit)); local cached_quota = quota_cache:get(uploader); if cached_quota and cached_quota.time > os.time()-86400 then cached_quota.size = cached_quota.size + filesize; quota_cache:set(uploader, cached_quota); end local authz = get_authz(slot, uploader, filename, filesize, filetype); local slot_url = get_url(slot, filename); local upload_url = slot_url; local reply = st.reply(stanza) :tag("slot", { xmlns = namespace }) :tag("get", { url = slot_url }):up() :tag("put", { url = upload_url }) :text_tag("header", "Bearer "..authz, {name="Authorization"}) :reset(); origin.send(reply); return true; end function handle_upload(event, path) -- PUT /upload/:slot local request = event.request; 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 upload_errors.new("authzmalformed", { request = request }); 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 upload_errors.new("unauthz", { request = request; wrapped_error = authed_upload_info }); 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 upload_errors.new("unauthz", { request = request }); end if request.headers.content_length and tonumber(request.headers.content_length) ~= authed_upload_info.filesize then return upload_errors.new("filesize", { request = request }); -- 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); do -- check if upload has been completed already -- we want to allow retry of a failed upload attempt, but not after it's been completed local f = io.open(filename, "r"); if f then f:close(); return upload_errors.new("alreadycompleted", { request = request }); end end if not request.body_sink then module:log("debug", "Preparing to receive upload into %q, expecting %s", filename, B(upload_info.filesize)); local fh, err = io.open(filename.."~", "w"); if not fh then module:log("error", "Could not open file for writing: %s", err); return upload_errors.new("writefail", { request = request; wrapped_error = err }); end function event.response:on_destroy() -- luacheck: ignore 212/self -- Clean up incomplete upload if io.type(fh) == "file" then -- still open fh:close(); os.remove(filename.."~"); end end request.body_sink = fh; if request.body == false then if request.headers.expect == "100-continue" then request.conn:write("HTTP/1.1 100 Continue\r\n\r\n"); end return true; end end if request.body then module:log("debug", "Complete upload available, %s", B(#request.body)); -- Small enough to have been uploaded already local written, err = errors.coerce(request.body_sink:write(request.body)); if not written then return err; end request.body = nil; end if request.body_sink then local final_size = request.body_sink:seek(); local uploaded, err = errors.coerce(request.body_sink:close()); if final_size ~= upload_info.filesize then -- Could be too short as well, but we say the same thing uploaded, err = false, upload_errors.new("filesize", { request = request }); end if uploaded then module:log("debug", "Upload of %q completed, %s", filename, B(final_size)); assert(os.rename(filename.."~", filename)); measure_uploads(final_size); upload_cache:set(upload_info.slot, { name = upload_info.filename; size = tostring(upload_info.filesize); type = upload_info.filetype; time = os.time(); }); return 201; else assert(os.remove(filename.."~")); return err; end end end local download_cache_hit = module:measure("download_cache_hit", "rate"); local download_cache_miss = module:measure("download_cache_miss", "rate"); function handle_download(event, path) -- GET /uploads/:slot+filename local request, response = event.request, event.response; local slot_id = path:match("^[^/]+"); local basename, filetime, filetype, filesize; local cached = upload_cache:get(slot_id); if cached then module:log("debug", "Cache hit"); download_cache_hit(); basename = cached.name; filesize = cached.size; filetype = cached.type; filetime = cached.time; upload_cache:set(slot_id, cached); -- TODO cache negative hits? else module:log("debug", "Cache miss"); download_cache_miss(); local slot, when = errors.coerce(uploads:get(nil, slot_id)); if not slot then module:log("debug", "uploads:get(%q) --> not-found, %s", slot_id, when); else module:log("debug", "uploads:get(%q) --> %s, %d", slot_id, slot, when); basename = slot.attr.filename; filesize = slot.attr.size; filetype = slot.attr["content-type"]; filetime = when; upload_cache:set(slot_id, { name = basename; size = slot.attr.size; type = filetype; time = when; }); end end if not basename then return 404; end local last_modified = os.date('!%a, %d %b %Y %H:%M:%S GMT', filetime); if request.headers.if_modified_since == last_modified then return 304; end local filename = get_filename(slot_id); local handle, ferr = io.open(filename); if not handle then module:log("error", "Could not open file for reading: %s", ferr); -- This can be because the upload slot wasn't used, or the file disappeared -- somehow, or permission issues. return 410; end local request_range = request.headers.range; local response_range; if request_range then local last_byte = string.format("%d", tonumber(filesize) - 1); local range_start, range_end = request_range:match("^bytes=(%d+)%-(%d*)$") -- Only support resumption, ie ranges from somewhere in the middle until the end of the file. if (range_start and range_start ~= "0") and (range_end == "" or range_end == last_byte) then local pos, size = tonumber(range_start), tonumber(filesize); local new_pos = pos < size and handle:seek("set", pos); if new_pos and new_pos < size then response_range = "bytes "..range_start.."-"..last_byte.."/"..filesize; filesize = string.format("%d", size-pos); else handle:close(); return 416; end else handle:close(); return 416; end end if not filetype then filetype = "application/octet-stream"; end local disposition = "attachment"; if safe_types:contains(filetype) or safe_types:contains(filetype:gsub("/.*", "/*")) then disposition = "inline"; end response.headers.last_modified = last_modified; response.headers.content_length = filesize; response.headers.content_type = filetype; response.headers.content_disposition = string.format("%s; filename*=UTF-8''%s", disposition, urlencode(basename)); if response_range then response.status_code = 206; response.headers.content_range = response_range; end response.headers.accept_ranges = "bytes"; response.headers.cache_control = "max-age=31556952, immutable"; response.headers.content_security_policy = "default-src 'none'; frame-ancestors 'none';" response.headers.strict_transport_security = "max-age=31556952"; response.headers.x_content_type_options = "nosniff"; response.headers.x_frame_options = "DENY"; -- COMPAT IE missing support for CSP frame-ancestors response.headers.x_xss_protection = "1; mode=block"; return response:send_file(handle); end if expiry < math.huge and not external_base_url then -- TODO HTTP DELETE to the external endpoint? 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(); module:add_timer(t, done) wait(); end local prune_start = module:measure("prune", "times"); module:daily("Remove expired files", function(_, current_time) local prune_done = prune_start(); local boundary_time = (current_time or os.time()) - expiry; local iter, total = assert(uploads:find(nil, {["end"] = boundary_time; total = true})); if total == 0 then module:log("info", "No expired uploaded files to prune"); prune_done(); return; end module:log("info", "Pruning expired files uploaded earlier than %s", dt.datetime(boundary_time)); module:log("debug", "Total storage usage: %s / %s", B(total_storage_usage), B(total_storage_limit)); local obsolete_uploads = array(); local num_expired = 0; local size_sum = 0; local problem_deleting = false; for slot_id, slot_info in iter do num_expired = num_expired + 1; upload_cache:set(slot_id, nil); local filename = get_filename(slot_id); local deleted, err, errno = os.remove(filename); if deleted or errno == ENOENT then -- removed successfully or it was already gone size_sum = size_sum + tonumber(slot_info.attr.size); obsolete_uploads:push(slot_id); else module:log("error", "Could not prune expired file %q: %s", filename, err); problem_deleting = true; end if num_expired % 100 == 0 then sleep(0.1); end end -- obsolete_uploads now contains slot ids for which the files have been -- removed and that needs to be cleared from the database local deletion_query = {["end"] = boundary_time}; if not problem_deleting then module:log("info", "All (%d, %s) expired files successfully pruned", num_expired, B(size_sum)); -- we can delete based on time else module:log("warn", "%d out of %d expired files could not be pruned", num_expired-#obsolete_uploads, num_expired); -- we'll need to delete only those entries where the files were -- successfully removed, and then try again with the failed ones. -- eventually the admin ought to notice and fix the permissions or -- whatever the problem is. deletion_query = {ids = obsolete_uploads}; end total_storage_usage = total_storage_usage - size_sum; module:log("debug", "Total storage usage: %s / %s", B(total_storage_usage), B(total_storage_limit)); persist_stats:set(nil, "total", total_storage_usage); if #obsolete_uploads == 0 then module:log("debug", "No metadata to remove"); else local removed, err = uploads:delete(nil, deletion_query); if removed == true or removed == num_expired or removed == #obsolete_uploads then module:log("debug", "Expired upload metadata pruned successfully"); else module:log("error", "Problem removing metadata for expired files: %s", err); end end prune_done(); end); end local summary_start = module:measure("summary", "times"); module:weekly("Calculate total storage usage", function() local summary_done = summary_start(); local iter = assert(uploads:find(nil)); local count, sum = 0, 0; for _, file in iter do sum = sum + tonumber(file.attr.size); count = count + 1; end module:log("info", "Uploaded files total: %s in %d files", B(sum), count); if persist_stats:set(nil, "total", sum) then total_storage_usage = sum; else total_storage_usage = unknown; end module:log("debug", "Total storage usage: %s / %s", B(total_storage_usage), B(total_storage_limit)); summary_done(); end); -- Reachable from the console function check_files(query) local issues = {}; local iter = assert(uploads:find(nil, query)); for slot_id, file in iter do local filename = get_filename(slot_id); local size, err = lfs.attributes(filename, "size"); if not size then issues[filename] = err; elseif tonumber(file.attr.size) ~= size then issues[filename] = "file size mismatch"; end end return next(issues) == nil, issues; end module:hook("iq-get/host/urn:xmpp:http:upload:0:request", handle_slot_request); if not external_base_url then module:provides("http", { streaming_uploads = true; cors = { enabled = true; credentials = true; headers = { Authorization = true; }; }; route = { ["PUT /*"] = handle_upload; ["GET /*"] = handle_download; ["GET /"] = function (event) return prosody.events.fire_event("http-message", { response = event.response; --- title = "Prosody HTTP Upload endpoint"; message = "This is where files will be uploaded to, and served from."; warning = not (event.request.secure) and "This endpoint is not considered secure!" or nil; }) or "This is the Prosody HTTP Upload endpoint."; end } }); end prosody-13.0.1/plugins/PaxHeaders/mod_http_files.lua0000644000000000000000000000011714773555365017564 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.611710614 prosody-13.0.1/plugins/mod_http_files.lua0000644000175000017500000000602714773555365021770 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- module:depends("http"); local open = io.open; 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_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"); local mime_map = module:shared("/*/http_files/mime").types; if not mime_map then mime_map = { html = "text/html", htm = "text/html", xml = "application/xml", txt = "text/plain", css = "text/css", js = "application/javascript", png = "image/png", gif = "image/gif", jpeg = "image/jpeg", jpg = "image/jpeg", svg = "image/svg+xml", }; module:shared("/*/http_files/mime").types = mime_map; local mime_types, err = open(module:get_option_path("mime_types_file", "/etc/mime.types", "config"), "r"); if not mime_types then module:log("debug", "Could not open MIME database: %s", err); else local mime_data = mime_types:read("*a"); mime_types:close(); setmetatable(mime_map, { __index = function(t, ext) local typ = mime_data:match("\n(%S+)[^\n]*%s"..(ext:lower()).."%s") or "application/octet-stream"; t[ext] = typ; return typ; end }); end end local function get_calling_module() local info = debug.getinfo(3, "S"); if not info then return "An unknown module"; end return info.source:match"mod_[^/\\.]+" or info.short_src; end -- COMPAT -- TODO deprecate function serve(opts) if type(opts) ~= "table" then -- assume path string opts = { path = opts }; end if opts.directory_index == nil then opts.directory_index = directory_index; end if opts.mime_map == nil then opts.mime_map = mime_map; end if opts.cache_size == nil then opts.cache_size = cache_size; end if opts.cache_max_file_size == nil then opts.cache_max_file_size = cache_max_file_size; end if opts.index_files == nil then opts.index_files = dir_indices; end 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 '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); end end return routes; end module:provides("http", { route = { ["GET /*"] = fileserver.serve({ path = base_path; directory_index = directory_index; mime_map = mime_map; cache_size = cache_size; cache_max_file_size = cache_max_file_size; index_files = dir_indices; }); }; }); prosody-13.0.1/plugins/PaxHeaders/mod_http_openmetrics.lua0000644000000000000000000000011714773555365021012 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.611710614 prosody-13.0.1/plugins/mod_http_openmetrics.lua0000644000175000017500000000301714773555365023212 0ustar00prosodyprosody00000000000000-- Export statistics in OpenMetrics format -- -- Copyright (C) 2014 Daurnimator -- Copyright (C) 2018 Emmanuel Gil Peyrot -- Copyright (C) 2021 Jonas Schäfer -- -- This module is MIT/X11 licensed. module:set_global(); local statsman = require "prosody.core.statsmanager"; local ip = require "prosody.util.ip"; local get_metric_registry = statsman.get_metric_registry; local collect = statsman.collect; local get_metrics; local permitted_ips = module:get_option_set("openmetrics_allow_ips", { "::1", "127.0.0.1" }); local permitted_cidr = module:get_option_string("openmetrics_allow_cidr"); local function is_permitted(request) local ip_raw = request.ip; if permitted_ips:contains(ip_raw) or (permitted_cidr and ip.match(ip.new_ip(ip_raw), ip.parse_cidr(permitted_cidr))) then return true; end return false; end function get_metrics(event) if not is_permitted(event.request) then return 403; -- Forbidden end local response = event.response; response.headers.content_type = "application/openmetrics-text; version=0.0.4"; if collect then -- Ensure to get up-to-date samples when running in manual mode collect() end local registry = get_metric_registry() if registry == nil then response.headers.content_type = "text/plain; charset=utf-8" response.status_code = 404 return "No statistics provider configured\n" end return registry:render(); end module:depends "http"; module:provides("http", { default_path = "metrics"; route = { GET = get_metrics; }; }); prosody-13.0.1/plugins/PaxHeaders/mod_invites.lua0000644000000000000000000000011714773555365017104 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.611710614 prosody-13.0.1/plugins/mod_invites.lua0000644000175000017500000003723114773555365021311 0ustar00prosodyprosody00000000000000local id = require "prosody.util.id"; local it = require "prosody.util.iterators"; local url = require "socket.url"; 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 url_escape = require "prosody.util.http".urlencode; local render_url = require "prosody.util.interpolation".new("%b{}", url_escape, { urlescape = url_escape; noscheme = function (urlstring) return (urlstring:gsub("^[^:]+:", "")); end; }); local default_ttl = module:get_option_period("invite_expiry", "1 week"); local token_storage; if prosody.process_type == "prosody" or prosody.shutdown then token_storage = module:open_store("invite_token", "map"); end local function get_uri(action, jid, token, params) --> string return url.build({ scheme = "xmpp", path = jid, query = action..";preauth="..token..(params and (";"..params) or ""), }); end local function create_invite(invite_action, invite_jid, allow_registration, additional_data, ttl, reusable) local token = id.medium(); local created_at = os.time(); local expires = created_at + (ttl or default_ttl); local invite_params = (invite_action == "roster" and allow_registration) and "ibr=y" or nil; local invite = { type = invite_action; jid = invite_jid; token = token; allow_registration = allow_registration; additional_data = additional_data; uri = get_uri(invite_action, invite_jid, token, invite_params); created_at = created_at; expires = expires; reusable = reusable; }; module:fire_event("invite-created", invite); if allow_registration then local ok, err = token_storage:set(nil, token, invite); if not ok then module:log("warn", "Failed to store account invite: %s", err); return nil, "internal-server-error"; end end if invite_action == "roster" then local username = jid_node(invite_jid); local ok, err = token_storage:set(username, token, expires); if not ok then module:log("warn", "Failed to store subscription invite: %s", err); return nil, "internal-server-error"; end end return invite; end -- Create invitation to register an account (optionally restricted to the specified username) function create_account(account_username, additional_data, ttl) --luacheck: ignore 131/create_account local jid = account_username and (account_username.."@"..module.host) or module.host; return create_invite("register", jid, true, additional_data, ttl); end -- Create invitation to reset the password for an account function create_account_reset(account_username, ttl) --luacheck: ignore 131/create_account_reset return create_account(account_username, { allow_reset = account_username }, ttl or 86400); end -- Create invitation to become a contact of a local user function create_contact(username, allow_registration, additional_data, ttl) --luacheck: ignore 131/create_contact return create_invite("roster", username.."@"..module.host, allow_registration, additional_data, ttl); end -- Create invitation to register an account and join a user group -- If explicit ttl is passed, invite is valid for multiple signups -- during that time period function create_group(group_ids, additional_data, ttl) --luacheck: ignore 131/create_group local merged_additional_data = { groups = group_ids; }; if additional_data then for k, v in pairs(additional_data) do merged_additional_data[k] = v; end end return create_invite("register", module.host, true, merged_additional_data, ttl, not not ttl); end -- Iterates pending (non-expired, unused) invites that allow registration function pending_account_invites() --luacheck: ignore 131/pending_account_invites local store = module:open_store("invite_token"); local now = os.time(); local function is_valid_invite(_, invite) return invite.expires > now; end return it.filter(is_valid_invite, pairs(store:get(nil) or {})); end function get_account_invite_info(token) --luacheck: ignore 131/get_account_invite_info if not token then return nil, "no-token"; end -- Fetch from host store (account invite) local token_info = token_storage:get(nil, token); if not token_info then return nil, "token-invalid"; elseif os.time() > token_info.expires then return nil, "token-expired"; end return token_info; end function delete_account_invite(token) --luacheck: ignore 131/delete_account_invite if not token then return nil, "no-token"; end return token_storage:set(nil, token, nil); end local valid_invite_methods = {}; local valid_invite_mt = { __index = valid_invite_methods }; function valid_invite_methods:use() if self.reusable then return true; end if self.username then -- Also remove the contact invite if present, on the -- assumption that they now have a mutual subscription token_storage:set(self.username, self.token, nil); end token_storage:set(nil, self.token, nil); return true; end -- Get a validated invite (or nil, err). Must call :use() on the -- returned invite after it is actually successfully used -- For "roster" invites, the username of the local user (who issued -- the invite) must be passed. -- If no username is passed, but the registration is a roster invite -- from a local user, the "inviter" field of the returned invite will -- be set to their username. function get(token, username) if not token then return nil, "no-token"; end local valid_until, inviter; -- Fetch from host store (account invite) local token_info = token_storage:get(nil, token); if username then -- token being used for subscription -- Fetch from user store (subscription invite) valid_until = token_storage:get(username, token); else -- token being used for account creation valid_until = token_info and token_info.expires; if token_info and token_info.type == "roster" then username = jid_node(token_info.jid); inviter = username; end end if not valid_until then module:log("debug", "Got unknown token: %s", token); return nil, "token-invalid"; elseif os.time() > valid_until then module:log("debug", "Got expired token: %s", token); return nil, "token-expired"; end return setmetatable({ token = token; username = username; inviter = inviter; type = token_info and token_info.type or "roster"; uri = token_info and token_info.uri or get_uri("roster", username.."@"..module.host, token); additional_data = token_info and token_info.additional_data or nil; reusable = token_info and token_info.reusable or false; }, valid_invite_mt); end function use(token) --luacheck: ignore 131/use local invite = get(token); return invite and invite:use(); end -- Point at e.g. a deployment of https://github.com/modernxmpp/easy-xmpp-invitation -- This URL must always be absolute, as it is shared standalone local invite_url_template = module:get_option_string("invites_page"); local invites_page_supports = module:get_option_set("invites_page_supports", { "account", "contact", "account-and-contact" }); local function add_landing_url(invite) if not invite_url_template or invite.landing_page then return; end -- Determine whether this type of invitation is supported by the landing page local invite_type; if invite.type == "register" then invite_type = "account"; elseif invite.type == "roster" then if invite.allow_registration then invite_type = "account-and-contact"; else invite_type = "contact-only"; end end if not invites_page_supports:contains(invite_type) then return; -- Invitation type unsupported end invite.landing_page = render_url(invite_url_template, { host = module.host, invite = invite }); end module:hook("invite-created", add_landing_url, -1); --- shell command -- COMPAT: Dynamic groups are work in progress as of 13.0, so we'll use the -- presence of mod_invites_groups (a community module) to determine whether to -- expose our support for invites to groups. local have_group_invites = module:get_option_inherited_set("modules_enabled"):contains("invites_groups"); 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"; flags = { array_params = { role = true, group = have_group_invites }; value_params = { expires_after = true }; }; handler = function (self, user_jid, opts) --luacheck: ignore 212/self local username = jid_split(user_jid); local roles = opts and opts.role or {}; local groups = opts and opts.group or {}; if opts and opts.admin then -- Insert it first since we don't get order out of argparse table.insert(roles, 1, "prosody:admin"); end local ttl; if opts and opts.expires_after then ttl = human_io.parse_duration(opts.expires_after); if not ttl then return false, "Unable to parse duration: "..opts.expires_after; end end local invite = assert(create_account(username, { roles = roles; groups = groups; }, ttl)); return true, invite.landing_page or invite.uri; end; }); module:add_item("shell-command", { section = "invite"; section_desc = "Create and manage invitations"; name = "create_reset"; desc = "Create a password reset link for the specified user"; args = { { name = "user_jid", type = "string" } }; host_selector = "user_jid"; flags = { value_params = { expires_after = true }; }; handler = function (self, user_jid, opts) --luacheck: ignore 212/self local username = jid_split(user_jid); if not username then return nil, "Supply the JID of the account you want to generate a password reset for"; end local duration_sec = require "prosody.util.human.io".parse_duration(opts and opts.expires_after or "1d"); if not duration_sec then return nil, "Unable to parse duration: "..opts.expires_after; end local invite, err = create_account_reset(username, duration_sec); if not invite then return nil, err; end self.session.print(invite.landing_page or invite.uri); return true, ("Password reset link for %s valid until %s"):format(user_jid, os.date("%Y-%m-%d %T", invite.expires)); 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" } }; host_selector = "user_jid"; flags = { value_params = { expires_after = true }; kv_params = { allow_registration = true }; }; handler = function (self, user_jid, opts) --luacheck: ignore 212/self local username = jid_split(user_jid); if not username then return nil, "Supply the JID of the account you want the recipient to become a contact of"; end local ttl; if opts and opts.expires_after then ttl = require "prosody.util.human.io".parse_duration(opts.expires_after); if not ttl then return nil, "Unable to parse duration: "..opts.expires_after; end end local invite, err = create_contact(username, opts and opts.allow_registration, nil, ttl); if not invite then return nil, err; end return true, invite.landing_page or invite.uri; end; }); module:add_item("shell-command", { section = "invite"; section_desc = "Create and manage invitations"; name = "show"; desc = "Show details of an account invitation token"; args = { { name = "host", type = "string" }, { name = "token", type = "string" } }; host_selector = "host"; handler = function (self, host, token) --luacheck: ignore 212/self 212/host local invite, err = get_account_invite_info(token); if not invite then return nil, err; end local print = self.session.print; if invite.type == "roster" then print("Invitation to register and become a contact of "..invite.jid); elseif invite.type == "register" then local jid_user, jid_host = jid_split(invite.jid); if invite.additional_data and invite.additional_data.allow_reset then print("Password reset for "..invite.additional_data.allow_reset.."@"..jid_host); elseif jid_user then print("Invitation to register on "..jid_host.." with username '"..jid_user.."'"); else print("Invitation to register on "..jid_host); end else print("Unknown invitation type"); end if invite.inviter then print("Creator:", invite.inviter); end print("Created:", os.date("%Y-%m-%d %T", invite.created_at)); print("Expires:", os.date("%Y-%m-%d %T", invite.expires)); print(""); if invite.uri then print("XMPP URI:", invite.uri); end if invite.landing_page then print("Web link:", invite.landing_page); end if invite.additional_data then print(""); if invite.additional_data.roles then if invite.additional_data.roles[1] then print("Role:", invite.additional_data.roles[1]); end if invite.additional_data.roles[2] then print("Secondary roles:", table.concat(invite.additional_data.roles, ", ", 2, #invite.additional_data.roles)); end end if invite.additional_data.groups then print("Groups:", table.concat(invite.additional_data.groups, ", ")); end if invite.additional_data.note then print("Comment:", invite.additional_data.note); end end return true, "Invitation valid"; end; }); module:add_item("shell-command", { section = "invite"; section_desc = "Create and manage invitations"; name = "delete"; desc = "Delete/revoke an invitation token"; args = { { name = "host", type = "string" }, { name = "token", type = "string" } }; host_selector = "host"; handler = function (self, host, token) --luacheck: ignore 212/self 212/host local invite, err = delete_account_invite(token); if not invite then return nil, err; end return true, "Invitation deleted"; end; }); module:add_item("shell-command", { section = "invite"; section_desc = "Create and manage invitations"; name = "list"; desc = "List pending invitations which allow account registration"; args = { { name = "host", type = "string" } }; host_selector = "host"; handler = function (self, host) -- luacheck: ignore 212/host local print_row = human_io.table({ { title = "Token"; key = "invite"; width = 24; mapper = function (invite) return invite.token; end; }; { title = "Expires"; key = "invite"; width = 20; mapper = function (invite) return os.date("%Y-%m-%dT%T", invite.expires); end; }; { title = "Description"; key = "invite"; width = "100%"; mapper = function (invite) if invite.type == "roster" then return "Contact with "..invite.jid; elseif invite.type == "register" then local jid_user, jid_host = jid_split(invite.jid); if invite.additional_data and invite.additional_data.allow_reset then return "Password reset for "..invite.additional_data.allow_reset.."@"..jid_host; end if jid_user then return "Register on "..jid_host.." with username "..jid_user; end return "Register on "..jid_host; end end; }; }, self.session.width); self.session.print(print_row()); local count = 0; for _, invite in pending_account_invites() do count = count + 1; self.session.print(print_row({ invite = invite })); end return true, ("%d pending invites"):format(count); end; }); local subcommands = {}; --- prosodyctl command function module.command(arg) 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 return subcommands[cmd](arg); end function subcommands.generate() print("This command is deprecated. Please see 'prosodyctl shell help invite' for available commands."); return 1; end prosody-13.0.1/plugins/PaxHeaders/mod_invites_adhoc.lua0000644000000000000000000000011714773555365020242 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.611710614 prosody-13.0.1/plugins/mod_invites_adhoc.lua0000644000175000017500000001012214773555365022435 0ustar00prosodyprosody00000000000000-- XEP-0401: Easy User Onboarding 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; -- Whether local users can invite other users to create an account on this server local allow_user_invites = module:get_option_boolean("allow_user_invites", false); -- Who can see and use the contact invite command. It is strongly recommended to -- keep this available to all local users. To allow/disallow invite-registration -- on the server, use the option above instead. local allow_contact_invites = module:get_option_boolean("allow_contact_invites", true); 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 invites = module:depends("invites"); end local invite_result_form = dataforms.new({ title = "Your invite has been created", { name = "url" ; var = "landing-url"; label = "Invite web page"; desc = "Share this link"; }, { name = "uri"; label = "Invite URI"; desc = "This alternative link can be opened with some XMPP clients"; }, { name = "expire"; label = "Invite valid until"; }, }); -- 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(context) return module:may(":invite-users", context); end module:depends("adhoc"); -- This command is available to all local users, even if allow_user_invites = false -- If allow_user_invites is false, creating an invite still works, but the invite will -- not be valid for registration on the current server, only for establishing a roster -- subscription. module:provides("adhoc", new_adhoc("Create new contact invite", "urn:xmpp:invite#invite", function (_, data) local username, host = split_jid(data.from); if host ~= module.host then return { status = "completed"; error = { message = "This command is only available to users of "..module.host; }; }; end local invite = invites.create_contact(username, may_invite_new_users(data), { source = data.from }); --TODO: check errors return { status = "completed"; result = { layout = invite_result_form; values = { uri = invite.uri; url = invite.landing_page; expire = datetime.datetime(invite.expires); }; }; }; end, allow_contact_invites and "local_user" or "admin")); -- This is an admin-only command that creates a new invitation suitable for registering -- a new account. It does not add the new user to the admin's roster. module:provides("adhoc", new_adhoc("Create new account invite", "urn:xmpp:invite#create-account", function (_, data) local invite = invites.create_account(nil, { source = data.from }); --TODO: check errors return { status = "completed"; result = { layout = invite_result_form; values = { uri = invite.uri; url = invite.landing_page; expire = datetime.datetime(invite.expires); }; }; }; 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")); prosody-13.0.1/plugins/PaxHeaders/mod_invites_register.lua0000644000000000000000000000011714773555365021010 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.611710614 prosody-13.0.1/plugins/mod_invites_register.lua0000644000175000017500000001566314773555365023222 0ustar00prosodyprosody00000000000000local 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)); local invite_only = module:get_option_boolean("registration_invite_only", true); local invites; if prosody.process_type == "prosody" then invites = module:depends("invites"); if invite_only then module:depends("register_ibr"); end end local legacy_invite_stream_feature = st.stanza("register", { xmlns = "urn:xmpp:invite" }):up(); local invite_stream_feature = st.stanza("register", { xmlns = "urn:xmpp:ibr-token:0" }):up(); module:hook("stream-features", function(event) local session, features = event.origin, event.features; -- Advertise to unauthorized clients only. if session.type ~= "c2s_unauthed" or (require_encryption and not session.secure) then return end features:add_child(legacy_invite_stream_feature); features:add_child(invite_stream_feature); end); -- XEP-0379: Pre-Authenticated Roster Subscription module:hook("presence/bare", function (event) local stanza = event.stanza; if stanza.attr.type ~= "subscribe" then return end local preauth = stanza:get_child("preauth", "urn:xmpp:pars:0"); if not preauth then return end local token = preauth.attr.token; if not token then return end local username, host = jid_split(stanza.attr.to); local invite, err = invites.get(token, username); if not invite then module:log("debug", "Got invalid token, error: %s", err); return; end local contact = jid_bare(stanza.attr.from); module:log("debug", "Approving inbound subscription to %s from %s", username, contact); if rostermanager.set_contact_pending_in(username, host, contact, stanza) then if rostermanager.subscribed(username, host, contact) then invite:use(); rostermanager.roster_push(username, host, contact); -- Send back a subscription request (goal is mutual subscription) if not rostermanager.is_user_subscribed(username, host, contact) and not rostermanager.is_contact_pending_out(username, host, contact) then module:log("debug", "Sending automatic subscription request to %s from %s", contact, username); if rostermanager.set_contact_pending_out(username, host, contact) then rostermanager.roster_push(username, host, contact); module:send(st.presence({type = "subscribe", from = username.."@"..host, to = contact })); else module:log("warn", "Failed to set contact pending out for %s", username); end end end end end, 1); -- Client is submitting a preauth token to allow registration module:hook("stanza/iq/urn:xmpp:pars:0:preauth", function(event) local preauth = event.stanza.tags[1]; local token = preauth.attr.token; local validated_invite = invites.get(token); if not validated_invite then local reply = st.error_reply(event.stanza, "cancel", "forbidden", "The invite token is invalid or expired"); event.origin.send(reply); return true; end event.origin.validated_invite = validated_invite; local reply = st.reply(event.stanza); event.origin.send(reply); return true; end); -- Registration attempt - ensure a valid preauth token has been supplied module:hook("user-registering", function (event) local validated_invite = event.validated_invite or (event.session and event.session.validated_invite); if invite_only and not validated_invite then event.allowed = false; event.reason = "Registration on this server is through invitation only"; return; elseif not validated_invite then -- This registration is not using an invite, but -- the server is not in invite-only mode, so nothing -- for this module to do... return; end if validated_invite and validated_invite.additional_data and validated_invite.additional_data.allow_reset then event.allow_reset = validated_invite.additional_data.allow_reset; end end); -- Make a *one-way* subscription. User will see when contact is online, -- contact will not see when user is online. function subscribe(host, user_username, contact_username) local user_jid = user_username.."@"..host; local contact_jid = contact_username.."@"..host; -- Update user's roster to say subscription request is pending... rostermanager.set_contact_pending_out(user_username, host, contact_jid); -- Update contact's roster to say subscription request is pending... rostermanager.set_contact_pending_in(contact_username, host, user_jid); -- Update contact's roster to say subscription request approved... rostermanager.subscribed(contact_username, host, user_jid); -- Update user's roster to say subscription request approved... rostermanager.process_inbound_subscription_approval(user_username, host, contact_jid); end -- Make a mutual subscription between jid1 and jid2. Each JID will see -- when the other one is online. function subscribe_both(host, user1, user2) subscribe(host, user1, user2); subscribe(host, user2, user1); end -- Registration successful, if there was a preauth token, mark it as used module:hook("user-registered", function (event) local validated_invite = event.validated_invite or (event.session and event.session.validated_invite); if not validated_invite then return; end local inviter_username = validated_invite.inviter; local contact_username = event.username; validated_invite:use(); if inviter_username then module:log("debug", "Creating mutual subscription between %s and %s", inviter_username, contact_username); subscribe_both(module.host, inviter_username, contact_username); rostermanager.roster_push(inviter_username, module.host, contact_username.."@"..module.host); end if validated_invite.additional_data then module:log("debug", "Importing roles from invite"); local roles = validated_invite.additional_data.roles; 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 end); -- Equivalent of user-registered but for when the account already existed -- (i.e. password reset) module:hook("user-password-reset", function (event) local validated_invite = event.validated_invite or (event.session and event.session.validated_invite); if not validated_invite then return; end validated_invite:use(); end); prosody-13.0.1/plugins/PaxHeaders/mod_iq.lua0000644000000000000000000000011614773555365016033 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.61571063 prosody-13.0.1/plugins/mod_iq.lua0000644000175000017500000000453314773555365020240 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "prosody.util.stanza"; local full_sessions = prosody.full_sessions; if module:get_host_type() == "local" then module:hook("iq/full", function(data) -- IQ to full JID received local origin, stanza = data.origin, data.stanza; local session = full_sessions[stanza.attr.to]; if not (session and session.send(stanza)) then if stanza.attr.type == "get" or stanza.attr.type == "set" then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end end return true; end); end module:hook("iq/bare", function(data) -- IQ to bare JID received local stanza = data.stanza; local type = stanza.attr.type; -- TODO fire post processing events if type == "get" or type == "set" then local child = stanza.tags[1]; local xmlns = child.attr.xmlns or "jabber:client"; local ret = module:fire_event("iq/bare/"..xmlns..":"..child.name, data); if ret ~= nil then return ret; end return module:fire_event("iq-"..type.."/bare/"..xmlns..":"..child.name, data); else return module:fire_event("iq-"..type.."/bare/"..stanza.attr.id, data); end end); module:hook("iq/self", function(data) -- IQ to self JID received local stanza = data.stanza; local type = stanza.attr.type; if type == "get" or type == "set" then local child = stanza.tags[1]; local xmlns = child.attr.xmlns or "jabber:client"; local ret = module:fire_event("iq/self/"..xmlns..":"..child.name, data); if ret ~= nil then return ret; end return module:fire_event("iq-"..type.."/self/"..xmlns..":"..child.name, data); else return module:fire_event("iq-"..type.."/self/"..stanza.attr.id, data); end end); module:hook("iq/host", function(data) -- IQ to a local host received local stanza = data.stanza; local type = stanza.attr.type; if type == "get" or type == "set" then local child = stanza.tags[1]; local xmlns = child.attr.xmlns or "jabber:client"; local ret = module:fire_event("iq/host/"..xmlns..":"..child.name, data); if ret ~= nil then return ret; end return module:fire_event("iq-"..type.."/host/"..xmlns..":"..child.name, data); else return module:fire_event("iq-"..type.."/host/"..stanza.attr.id, data); end end); prosody-13.0.1/plugins/PaxHeaders/mod_lastactivity.lua0000644000000000000000000000011614773555365020142 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.61571063 prosody-13.0.1/plugins/mod_lastactivity.lua0000644000175000017500000000277514773555365022355 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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"); local map = {}; module:hook("pre-presence/bare", function(event) local stanza = event.stanza; if not(stanza.attr.to) and stanza.attr.type == "unavailable" then local t = os.time(); local s = stanza:get_child_text("status"); map[event.origin.username] = {s = s, t = t}; end end, 10); module:hook("iq-get/bare/jabber:iq:last:query", function(event) local origin, stanza = event.origin, event.stanza; local username = jid_split(stanza.attr.to) or origin.username; if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then local seconds, text = "0", ""; if map[username] then seconds = string.format("%d", os.difftime(os.time(), map[username].t)); text = map[username].s; end origin.send(st.reply(stanza):tag('query', {xmlns='jabber:iq:last', seconds=seconds}):text(text)); else origin.send(st.error_reply(stanza, 'auth', 'forbidden')); end return true; end); module.save = function() return {map = map}; end module.restore = function(data) map = data.map or {}; end prosody-13.0.1/plugins/PaxHeaders/mod_legacyauth.lua0000644000000000000000000000011614773555365017550 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.61571063 prosody-13.0.1/plugins/mod_legacyauth.lua0000644000175000017500000000656414773555365021763 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 "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) local origin, features = event.origin, event.features; if secure_auth_only and not origin.secure then -- Sorry, not offering to insecure streams! return; elseif not origin.username then features:tag("auth", {xmlns='http://jabber.org/features/iq-auth'}):up(); end end); module:hook("stanza/iq/jabber:iq:auth:query", function(event) local session, stanza = event.origin, event.stanza; if session.type ~= "c2s_unauthed" then (session.sends2s or session.send)(st.error_reply(stanza, "cancel", "service-unavailable", "Legacy authentication is only allowed for unauthenticated client connections.")); return true; end if secure_auth_only and not session.secure then session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server")); return true; end local query = stanza.tags[1]; local username = query:get_child("username"); local password = query:get_child("password"); local resource = query:get_child("resource"); if not (username and password and resource) then local reply = st.reply(stanza); session.send(reply:query("jabber:iq:auth") :tag("username"):up() :tag("password"):up() :tag("resource"):up()); else username, password, resource = t_concat(username), t_concat(password), t_concat(resource); username = nodeprep(username); resource = resourceprep(resource) if not (username and resource) then session.send(st.error_reply(stanza, "modify", "bad-request")); return true; end if usermanager.test_password(username, session.host, password) then -- Authentication successful! local success, err = sessionmanager.make_authenticated(session, username); if success then local err_type, err_msg; success, err_type, err, err_msg = sessionmanager.bind_resource(session, resource); if not success then session.send(st.error_reply(stanza, err_type, err, err_msg)); session.username, session.type = nil, "c2s_unauthed"; -- FIXME should this be placed in sessionmanager? return true; elseif resource ~= session.resource then -- server changed resource, not supported by legacy auth session.send(st.error_reply(stanza, "cancel", "conflict", "The requested resource could not be assigned to this session.")); session:close(); -- FIXME undo resource bind and auth instead of closing the session? return true; end session.send(st.reply(stanza)); else session.send(st.error_reply(stanza, "auth", "not-authorized", err)); end else session.send(st.error_reply(stanza, "auth", "not-authorized")); end end return true; end); prosody-13.0.1/plugins/PaxHeaders/mod_limits.lua0000644000000000000000000000011614773555365016723 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.61571063 prosody-13.0.1/plugins/mod_limits.lua0000644000175000017500000001077714773555365021137 0ustar00prosodyprosody00000000000000-- Because we deal with pre-authed sessions and streams we can't be host-specific module:set_global(); 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_period("limits_resolution", 1); local default_bytes_per_second = 3000; local default_burst = 2; local rate_units = { b = 1, k = 3, m = 6, g = 9, t = 12 } -- Plan for the future. local function parse_rate(rate, sess_type) local quantity, unit, exp; if rate then quantity, unit = rate:match("^(%d+) ?([^/]+)/s$"); exp = quantity and rate_units[unit:sub(1,1):lower()]; end if not exp then module:log("error", "Error parsing rate for %s: %q, using default rate (%d bytes/s)", sess_type, rate, default_bytes_per_second); return default_bytes_per_second; end return quantity*(10^exp); end local function parse_burst(burst, sess_type) if type(burst) == "string" then burst = burst:match("^(%d+) ?s$"); end local n_burst = tonumber(burst); if burst and not n_burst then module:log("error", "Unable to parse burst for %s: %q, using default burst interval (%ds)", sess_type, burst, default_burst); end return n_burst or default_burst; end -- Process config option into limits table: -- limits = { c2s = { bytes_per_second = X, burst_seconds = Y } } local limits = { c2s = { bytes_per_second = 10 * 1024; burst_seconds = 2; }; s2sin = { bytes_per_second = 30 * 1024; burst_seconds = 2; }; }; for sess_type, sess_limits in pairs(limits_cfg) do limits[sess_type] = { bytes_per_second = parse_rate(sess_limits.rate, sess_type); burst_seconds = parse_burst(sess_limits.burst, sess_type); }; end local default_filter_set = {}; function default_filter_set.bytes_in(bytes, session) local sess_throttle = session.throttle; if sess_throttle then local ok, _, outstanding = sess_throttle:poll(#bytes, true); if not ok then session.log("debug", "Session over rate limit (%d) with %d (by %d), pausing", sess_throttle.max, #bytes, outstanding); outstanding = ceil(outstanding); session.conn:pause(); -- Read no more data from the connection until there is no outstanding data local outstanding_data = bytes:sub(-outstanding); bytes = bytes:sub(1, #bytes-outstanding); timer.add_task(limits_resolution, function () if not session.conn then return; end if sess_throttle:peek(#outstanding_data) then session.log("debug", "Resuming paused session"); session.conn:resume(); end -- Handle what we can of the outstanding data session.data(outstanding_data); end); end end return bytes; end local type_filters = { c2s = default_filter_set; s2sin = default_filter_set; s2sout = default_filter_set; }; local function filter_hook(session) local session_type = session.type:match("^[^_]+"); local filter_set, opts = type_filters[session_type], limits[session_type]; if opts then if session.conn and session.conn.setlimit then session.conn:setlimit(opts.bytes_per_second); -- Currently no burst support else session.throttle = throttle.create(opts.bytes_per_second * opts.burst_seconds, opts.burst_seconds); filters.add_filter(session, "bytes/in", filter_set.bytes_in, 1000); end end end function module.load() filters.add_filter_hook(filter_hook); end function module.unload() filters.remove_filter_hook(filter_hook); end function unlimited(session) local session_type = session.type:match("^[^_]+"); if session.conn and session.conn.setlimit then session.conn:setlimit(0); -- Currently no burst support else local filter_set = type_filters[session_type]; filters.remove_filter(session, "bytes/in", filter_set.bytes_in); session.throttle = nil; end end function module.add_host(module) local unlimited_jids = module:get_option_inherited_set("unlimited_jids", {}); if not unlimited_jids:empty() then module:hook("authentication-success", function (event) local session = event.session; local jid = session.username .. "@" .. session.host; if unlimited_jids:contains(jid) then unlimited(session); end end); module:hook("s2sout-established", function (event) local session = event.session; if unlimited_jids:contains(session.to_host) then unlimited(session); end end); module:hook("s2sin-established", function (event) local session = event.session; if session.from_host and unlimited_jids:contains(session.from_host) then unlimited(session); end end); end end prosody-13.0.1/plugins/PaxHeaders/mod_mam0000644000000000000000000000013014773555365015410 xustar0029 mtime=1743706869.95171197 29 atime=1743706869.61571063 30 ctime=1743706869.619710646 prosody-13.0.1/plugins/mod_mam/0000755000175000017500000000000014773555365017671 5ustar00prosodyprosody00000000000000prosody-13.0.1/plugins/mod_mam/PaxHeaders/mamprefs.lib.lua0000644000000000000000000000011614773555365020553 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.61571063 prosody-13.0.1/plugins/mod_mam/mamprefs.lib.lua0000644000175000017500000000440214773555365022753 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2017 Matthew Wild -- Copyright (C) 2008-2017 Waqas Hussain -- Copyright (C) 2011-2017 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- XEP-0313: Message Archive Management for Prosody -- -- luacheck: ignore 122/prosody 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 = { [false] = "roster", -- default ::= true | false | "roster" -- true = always, false = never, nil = global default ["romeo@montague.net"] = true, -- always ["montague@montague.net"] = false, -- newer }; end local sessions = prosody.hosts[module.host].sessions; local archive_store = module:get_option_string("archive_store", "archive"); local prefs = module:open_store(archive_store .. "_prefs"); local function get_prefs(user, explicit) local user_sessions = sessions[user]; local user_prefs = user_sessions and user_sessions.archive_prefs if not user_prefs then -- prefs not cached user_prefs = prefs:get(user); if not user_prefs then -- prefs not set if smart_enable and explicit then -- a mam-capable client was involved in this action, set defaults user_prefs = { [false] = global_default_policy }; prefs:set(user, user_prefs); end end if user_sessions then -- cache settings if they originate from user action user_sessions.archive_prefs = user_prefs; end if not user_prefs then if smart_enable then -- not yet enabled, either explicitly or "smart" user_prefs = { [false] = false }; else -- no explicit settings, return defaults user_prefs = { [false] = global_default_policy }; end end end return user_prefs; end local function set_prefs(user, user_prefs) local user_sessions = sessions[user]; if user_sessions then user_sessions.archive_prefs = user_prefs; end return prefs:set(user, user_prefs); end return { get = get_prefs, set = set_prefs, } prosody-13.0.1/plugins/mod_mam/PaxHeaders/mamprefsxml.lib.lua0000644000000000000000000000011614773555365021274 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.61571063 prosody-13.0.1/plugins/mod_mam/mamprefsxml.lib.lua0000644000175000017500000000322414773555365023475 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2017 Matthew Wild -- Copyright (C) 2008-2017 Waqas Hussain -- Copyright (C) 2011-2017 Kim Alvefur -- Copyright (C) 2018 Emmanuel Gil Peyrot -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- XEP-0313: Message Archive Management for Prosody -- local st = require"prosody.util.stanza"; local jid_prep = require"prosody.util.jid".prep; local xmlns_mam = "urn:xmpp:mam:2"; local default_attrs = { always = true, [true] = "always", never = false, [false] = "never", roster = "roster", } local function tostanza(prefs) local default = prefs[false]; default = default_attrs[default]; local prefstanza = st.stanza("prefs", { xmlns = xmlns_mam, default = default }); local always = st.stanza("always"); local never = st.stanza("never"); for jid, choice in pairs(prefs) do if jid then (choice and always or never):tag("jid"):text(jid):up(); end end prefstanza:add_child(always):add_child(never); return prefstanza; end local function fromstanza(prefstanza) local prefs = {}; local default = prefstanza.attr.default; if default then prefs[false] = default_attrs[default]; end local always = prefstanza:get_child("always"); if always then for rule in always:childtags("jid") do local jid = jid_prep(rule:get_text()); if jid then prefs[jid] = true; end end end local never = prefstanza:get_child("never"); if never then for rule in never:childtags("jid") do local jid = jid_prep(rule:get_text()); if jid then prefs[jid] = false; end end end return prefs; end return { tostanza = tostanza; fromstanza = fromstanza; } prosody-13.0.1/plugins/mod_mam/PaxHeaders/mod_mam.lua0000644000000000000000000000011714773555365017606 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.619710646 prosody-13.0.1/plugins/mod_mam/mod_mam.lua0000644000175000017500000005011014773555365022002 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2017 Matthew Wild -- Copyright (C) 2008-2017 Waqas Hussain -- Copyright (C) 2011-2021 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- XEP-0313: Message Archive Management for Prosody -- local xmlns_mam = "urn:xmpp:mam:2"; local xmlns_mam_ext = "urn:xmpp:mam:2#extended"; local xmlns_delay = "urn:xmpp:delay"; local xmlns_forward = "urn:xmpp:forward:0"; local xmlns_st_id = "urn:xmpp:sid:0"; 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 "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 "prosody.core.rostermanager".load_roster; local is_stanza = st.is_stanza; local tostring = tostring; 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_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_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 error("mod_"..(archive._provided_by or archive.name and "storage_"..archive.name).." does not support archiving\n" .."See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information"); end local use_total = module:get_option_boolean("mam_include_total", true); 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. module:hook("iq/self/"..xmlns_mam..":prefs", function(event) local origin, stanza = event.origin, event.stanza; local user = origin.username; if stanza.attr.type == "set" then local new_prefs = stanza:get_child("prefs", xmlns_mam); local prefs = prefs_from_stanza(new_prefs); local ok, err = set_prefs(user, prefs); if not ok then origin.send(st.error_reply(stanza, "cancel", "internal-server-error", "Error storing preferences: "..tostring(err))); return true; end end local prefs = prefs_to_stanza(get_prefs(user, true)); local reply = st.reply(stanza):add_child(prefs); origin.send(reply); return true; end); local query_form = dataform { { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam }; { name = "with"; type = "jid-single" }; { name = "start"; type = "text-single"; datatype = "xs:dateTime" }; { name = "end"; type = "text-single"; datatype = "xs:dateTime" }; }; if archive.caps and archive.caps.full_id_range then table.insert(query_form, { name = "before-id"; type = "text-single"; }); table.insert(query_form, { name = "after-id"; type = "text-single"; }); end if archive.caps and archive.caps.ids then table.insert(query_form, { name = "ids"; type = "list-multi"; }); end -- Serve form module:hook("iq-get/self/"..xmlns_mam..":query", function(event) local origin, stanza = event.origin, event.stanza; get_prefs(origin.username, true); origin.send(st.reply(stanza):query(xmlns_mam):add_child(query_form:form())); return true; end); -- Handle archive queries module:hook("iq-set/self/"..xmlns_mam..":query", function(event) local origin, stanza = event.origin, event.stanza; local query = stanza.tags[1]; local qid = query.attr.queryid; origin.mam_requested = true; get_prefs(origin.username, true); -- Search query parameters local qwith, qstart, qend, qbefore, qafter, qids; local form = query:get_child("x", "jabber:x:data"); if form then local form_type, err = get_form_type(form); if not form_type then origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid dataform: "..err)); return true; elseif form_type ~= xmlns_mam then origin.send(st.error_reply(stanza, "modify", "bad-request", "Unexpected FORM_TYPE, expected '"..xmlns_mam.."'")); return true; end form, err = query_form:data(form); if err then origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err)))); return true; end qwith, qstart, qend = form["with"], form["start"], form["end"]; qbefore, qafter = form["before-id"], form["after-id"]; qids = form["ids"]; qwith = qwith and jid_bare(qwith); -- dataforms does jidprep end -- RSM stuff local qset = rsm.get(query); local qmax = m_min(qset and qset.max or default_max_items, max_max_items); local reverse = qset and qset.before or false; local before, after = qset and qset.before or qbefore, qset and qset.after or qafter; if type(before) ~= "string" then before = nil; end -- A reverse query needs to be flipped local flip = reverse; -- A flip-page query needs to be the opposite of that. if query:get_child("flip-page") then flip = not flip end module:log("debug", "Archive query by %s id=%s with=%s when=%s...%s rsm=%q", origin.username, qid or stanza.attr.id, qwith or "*", qstart and timestamp(qstart) or "", qend and timestamp(qend) or "", qset); -- Load all the data! local data, err = archive:find(origin.username, { start = qstart; ["end"] = qend; -- Time range with = qwith; limit = qmax == 0 and 0 or qmax + 1; before = before; after = after; ids = qids; reverse = reverse; total = use_total or qmax == 0; }); if not data then module:log("debug", "Archive query id=%s failed: %s", qid or stanza.attr.id, err); if err == "item-not-found" then origin.send(st.error_reply(stanza, "modify", "item-not-found")); else origin.send(st.error_reply(stanza, "cancel", "internal-server-error")); end return true; end local total = tonumber(err); local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to }; local results = {}; -- Wrap it in stuff and deliver local first, last; local count = 0; local complete = "true"; for id, item, when in data do count = count + 1; if count > qmax then -- We requested qmax+1 items. If that many items are retrieved then -- there are more results to page through, so: complete = nil; break; end local fwd_st = st.message(msg_reply_attr) :tag("result", { xmlns = xmlns_mam, queryid = qid, id = id }) :tag("forwarded", { xmlns = xmlns_forward }) :tag("delay", { xmlns = xmlns_delay, stamp = timestamp(when) }):up(); if not is_stanza(item) then item = st.deserialize(item); end item.attr.xmlns = "jabber:client"; fwd_st:add_child(item); if not first then first = id; end last = id; if flip then results[count] = fwd_st; else origin.send(fwd_st); end end if flip then for i = #results, 1, -1 do origin.send(results[i]); end end if reverse then first, last = last, first; end origin.send(st.reply(stanza) :tag("fin", { xmlns = xmlns_mam, complete = complete }) :add_child(rsm.generate { first = first, last = last, count = total })); -- That's all folks! module:log("debug", "Archive query id=%s completed, %d items returned", qid or stanza.attr.id, complete and count or count - 1); return true; end); module:hook("iq-get/self/"..xmlns_mam..":metadata", function (event) local origin, stanza = event.origin, event.stanza; local reply = st.reply(stanza):tag("metadata", { xmlns = xmlns_mam }); do local first = archive:find(origin.username, { limit = 1 }); if not first then origin.send(st.error_reply(stanza, "cancel", "internal-server-error")); return true; end for id, _, when in first do reply:tag("start", { id = id, timestamp = timestamp(when) }):up(); end end do local last = archive:find(origin.username, { limit = 1, reverse = true }); if not last then origin.send(st.error_reply(stanza, "cancel", "internal-server-error")); return true; end for id, _, when in last do reply:tag("end", { id = id, timestamp = timestamp(when) }):up(); end end origin.send(reply); return true; end); local function has_in_roster(user, who) local roster = rm_load_roster(user, host); module:log("debug", "%s has %s in roster? %s", user, who, roster[who] and "yes" or "no"); return roster[who]; end local function shall_store(user, who) -- TODO Cache this? if not um.user_exists(user, host) then module:log("debug", "%s@%s does not exist", user, host) return false; end local prefs = get_prefs(user); local rule = prefs[who]; module:log("debug", "%s's rule for %s is %s", user, who, rule); if rule ~= nil then return rule; end -- Below could be done by a metatable local default = prefs[false]; module:log("debug", "%s's default rule is %s", user, default); if default == "roster" then return has_in_roster(user, who); end return default; end local function strip_stanza_id(stanza, user) if stanza:get_child("stanza-id", xmlns_st_id) then stanza = st.clone(stanza); stanza:maptags(function (tag) if tag.name == "stanza-id" and tag.attr.xmlns == xmlns_st_id then local by_user, by_host, res = jid_prepped_split(tag.attr.by); if not res and by_host == host and by_user == user then return nil; end end return tag; end); end return stanza; end local function should_store(stanza) --> boolean, reason: string local st_type = stanza.attr.type or "normal"; -- FIXME pass direction of stanza and use that along with bare/full JID addressing -- for more accurate MUC / type=groupchat check if st_type == "headline" then -- Headline messages are ephemeral by definition return false, "headline"; end if st_type == "error" then -- Errors not sent sent from a local client -- Why would a client send an error anyway? if jid_resource(stanza.attr.to) then -- Store delivery failure notifications so you know if your own messages -- were not delivered. return true, "bounce"; else -- Skip errors for messages that come from your account, such as PEP -- notifications. return false, "bounce"; end end if st_type == "groupchat" then -- MUC messages always go to the full JID, usually archived by the MUC return false, "groupchat"; end if stanza:get_child("no-store", "urn:xmpp:hints") or stanza:get_child("no-permanent-store", "urn:xmpp:hints") then return false, "hint"; end if stanza:get_child("store", "urn:xmpp:hints") then return true, "hint"; end if stanza:get_child("body") then return true, "body"; end if stanza:get_child("subject") then -- XXX Who would send a message with a subject but without a body? return true, "subject"; end if stanza:get_child("encryption", "urn:xmpp:eme:0") then -- Since we can't know what an encrypted message contains, we assume it's important -- XXX Experimental XEP return true, "encrypted"; end if stanza:get_child(nil, "urn:xmpp:receipts") then -- If it's important enough to ask for a receipt then it's important enough to archive -- and the same applies to the receipt return true, "receipt"; end if stanza:get_child(nil, "urn:xmpp:chat-markers:0") then return true, "marker"; end if stanza:get_child("x", "jabber:x:conference") or stanza:find("{http://jabber.org/protocol/muc#user}x/invite") then return true, "invite"; end if stanza:get_child(nil, "urn:xmpp:jingle-message:0") or stanza:get_child(nil, "urn:xmpp:jingle-message:1") then -- XXX Experimental XEP return true, "jingle call"; end -- The IM-NG thing to do here would be to return `not st_to_full` -- One day ... return false, "default"; end module:hook("archive-should-store", function (event) local should, why = should_store(event.stanza); event.reason = why; return should; end, -1) -- Handle messages local function message_handler(event, c2s) local origin, stanza = event.origin, event.stanza; local log = c2s and origin.log or module._log; local orig_from = stanza.attr.from; local orig_to = stanza.attr.to or orig_from; -- Stanza without 'to' are treated as if it was to their own bare jid -- Whose storage do we put it in? local store_user = c2s and origin.username or jid_split(orig_to); -- And who are they chatting with? local with = jid_bare(c2s and orig_to or orig_from); -- Filter out that claim to be from us event.stanza = strip_stanza_id(stanza, store_user); local event_payload = { stanza = stanza; session = origin }; local should = module:fire_event("archive-should-store", event_payload); local why = event_payload.reason; if not should then log("debug", "Not archiving stanza: %s (%s)", stanza:top_tag(), event_payload.reason); return; end local clone_for_storage; if not strip_tags:empty() then clone_for_storage = st.clone(stanza); clone_for_storage:maptags(function (tag) if strip_tags:contains(tag.attr.xmlns) then return nil; else return tag; end end); if #clone_for_storage.tags == 0 then log("debug", "Not archiving stanza: %s (empty when stripped)", stanza:top_tag()); return; end else clone_for_storage = stanza; end -- Check with the users preferences if shall_store(store_user, with) then log("debug", "Archiving stanza: %s (%s)", stanza:top_tag(), why); -- And stash it 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 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); }); if cleaned then ok, err = archive:append(store_user, nil, clone_for_storage, time, with); end end if not ok and (archive.caps and archive.caps.truncate) then module:log("debug", "User '%s' over quota, truncating archive", store_user); local truncated = archive:delete(store_user, { truncate = archive_truncate; }); if truncated then ok, err = archive:append(store_user, nil, clone_for_storage, time, with); end end end if ok then local clone_for_other_handlers = st.clone(stanza); local id = ok; clone_for_other_handlers:tag("stanza-id", { xmlns = xmlns_st_id, by = store_user.."@"..host, id = id }):up(); event.stanza = clone_for_other_handlers; schedule_cleanup(store_user); module:fire_event("archive-message-added", { origin = origin, stanza = clone_for_storage, for_user = store_user, id = id }); else log("error", "Could not archive stanza: %s", err); end else log("debug", "Not archiving stanza: %s (prefs)", stanza:top_tag()); end end local function c2s_message_handler(event) return message_handler(event, true); end -- Filter out before the message leaves the server to prevent privacy leak. local function strip_stanza_id_after_other_events(event) event.stanza = strip_stanza_id(event.stanza, event.origin.username); end module:hook("pre-message/bare", strip_stanza_id_after_other_events, -1); module:hook("pre-message/full", strip_stanza_id_after_other_events, -1); -- Catch messages not stored by mod_offline and mark them as stored if they -- have been archived. This would generally only happen if mod_offline is -- disabled. Otherwise the message would generate a delivery failure report, -- which would not be accurate because it has been archived. module:hook("message/offline/handle", function(event) local stanza = event.stanza; local user = event.username .. "@" .. host; if stanza:get_child_with_attr("stanza-id", xmlns_st_id, "by", user) then return true; end end, -2); -- Don't broadcast offline messages to clients that have queried the archive. module:hook("message/offline/broadcast", function (event) if event.origin.mam_requested then return true; end end); if cleanup_after ~= math.huge then local cleanup_storage = module:open_store("archive_cleanup"); local cleanup_map = module:open_store("archive_cleanup", "map"); module:log("debug", "archive_expires_after = %d -- in seconds", cleanup_after); if not archive.delete then module:log("error", "archive_expires_after set but mod_%s does not support deleting", archive._provided_by); return false; end -- For each day, store a set of users that have new messages. To expire -- messages, we collect the union of sets of users from dates that fall -- outside the cleanup range. if not (archive.caps and archive.caps.wildcard_delete) then 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 local ok = cleanup_map:set(date, username, true); if ok then last_date:set(username, date); end end end local cleanup_time = module:measure("cleanup", "times"); local async = require "prosody.util.async"; module:daily("Remove expired messages", function () local cleanup_done = cleanup_time(); if archive.caps and archive.caps.wildcard_delete then local ok, err = archive:delete(true, { ["end"] = os.time() - cleanup_after }) if ok then local sum = tonumber(ok); if sum then module:log("info", "Deleted %d expired messages", sum); else -- driver did not tell module:log("info", "Deleted all expired messages"); end else module:log("error", "Could not delete messages: %s", err); end cleanup_done(); return; end local users = {}; local cut_off = datestamp(os.time() - cleanup_after); for date in cleanup_storage:users() do if date <= cut_off then module:log("debug", "Messages from %q should be expired", date); local messages_this_day = cleanup_storage:get(date); if messages_this_day then for user in pairs(messages_this_day) do users[user] = true; end if date < cut_off then -- Messages from the same day as the cut-off might not have expired yet, -- but all earlier will have, so clear storage for those days. cleanup_storage:set(date, nil); end end end end local sum, num_users = 0, 0; for user in pairs(users) do local ok, err = archive:delete(user, { ["end"] = os.time() - cleanup_after; }) if ok then num_users = num_users + 1; sum = sum + (tonumber(ok) or 0); else cleanup_map:set(cut_off, user, true); module:log("error", "Could not delete messages for user '%s': %s", user, err); end local wait, done = async.waiter(); module:add_timer(0.01, done); wait(); end module:log("info", "Deleted %d expired messages for %d users", sum, num_users); cleanup_done(); end); else module:log("debug", "Archive expiry disabled"); -- Don't ask the backend to count the potentially unbounded number of items, -- it'll get slow. use_total = module:get_option_boolean("mam_include_total", false); end -- Stanzas sent by local clients module:hook("pre-message/bare", c2s_message_handler, 0); module:hook("pre-message/full", c2s_message_handler, 0); -- Stanzas to local clients module:hook("message/bare", message_handler, 0); module:hook("message/full", message_handler, 0); local advertise_extended = archive.caps and archive.caps.full_id_range and archive.caps.ids; module:hook("account-disco-info", function(event) (event.reply or event.stanza):tag("feature", {var=xmlns_mam}):up(); if advertise_extended then (event.reply or event.stanza):tag("feature", {var=xmlns_mam_ext}):up(); end (event.reply or event.stanza):tag("feature", {var=xmlns_st_id}):up(); end); prosody-13.0.1/plugins/PaxHeaders/mod_message.lua0000644000000000000000000000011714773555365017047 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.619710646 prosody-13.0.1/plugins/mod_message.lua0000644000175000017500000000503314773555365021247 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local full_sessions = prosody.full_sessions; local bare_sessions = prosody.bare_sessions; 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]; local t = stanza.attr.type; if t == "error" then return true; -- discard elseif t == "groupchat" then local node, host = jid_split(bare); if user_exists(node, host) then if module:fire_event("message/bare/groupchat", { origin = origin, stanza = stanza; }) then return true; end end origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); elseif t == "headline" then if user and stanza.attr.to == bare then for _, session in pairs(user.sessions) do if session.presence and session.priority >= 0 then session.send(stanza); end end end -- current policy is to discard headlines if no recipient is available else -- chat or normal message if user then -- some resources are connected local recipients = user.top_resources; if recipients then local sent; for i=1,#recipients do sent = recipients[i].send(stanza) or sent; end if sent then return true; end end end -- no resources are online local node, host = jid_split(bare); local ok if user_exists(node, host) then ok = module:fire_event('message/offline/handle', { username = node, -- username of the recipient of the offline message origin = origin, -- the sender stanza = stanza, }); end if not ok then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end end return true; end module:hook("message/full", function(data) -- message to full JID received local origin, stanza = data.origin, data.stanza; local session = full_sessions[stanza.attr.to]; if session and session.send(stanza) then return true; else -- resource not online return process_to_bare(jid_bare(stanza.attr.to), origin, stanza); end end, -1); module:hook("message/bare", function(data) -- message to bare JID received local origin, stanza = data.origin, data.stanza; return process_to_bare(stanza.attr.to or (origin.username..'@'..origin.host), origin, stanza); end, -1); prosody-13.0.1/plugins/PaxHeaders/mod_mimicking.lua0000644000000000000000000000011714773555365017372 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.619710646 prosody-13.0.1/plugins/mod_mimicking.lua0000644000175000017500000000533114773555365021573 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2012 Florian Zeitz -- Copyright (C) 2019 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 "prosody.util.prosodyctl".show_usage; local usermanager = require "prosody.core.usermanager"; local storagemanager = require "prosody.core.storagemanager"; local skeletons function module.load() if module.host ~= "*" then skeletons = module:open_store("skeletons"); end end module:hook("user-registered", function(user) local skel = skeleton(user.username); local ok, err = skeletons:set(skel, { username = user.username }); if not ok then module:log("error", "Unable to store mimicry data (%q => %q): %s", user.username, skel, err); end end); module:hook_global("user-deleted", function(user) if user.host ~= module.host then return end local skel = skeleton(user.username); local ok, err = skeletons:set(skel, nil); if not ok and err then module:log("error", "Unable to clear mimicry data (%q): %s", skel, err); end end); module:hook("user-registering", function(user) local existing, err = skeletons:get(skeleton(user.username)); if existing then module:log("debug", "Attempt to register username '%s' which could be confused with '%s'", user.username, existing.username); user.allowed = false; elseif err then module:log("error", "Unable to check if new username '%s' can be confused with any existing user: %s", err); end end); function module.command(arg) if (arg[1] ~= "bootstrap" or not arg[2]) then usage("mod_mimicking bootstrap ", "Initialize username mimicry database"); return; end local host = arg[2]; local host_session = prosody.hosts[host]; if not host_session then return "No such host"; end storagemanager.initialize_host(host); usermanager.initialize_host(host); skeletons = storagemanager.open(host, "skeletons"); local count = 0; for user in usermanager.users(host) do local skel = skeleton(user); local existing, err = skeletons:get(skel); if existing and existing.username ~= user then module:log("warn", "Existing usernames '%s' and '%s' are confusable", existing.username, user); elseif err then module:log("error", "Error checking for existing mimicry data (%q = %q): %s", user, skel, err); end local ok, err = skeletons:set(skel, { username = user }); if ok then count = count + 1; elseif err then module:log("error", "Unable to store mimicry data (%q => %q): %s", user, skel, err); end end module:log("info", "%d usernames indexed", count); end prosody-13.0.1/plugins/PaxHeaders/mod_motd.lua0000644000000000000000000000011714773555365016366 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.619710646 prosody-13.0.1/plugins/mod_motd.lua0000644000175000017500000000156114773555365020570 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2010 Jeff Mitchell -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local host = module:get_host(); local motd_text = module:get_option_string("motd_text"); local motd_jid = module:get_option_string("motd_jid", host); if not motd_text then return; end local st = require "prosody.util.stanza"; motd_text = motd_text:gsub("^%s*(.-)%s*$", "%1"):gsub("\n[ \t]+", "\n"); -- Strip indentation from the config module:hook("presence/initial", function (event) local session = event.origin; local motd_stanza = st.message({ to = session.full_jid, from = motd_jid }) :tag("body"):text(motd_text); module:send(motd_stanza); module:log("debug", "MOTD send to user %s", session.full_jid); end, 1); prosody-13.0.1/plugins/PaxHeaders/mod_muc_mam.lua0000644000000000000000000000011714773555365017041 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.623710662 prosody-13.0.1/plugins/mod_muc_mam.lua0000644000175000017500000004247314773555365021252 0ustar00prosodyprosody00000000000000-- XEP-0313: Message Archive Management for Prosody MUC -- Copyright (C) 2011-2021 Kim Alvefur -- -- This file is MIT/X11 licensed. if module:get_host_type() ~= "component" then module:log_status("error", "mod_%s should be loaded only on a MUC component, not normal hosts", module.name); return; end local xmlns_mam = "urn:xmpp:mam:2"; local xmlns_mam_ext = "urn:xmpp:mam:2#extended"; local xmlns_delay = "urn:xmpp:delay"; local xmlns_forward = "urn:xmpp:forward:0"; 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 "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 = require "prosody.util.time".now; local m_min = math.min; 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_period("muc_log_expires_after", "1w"); local default_history_length = 20; 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); end function schedule_cleanup() -- replaced by non-noop later if cleanup is enabled end local log_all_rooms = module:get_option_boolean("muc_log_all_rooms", false); 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_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 if not archive.find then module:log("error", "Attempt to open archive storage returned a driver without archive API support"); module:log("error", "mod_%s does not support archiving", archive._provided_by or archive.name and "storage_"..archive.name.."(?)" or ""); else module:log("error", "Attempt to open archive storage returned null driver"); end module:log("info", "See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information"); return false; end local use_total = module:get_option_boolean("muc_log_include_total", true); local function archiving_enabled(room) if log_all_rooms then module:log("debug", "Archiving all rooms"); return true; end local enabled = room._data.archiving; if enabled == nil then module:log("debug", "Default is %s (for %s)", log_by_default, room.jid); return log_by_default; end module:log("debug", "Logging in room %s is %s", room.jid, enabled); return enabled; end if not log_all_rooms then module:hook("muc-config-form", function(event) local room, form = event.room, event.form; table.insert(form, { name = muc_form_enable, type = "boolean", label = "Archive chat on server", value = archiving_enabled(room), } ); end); module:hook("muc-config-submitted/"..muc_form_enable, function(event) event.room._data.archiving = event.value; event.status_codes[event.value and "170" or "171"] = true; end); end -- Note: We ignore the 'with' field as this is internally used for stanza types local query_form = dataform { { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam }; { name = "with"; type = "jid-single" }; { name = "start"; type = "text-single"; datatype = "xs:dateTime" }; { name = "end"; type = "text-single"; datatype = "xs:dateTime" }; }; if archive.caps and archive.caps.full_id_range then table.insert(query_form, { name = "before-id"; type = "text-single"; }); table.insert(query_form, { name = "after-id"; type = "text-single"; }); end if archive.caps and archive.caps.ids then table.insert(query_form, { name = "ids"; type = "list-multi"; }); end -- Serve form module:hook("iq-get/bare/"..xmlns_mam..":query", function(event) local origin, stanza = event.origin, event.stanza; origin.send(st.reply(stanza):query(xmlns_mam):add_child(query_form:form())); return true; end); -- Handle archive queries module:hook("iq-set/bare/"..xmlns_mam..":query", function(event) local origin, stanza = event.origin, event.stanza; local room_jid = stanza.attr.to; local room_node = jid_split(room_jid); local orig_from = stanza.attr.from; local query = stanza.tags[1]; local room = get_room_from_jid(room_jid); if not room then origin.send(st.error_reply(stanza, "cancel", "item-not-found")) return true; end local from = jid_bare(orig_from); -- Banned or not a member of a members-only room? local from_affiliation = room:get_affiliation(from); if from_affiliation == "outcast" -- banned or room:get_members_only() and not from_affiliation then -- members-only, not a member origin.send(st.error_reply(stanza, "auth", "forbidden")) return true; end local qid = query.attr.queryid; -- Search query parameters local qstart, qend; local qbefore, qafter; local qids; local form = query:get_child("x", "jabber:x:data"); if form then local form_type, err = get_form_type(form); if not form_type then origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid dataform: "..err)); return true; elseif form_type ~= xmlns_mam then origin.send(st.error_reply(stanza, "modify", "bad-request", "Unexpected FORM_TYPE, expected '"..xmlns_mam.."'")); return true; end form, err = query_form:data(form); if err then origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err)))); return true; end qstart, qend = form["start"], form["end"]; qbefore, qafter = form["before-id"], form["after-id"]; qids = form["ids"]; end -- RSM stuff local qset = rsm.get(query); local qmax = m_min(qset and qset.max or default_max_items, max_max_items); local reverse = qset and qset.before or false; local before, after = qset and qset.before or qbefore, qset and qset.after or qafter; if type(before) ~= "string" then before = nil; end -- A reverse query needs to be flipped local flip = reverse; -- A flip-page query needs to be the opposite of that. if query:get_child("flip-page") then flip = not flip end module:log("debug", "Archive query by %s id=%s when=%s...%s rsm=%q", from, qid or stanza.attr.id, qstart and timestamp(qstart) or "", qend and timestamp(qend) or "", qset); -- Load all the data! local data, err = archive:find(room_node, { start = qstart; ["end"] = qend; -- Time range with = "message qmax then -- We requested qmax+1 items. If that many items are retrieved then -- there are more results to page through, so: complete = nil; break; end local fwd_st = st.message(msg_reply_attr) :tag("result", { xmlns = xmlns_mam, queryid = qid, id = id }) :tag("forwarded", { xmlns = xmlns_forward }) :tag("delay", { xmlns = xmlns_delay, stamp = timestamp(when) }):up(); -- Strip tag, containing the original senders JID, unless the room makes this public if room:get_whois() ~= "anyone" then item:maptags(function (tag) if tag.name == "x" and tag.attr.xmlns == xmlns_muc_user then return nil; end return tag; end); end if not is_stanza(item) then item = st.deserialize(item); end item.attr.to = nil; item.attr.xmlns = "jabber:client"; fwd_st:add_child(item); if not first then first = id; end last = id; if flip then results[count] = fwd_st; else origin.send(fwd_st); end end if flip then for i = #results, 1, -1 do origin.send(results[i]); end end if reverse then first, last = last, first; end origin.send(st.reply(stanza) :tag("fin", { xmlns = xmlns_mam, complete = complete }) :add_child(rsm.generate { first = first, last = last, count = total })); -- That's all folks! module:log("debug", "Archive query id=%s completed, %d items returned", qid or stanza.attr.id, complete and count or count - 1); return true; end); module:hook("muc-get-history", function (event) local room = event.room; if not archiving_enabled(room) then return end local room_jid = room.jid; local maxstanzas = event.maxstanzas; local maxchars = event.maxchars; local since = event.since; local to = event.to; if maxstanzas == 0 or maxchars == 0 then return -- No history requested end if not maxstanzas or maxstanzas > get_historylength(room) then maxstanzas = get_historylength(room); end if room._history and #room._history >= maxstanzas then return -- It can deal with this itself end -- Load all the data! local query = { limit = maxstanzas; start = since; reverse = true; with = "message that claim to be from us stanza:maptags(function (tag) if tag.name == "stanza-id" and tag.attr.xmlns == xmlns_st_id and jid_prep(tag.attr.by) == room.jid then return nil; end return tag; end); end, 1); -- Handle messages local function save_to_history(self, stanza) local room_node = jid_split(self.jid); local stored_stanza = stanza; if stanza.name == "message" and self:get_whois() == "anyone" then stored_stanza = st.clone(stanza); stored_stanza.attr.to = nil; local occupant = self._occupants[stanza.attr.from]; if occupant then local actor = jid_bare(occupant.jid); local affiliation = self:get_affiliation(actor) or "none"; local role = self:get_role(actor) or self:get_default_role(affiliation); stored_stanza:add_direct_child(st.stanza("x", { xmlns = xmlns_muc_user }) :tag("item", { affiliation = affiliation; role = role; jid = actor })); end end -- Policy check if not archiving_enabled(self) then return end -- Don't log -- Save the type in the 'with' field, allows storing presence without conflicts local with = stanza.name if stanza.attr.type then with = with .. "<" .. stanza.attr.type end -- And stash it local time = time_now(); local id, err = archive:append(room_node, nil, stored_stanza, time, with); if not id and err == "quota-limit" 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); }); if cleaned then id, err = archive:append(room_node, nil, stored_stanza, time, with); end end if not id and (archive.caps and archive.caps.truncate) then module:log("debug", "Room '%s' over quota, truncating archive", room_node); local truncated = archive:delete(room_node, { truncate = archive_truncate; }); if truncated then id, err = archive:append(room_node, nil, stored_stanza, time, with); end end end if id then schedule_cleanup(room_node); stanza:add_direct_child(st.stanza("stanza-id", { xmlns = xmlns_st_id, by = self.jid, id = id })); else module:log("error", "Could not archive stanza: %s", err); end end module:hook("muc-add-history", function (event) local room, stanza = event.room, event.stanza; save_to_history(room, stanza); end); if module:get_option_boolean("muc_log_presences", false) then module:hook("muc-occupant-joined", function (event) save_to_history(event.room, st.stanza("presence", { from = event.nick }):tag("x", { xmlns = "http://jabber.org/protocol/muc" })); end); module:hook("muc-occupant-left", function (event) save_to_history(event.room, st.stanza("presence", { type = "unavailable", from = event.nick })); end); end if not archive.delete then module:log("warn", "Storage driver %s does not support deletion", archive._provided_by); module:log("warn", "Archived message will persist after a room has been destroyed"); else module:hook("muc-room-destroyed", function(event) local room_node = jid_split(event.room.jid); archive:delete(room_node); end); end -- And role/affiliation changes? module:add_feature(xmlns_mam); local advertise_extended = archive.caps and archive.caps.full_id_range and archive.caps.ids; module:hook("muc-disco#info", function(event) if archiving_enabled(event.room) then event.reply:tag("feature", {var=xmlns_mam}):up(); if advertise_extended then (event.reply or event.stanza):tag("feature", {var=xmlns_mam_ext}):up(); end end event.reply:tag("feature", {var=xmlns_st_id}):up(); end); -- Cleanup 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"); module:log("debug", "muc_log_expires_after = %d -- in seconds", cleanup_after); if not archive.delete then module:log("error", "muc_log_expires_after set but mod_%s does not support deleting", archive._provided_by); return false; end -- For each day, store a set of rooms that have new messages. To expire -- messages, we collect the union of sets of rooms from dates that fall -- outside the cleanup range. 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(); if last_date:get(roomname) == date then return end local ok = cleanup_map:set(date, roomname, true); if ok then last_date:set(roomname, date); end end end local cleanup_time = module:measure("cleanup", "times"); local async = require "prosody.util.async"; module:daily("Remove expired messages", function () local cleanup_done = cleanup_time(); if archive.caps and archive.caps.wildcard_delete then local ok, err = archive:delete(true, { ["end"] = os.time() - cleanup_after }) if ok then local sum = tonumber(ok); if sum then module:log("info", "Deleted %d expired messages", sum); else -- driver did not tell module:log("info", "Deleted all expired messages"); end else module:log("error", "Could not delete messages: %s", err); end cleanup_done(); return; end local rooms = {}; local cut_off = datestamp(os.time() - cleanup_after); for date in cleanup_storage:users() do if date <= cut_off then module:log("debug", "Messages from %q should be expired", date); local messages_this_day = cleanup_storage:get(date); if messages_this_day then for room in pairs(messages_this_day) do rooms[room] = true; end if date < cut_off then -- Messages from the same day as the cut-off might not have expired yet, -- but all earlier will have, so clear storage for those days. cleanup_storage:set(date, nil); end end end end local sum, num_rooms = 0, 0; for room in pairs(rooms) do local ok, err = archive:delete(room, { ["end"] = os.time() - cleanup_after; }) if ok then num_rooms = num_rooms + 1; sum = sum + (tonumber(ok) or 0); else cleanup_map:set(cut_off, room, true); module:log("error", "Could not delete messages for room '%s': %s", room, err); end local wait, done = async.waiter(); module:add_timer(0.01, done); wait(); end module:log("info", "Deleted %d expired messages for %d rooms", sum, num_rooms); cleanup_done(); end); else module:log("debug", "Archive expiry disabled"); -- Don't ask the backend to count the potentially unbounded number of items, -- it'll get slow. use_total = module:get_option_boolean("mam_include_total", false); end prosody-13.0.1/plugins/PaxHeaders/mod_muc_unique.lua0000644000000000000000000000011714773555365017575 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.623710662 prosody-13.0.1/plugins/mod_muc_unique.lua0000644000175000017500000000076614773555365022005 0ustar00prosodyprosody00000000000000-- XEP-0307: Unique Room Names for Multi-User Chat 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; origin.send(st.reply(stanza) :tag("unique", {xmlns = "http://jabber.org/protocol/muc#unique"}) :text(unique_name():lower()) ); return true; end,-1); prosody-13.0.1/plugins/PaxHeaders/mod_net_multiplex.lua0000644000000000000000000000011714773555365020314 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.623710662 prosody-13.0.1/plugins/mod_net_multiplex.lua0000644000175000017500000000650214773555365022516 0ustar00prosodyprosody00000000000000module:set_global(); 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 "prosody.core.portmanager"; local available_services = {}; local service_by_protocol = {}; local available_protocols = array(); local function add_service(service) local multiplex_pattern = service.multiplex and service.multiplex.pattern; local protocol_name = service.multiplex and service.multiplex.protocol; if protocol_name then module:log("debug", "Adding multiplex service %q with protocol %q", service.name, protocol_name); service_by_protocol[protocol_name] = service; available_protocols:push(protocol_name); end if multiplex_pattern then module:log("debug", "Adding multiplex service %q with pattern %q", service.name, multiplex_pattern); available_services[service] = multiplex_pattern; elseif not protocol_name then module:log("debug", "Service %q is not multiplex-capable", service.name); end end module:hook("service-added", function (event) add_service(event.service); end); module:hook("service-removed", function (event) available_services[event.service] = nil; if event.service.multiplex and event.service.multiplex.protocol then available_protocols:filter(function (p) return p ~= event.service.multiplex.protocol end); service_by_protocol[event.service.multiplex.protocol] = nil; end end); for _, services in pairs(portmanager.get_registered_services()) do for _, service in ipairs(services) do add_service(service); end end local buffers = {}; local listener = { default_mode = max_buffer_len }; function listener.onconnect(conn) local sock = conn:socket(); if sock.getalpn then local selected_proto = sock:getalpn(); local service = service_by_protocol[selected_proto]; if service then module:log("debug", "Routing incoming connection to %s based on ALPN %q", service.name, selected_proto); local next_listener = service.listener; conn:setlistener(next_listener); conn:set_mode(next_listener.default_mode or default_mode); local onconnect = next_listener.onconnect; if onconnect then return onconnect(conn) end end end end function listener.onincoming(conn, data) if not data then return; end local buf = buffers[conn]; buf = buf and buf..data or data; for service, multiplex_pattern in pairs(available_services) do if buf:match(multiplex_pattern) then module:log("debug", "Routing incoming connection to %s", service.name); local next_listener = service.listener; conn:setlistener(next_listener); conn:set_mode(next_listener.default_mode or default_mode); local onconnect = next_listener.onconnect; if onconnect then onconnect(conn) end return next_listener.onincoming(conn, buf); end end if #buf > max_buffer_len then -- Give up conn:close(); else buffers[conn] = buf; end end function listener.ondisconnect(conn) buffers[conn] = nil; -- warn if no buffer? end listener.ondetach = listener.ondisconnect; module:provides("net", { name = "multiplex"; config_prefix = ""; listener = listener; }); module:provides("net", { name = "multiplex_ssl"; config_prefix = "ssl"; encryption = "ssl"; ssl_config = { alpn = function () return available_protocols; end; }; listener = listener; }); prosody-13.0.1/plugins/PaxHeaders/mod_offline.lua0000644000000000000000000000011714773555365017045 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.623710662 prosody-13.0.1/plugins/mod_offline.lua0000644000175000017500000000263614773555365021253 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2009 Matthew Wild -- Copyright (C) 2008-2009 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local datetime = require "prosody.util.datetime"; local jid_split = require "prosody.util.jid".split; local offline_messages = module:open_store("offline", "archive"); module:add_feature("msgoffline"); module:hook("message/offline/handle", function(event) local origin, stanza = event.origin, event.stanza; local to = stanza.attr.to; local node; if to then node = jid_split(to) else node = origin.username; end local ok = offline_messages:append(node, nil, stanza, os.time(), ""); if ok then module:log("debug", "Saved to offline storage: %s", stanza:top_tag()); end return ok; end, -1); module:hook("message/offline/broadcast", function(event) local origin = event.origin; origin.log("debug", "Broadcasting offline messages"); local node, host = origin.username, origin.host; local data = offline_messages:find(node); if not data then return true; end for _, stanza, when in data do stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime(when)}):up(); -- XEP-0203 origin.send(stanza); end local ok = offline_messages:delete(node); if type(ok) == "number" and ok > 0 then origin.log("debug", "%d offline messages consumed"); end return true; end, -1); prosody-13.0.1/plugins/PaxHeaders/mod_pep.lua0000644000000000000000000000011714773555365016207 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.623710662 prosody-13.0.1/plugins/mod_pep.lua0000644000175000017500000003751714773555365020423 0ustar00prosodyprosody00000000000000local 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(); -- username -> object passed to module:add_items() local pep_service_items = {}; -- size of caches with full pubsub service objects 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, _) local item = pep_service_items[username]; pep_service_items[username] = nil; if item then module:remove_item("pep-service", item); end end):table(); -- size of caches with smaller objects 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(); -- caps hash -> set of nodes local hash_map = cache.new(info_cache_size):table(); 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, 0); local function tonumber_max_items(n) if n == "max" then return max_max_items; end return tonumber(n); end for _, field in ipairs(lib_pubsub.node_config_form) do if field.var == "pubsub#max_items" then field.range_max = max_max_items; break; end end function module.save() return { recipients = recipients; }; end function module.restore(data) recipients = data.recipients; end function is_item_stanza(item) return st.is_stanza(item) and item.attr.xmlns == xmlns_pubsub and item.name == "item" and #item.tags == 1; end function check_node_config(node, actor, new_config) -- luacheck: ignore 212/node 212/actor if (tonumber_max_items(new_config["max_items"]) or 1) > max_max_items then 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; end return true; end local function subscription_presence(username, recipient) local user_bare = jid_join(username, host); local recipient_bare = jid_bare(recipient); if (recipient_bare == user_bare) then return true; end return is_contact_subscribed(username, host, recipient_bare); end local function nodestore(username) -- luacheck: ignore 212/self local store = {}; function store:get(node) local data, err = node_config:get(username, node) if data == true then -- COMPAT Previously stored only a boolean representing 'persist_items' data = { name = node; config = {}; subscribers = {}; affiliations = {}; }; end return data, err; end function store:set(node, data) return node_config:set(username, node, data); end function store:users() return pairs(known_nodes:get(username) or {}); end return store; end local function simple_itemstore(username) local driver = storagemanager.get_driver(module.host, "pep_data"); return function (config, node) local max_items = tonumber_max_items(config["max_items"]); module:log("debug", "Creating new persistent item store for user %s, node %q", username, node); local archive = driver:open("pep_"..node, "archive"); return lib_pubsub.archive_itemstore(archive, max_items, username, node, false); end 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 container end if item then item = st.clone(item); item.attr.xmlns = nil; -- Clear the pubsub namespace if kind == "items" then 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 local id = new_id(); local message = st.message({ from = user_bare, type = "headline", id = id }) :tag("event", { xmlns = xmlns_pubsub_event }) :tag(kind, { node = node }); if item then message:add_child(item); end for jid in pairs(jids) do module:log("debug", "Sending notification to %s from %s for node %s", jid, user_bare, node); message.attr.to = jid; module:send(message); end end return simple_broadcast; end local function get_subscriber_filter(username) return function (jids, node) local broadcast_to = {}; for jid, opts in pairs(jids) do broadcast_to[jid] = opts; end local service_recipients = recipients[username]; if service_recipients then local service = services[username]; for recipient, nodes in pairs(service_recipients) do if nodes:contains(node) and service:may(node, recipient, "subscribe") then broadcast_to[recipient] = true; end end end return broadcast_to; end end -- Read-only service with no nodes where nobody is allowed anything to act as a -- fallback for interactions with non-existent users local nobody_service = pubsub.new({ node_defaults = { ["max_items"] = 1; ["persist_items"] = false; ["access_model"] = "presence"; ["send_last_published_item"] = "on_sub_and_presence"; }; autocreate_on_publish = false; autocreate_on_subscribe = false; get_affiliation = function () return "outcast"; end; }); function get_pep_service(username) local user_bare = jid_join(username, host); local service = services[username]; if service then return service; end if not usermanager.user_exists(username, host) then return nobody_service; end module:log("debug", "Creating pubsub service for user %q", username); service = pubsub.new({ pep_username = username; node_defaults = { ["max_items"] = 1; ["persist_items"] = true; ["access_model"] = "presence"; ["send_last_published_item"] = "on_sub_and_presence"; }; max_items = max_max_items; autocreate_on_publish = true; autocreate_on_subscribe = false; nodestore = nodestore(username); itemstore = simple_itemstore(username); broadcaster = get_broadcaster(username); subscriber_filter = get_subscriber_filter(username); itemcheck = is_item_stanza; get_affiliation = function (jid) if jid_bare(jid) == user_bare then return "owner"; end end; access_models = { presence = function (jid) if subscription_presence(username, jid) then return "member"; 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; normalize_jid = jid_bare; check_node_config = check_node_config; }); services[username] = service; local item = { service = service, jid = user_bare } pep_service_items[username] = item; module:add_item("pep-service", item); return service; end function handle_pubsub_iq(event) local origin, stanza = event.origin, event.stanza; local service_name = origin.username; if stanza.attr.to ~= nil then service_name = jid_split(stanza.attr.to); end local service = get_pep_service(service_name); return lib_pubsub.handle_pubsub_iq(event, service) end module:hook("iq/bare/"..xmlns_pubsub..":pubsub", handle_pubsub_iq); module:hook("iq/bare/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq); local function get_caps_hash_from_presence(stanza, current) local t = stanza.attr.type; if not t then local child = stanza:get_child("c", "http://jabber.org/protocol/caps"); if child then local attr = child.attr; if attr.hash then -- new caps if attr.hash == 'sha-1' and attr.node and attr.ver then return attr.ver, attr.node.."#"..attr.ver; end else -- legacy caps if attr.node and attr.ver then return attr.node.."#"..attr.ver.."#"..(attr.ext or ""), attr.node.."#"..attr.ver; end end end return; -- no or bad caps elseif t == "unavailable" or t == "error" then return; end return current; -- no caps, could mean caps optimization, so return current end local function resend_last_item(jid, node, service) local ok, config = service:get_node_config(node, true); 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, true, service.nodes[node], service); end local function update_subscriptions(recipient, service_name, nodes) nodes = nodes or empty_set; local service_recipients = recipients[service_name]; if not service_recipients then service_recipients = {}; recipients[service_name] = service_recipients; end local current = service_recipients[recipient]; if not current then current = empty_set; end if (current == empty_set or current:empty()) and (nodes == empty_set or nodes:empty()) then return; end local service = get_pep_service(service_name); for node in nodes - current do if service:may(node, recipient, "subscribe") then resend_last_item(recipient, node, service); end end if nodes == empty_set or nodes:empty() then nodes = nil; end service_recipients[recipient] = nodes; end module:hook("presence/bare", function(event) -- inbound presence to bare JID received local origin, stanza = event.origin, event.stanza; local t = stanza.attr.type; local is_self = not stanza.attr.to; local username = jid_split(stanza.attr.to); local user_bare = jid_bare(stanza.attr.to); if is_self then username = origin.username; user_bare = jid_join(username, host); end if not t then -- available presence if is_self or subscription_presence(username, stanza.attr.from) then local recipient = stanza.attr.from; local current = recipients[username] and recipients[username][recipient]; local hash, query_node = get_caps_hash_from_presence(stanza, current); if current == hash or (current and current == hash_map[hash]) then return; end if not hash then update_subscriptions(recipient, username); else recipients[username] = recipients[username] or {}; if hash_map[hash] then update_subscriptions(recipient, username, hash_map[hash]); else -- COMPAT from ~= stanza.attr.to because OneTeam can't deal with missing from attribute origin.send( st.stanza("iq", {from=user_bare, to=stanza.attr.from, id="disco", type="get"}) :tag("query", {xmlns = "http://jabber.org/protocol/disco#info", node = query_node}) ); end end end elseif t == "unavailable" then update_subscriptions(stanza.attr.from, username); elseif not is_self and t == "unsubscribe" then local from = jid_bare(stanza.attr.from); local subscriptions = recipients[username]; if subscriptions then for subscriber in pairs(subscriptions) do if jid_bare(subscriber) == from then update_subscriptions(subscriber, username); end end end end end, 10); module:hook("iq-result/bare/disco", function(event) local origin, stanza = event.origin, event.stanza; local disco = stanza:get_child("query", "http://jabber.org/protocol/disco#info"); if not disco then return; end -- Process disco response local is_self = stanza.attr.to == nil; local user_bare = jid_bare(stanza.attr.to); local username = jid_split(stanza.attr.to); if is_self then username = origin.username; user_bare = jid_join(username, host); end local contact = stanza.attr.from; local ver = calculate_hash(disco.tags); -- calculate hash local notify = set_new(); for _, feature in pairs(disco.tags) do if feature.name == "feature" and feature.attr.var then local nfeature = feature.attr.var:match("^(.*)%+notify$"); if nfeature then notify:add(nfeature); end end end hash_map[ver] = notify; -- update hash map if is_self then -- Optimization: Fiddle with other local users for jid, item in pairs(origin.roster) do -- for all interested contacts if jid then local contact_node, contact_host = jid_split(jid); if contact_host == host and (item.subscription == "both" or item.subscription == "from") then update_subscriptions(user_bare, contact_node, notify); end end end end update_subscriptions(contact, username, notify); end); module:hook("account-disco-info-node", function(event) local stanza, origin = event.stanza, event.origin; local service_name = origin.username; if stanza.attr.to ~= nil then service_name = jid_split(stanza.attr.to); end local service = get_pep_service(service_name); return lib_pubsub.handle_disco_info_node(event, service); end); module:hook("account-disco-info", function(event) local origin, reply = event.origin, event.reply; reply:tag('identity', {category='pubsub', type='pep'}):up(); local username = jid_split(reply.attr.from) or origin.username; local service = get_pep_service(username); local supported_features = lib_pubsub.get_feature_set(service) + set.new{ -- Features not covered by the above "auto-subscribe", "filtered-notifications", "last-published", "presence-notifications", "presence-subscribe", }; reply:tag('feature', {var=xmlns_pubsub}):up(); for feature in supported_features do reply:tag('feature', {var=xmlns_pubsub.."#"..feature}):up(); end end); module:hook("account-disco-items-node", function(event) local stanza, origin = event.stanza, event.origin; local is_self = stanza.attr.to == nil; local username = jid_split(stanza.attr.to); if is_self then username = origin.username; end local service = get_pep_service(username); return lib_pubsub.handle_disco_items_node(event, service); end); module:hook("account-disco-items", function(event) local reply, stanza, origin = event.reply, event.stanza, event.origin; local is_self = stanza.attr.to == nil; local user_bare = jid_bare(stanza.attr.to); local username = jid_split(stanza.attr.to); if is_self then username = origin.username; user_bare = jid_join(username, host); end local service = get_pep_service(username); local ok, ret = service:get_nodes(jid_bare(stanza.attr.from)); if not ok then return; end for node, node_obj in pairs(ret) do reply:tag("item", { jid = user_bare, node = node, name = node_obj.config.title }):up(); end end); module:hook_global("user-deleted", function(event) if event.host ~= host then return end local username = event.username; local service = services[username]; if not service then return end for node in pairs(service.nodes) do service:delete(node, true); end local item = pep_service_items[username]; pep_service_items[username] = nil; if item then module:remove_item("pep-service", item); end recipients[username] = nil; end); module:require("mod_pubsub/commands").add_commands(function (service_jid) return get_pep_service((jid_split(service_jid))); end); prosody-13.0.1/plugins/PaxHeaders/mod_pep_plus.lua0000644000000000000000000000011714773555365017252 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.623710662 prosody-13.0.1/plugins/mod_pep_plus.lua0000644000175000017500000000022114773555365021444 0ustar00prosodyprosody00000000000000module:log("error", "mod_pep_plus has been renamed to mod_pep, please update your config file. Auto-loading mod_pep..."); module:depends("pep"); prosody-13.0.1/plugins/PaxHeaders/mod_pep_simple.lua0000644000000000000000000000011714773555365017560 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.627710677 prosody-13.0.1/plugins/mod_pep_simple.lua0000644000175000017500000002644514773555365021772 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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; local calculate_hash = require "prosody.util.caps".calculate_hash; local core_post_stanza = prosody.core_post_stanza; local bare_sessions = prosody.bare_sessions; local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; -- Used as canonical 'empty table' local NULL = {}; -- data[user_bare_jid][node] = item_stanza local data = {}; --- recipients[user_bare_jid][contact_full_jid][subscribed_node] = true local recipients = {}; -- hash_map[hash][subscribed_nodes] = true local hash_map = {}; module.save = function() return { data = data, recipients = recipients, hash_map = hash_map }; end module.restore = function(state) data = state.data or {}; recipients = state.recipients or {}; hash_map = state.hash_map or {}; end local function subscription_presence(user_bare, recipient) local recipient_bare = jid_bare(recipient); if (recipient_bare == user_bare) then return true end local username, host = jid_split(user_bare); return is_contact_subscribed(username, host, recipient_bare); end module:hook("pep-publish-item", function (event) local session, bare, node, id, item = event.session, event.user, event.node, event.id, event.item; item.attr.xmlns = nil; local disable = #item.tags ~= 1 or #item.tags[1] == 0; if #item.tags == 0 then item.name = "retract"; end local stanza = st.message({from=bare, type='headline'}) :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'}) :tag('items', {node=node}) :add_child(item) :up() :up(); -- store for the future local user_data = data[bare]; if disable then if user_data then user_data[node] = nil; if not next(user_data) then data[bare] = nil; end end else if not user_data then user_data = {}; data[bare] = user_data; end user_data[node] = {id, item}; end -- broadcast for recipient, notify in pairs(recipients[bare] or NULL) do if notify[node] then stanza.attr.to = recipient; core_post_stanza(session, stanza); end end end); local function publish_all(user, recipient, session) local d = data[user]; local notify = recipients[user] and recipients[user][recipient]; if d and notify then for node in pairs(notify) do if d[node] then -- luacheck: ignore id local id, item = unpack(d[node]); session.send(st.message({from=user, to=recipient, type='headline'}) :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'}) :tag('items', {node=node}) :add_child(item) :up() :up()); end end end end local function get_caps_hash_from_presence(stanza, current) local t = stanza.attr.type; if not t then for _, child in pairs(stanza.tags) do if child.name == "c" and child.attr.xmlns == "http://jabber.org/protocol/caps" then local attr = child.attr; if attr.hash then -- new caps if attr.hash == 'sha-1' and attr.node and attr.ver then return attr.ver, attr.node.."#"..attr.ver; end else -- legacy caps if attr.node and attr.ver then return attr.node.."#"..attr.ver.."#"..(attr.ext or ""), attr.node.."#"..attr.ver; end end return; -- bad caps format end end elseif t == "unavailable" or t == "error" then return; end return current; -- no caps, could mean caps optimization, so return current end module:hook("presence/bare", function(event) -- inbound presence to bare JID received local origin, stanza = event.origin, event.stanza; local user = stanza.attr.to or (origin.username..'@'..origin.host); local t = stanza.attr.type; local self = not stanza.attr.to; -- Only cache subscriptions if user is online if not bare_sessions[user] then return; end if not t then -- available presence if self or subscription_presence(user, stanza.attr.from) then local recipient = stanza.attr.from; local current = recipients[user] and recipients[user][recipient]; local hash = get_caps_hash_from_presence(stanza, current); if current == hash or (current and current == hash_map[hash]) then return; end if not hash then if recipients[user] then recipients[user][recipient] = nil; end else recipients[user] = recipients[user] or {}; if hash_map[hash] then recipients[user][recipient] = hash_map[hash]; publish_all(user, recipient, origin); else recipients[user][recipient] = hash; local from_bare = origin.type == "c2s" and origin.username.."@"..origin.host; if self or origin.type ~= "c2s" or (recipients[from_bare] and recipients[from_bare][origin.full_jid]) ~= hash then -- COMPAT from ~= stanza.attr.to because OneTeam and Asterisk 1.8 can't deal with missing from attribute origin.send( st.stanza("iq", {from=user, to=stanza.attr.from, id="disco", type="get"}) :query("http://jabber.org/protocol/disco#info") ); end end end end elseif t == "unavailable" then if recipients[user] then recipients[user][stanza.attr.from] = nil; end elseif not self and t == "unsubscribe" then local from = jid_bare(stanza.attr.from); local subscriptions = recipients[user]; if subscriptions then for subscriber in pairs(subscriptions) do if jid_bare(subscriber) == from then recipients[user][subscriber] = nil; end end end end end, 10); module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) local session, stanza = event.origin, event.stanza; local payload = stanza.tags[1]; if stanza.attr.type == 'set' and (not stanza.attr.to or jid_bare(stanza.attr.from) == stanza.attr.to) then payload = payload.tags[1]; -- if payload and (payload.name == 'publish' or payload.name == 'retract') and payload.attr.node then local node = payload.attr.node; payload = payload.tags[1]; if payload and payload.name == "item" then -- local id = payload.attr.id or "1"; payload.attr.id = id; session.send(st.reply(stanza)); module:fire_event("pep-publish-item", { node = node, user = jid_bare(session.full_jid), actor = session.jid, id = id, session = session, item = st.clone(payload); }); return true; else module:log("debug", "Payload is missing the ", node); end else module:log("debug", "Unhandled payload: %s", payload and payload:top_tag() or "(no payload)"); end elseif stanza.attr.type == 'get' then local user = stanza.attr.to and jid_bare(stanza.attr.to) or session.username..'@'..session.host; if subscription_presence(user, stanza.attr.from) then local user_data = data[user]; local node, requested_id; payload = payload.tags[1]; if payload and payload.name == 'items' then node = payload.attr.node; local item = payload.tags[1]; if item and item.name == "item" then requested_id = item.attr.id; end end if node and user_data and user_data[node] then -- Send the last item local id, item = unpack(user_data[node]); if not requested_id or id == requested_id then local reply_stanza = st.reply(stanza) :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'}) :tag('items', {node=node}) :add_child(item) :up() :up(); session.send(reply_stanza); return true; else -- requested item doesn't exist local reply_stanza = st.reply(stanza) :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'}) :tag('items', {node=node}) :up(); session.send(reply_stanza); return true; end elseif node then -- node doesn't exist session.send(st.error_reply(stanza, 'cancel', 'item-not-found')); module:log("debug", "Item '%s' not found", node) return true; else --invalid request session.send(st.error_reply(stanza, 'modify', 'bad-request')); module:log("debug", "Invalid request: %s", payload); return true; end else --no presence subscription session.send(st.error_reply(stanza, 'auth', 'not-authorized') :tag('presence-subscription-required', {xmlns='http://jabber.org/protocol/pubsub#errors'})); module:log("debug", "Unauthorized request: %s", payload); return true; end end end); module:hook("iq-result/bare/disco", function(event) local session, stanza = event.origin, event.stanza; if stanza.attr.type == "result" then local disco = stanza.tags[1]; if disco and disco.name == "query" and disco.attr.xmlns == "http://jabber.org/protocol/disco#info" then -- Process disco response local self = not stanza.attr.to; local user = stanza.attr.to or (session.username..'@'..session.host); local contact = stanza.attr.from; local current = recipients[user] and recipients[user][contact]; if type(current) ~= "string" then return; end -- check if waiting for recipient's response local ver = current; if not string.find(current, "#") then ver = calculate_hash(disco.tags); -- calculate hash end local notify = {}; for _, feature in pairs(disco.tags) do if feature.name == "feature" and feature.attr.var then local nfeature = feature.attr.var:match("^(.*)%+notify$"); if nfeature then notify[nfeature] = true; end end end hash_map[ver] = notify; -- update hash map if self then for jid, item in pairs(session.roster) do -- for all interested contacts if item.subscription == "both" or item.subscription == "from" then if not recipients[jid] then recipients[jid] = {}; end recipients[jid][contact] = notify; publish_all(jid, contact, session); end end end recipients[user][contact] = notify; -- set recipient's data to calculated data -- send messages to recipient publish_all(user, contact, session); end end end); module:hook("account-disco-info", function(event) local reply = event.reply; reply:tag('identity', {category='pubsub', type='pep'}):up(); reply:tag('feature', {var=xmlns_pubsub}):up(); local features = { "access-presence", "auto-create", "auto-subscribe", "filtered-notifications", "item-ids", "last-published", "presence-notifications", "presence-subscribe", "publish", "retract-items", "retrieve-items", }; for _, feature in ipairs(features) do reply:tag('feature', {var=xmlns_pubsub.."#"..feature}):up(); end end); module:hook("account-disco-items", function(event) local reply = event.reply; local bare = reply.attr.to; local user_data = data[bare]; if user_data then for node, _ in pairs(user_data) do reply:tag('item', {jid=bare, node=node}):up(); end end end); module:hook("account-disco-info-node", function (event) local stanza, node = event.stanza, event.node; local user = stanza.attr.to; local user_data = data[user]; if user_data and user_data[node] then event.exists = true; event.reply:tag('identity', {category='pubsub', type='leaf'}):up(); end end); module:hook("resource-unbind", function (event) local user_bare_jid = event.session.username.."@"..event.session.host; if not bare_sessions[user_bare_jid] then -- User went offline -- We don't need this info cached anymore, clear it. recipients[user_bare_jid] = nil; end end); prosody-13.0.1/plugins/PaxHeaders/mod_ping.lua0000644000000000000000000000011714773555365016360 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.627710677 prosody-13.0.1/plugins/mod_ping.lua0000644000175000017500000000100314773555365020551 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "prosody.util.stanza"; module:add_feature("urn:xmpp:ping"); local function ping_handler(event) event.origin.send(st.reply(event.stanza)); return true; end module:hook("iq-get/bare/urn:xmpp:ping:ping", ping_handler); module:hook("iq-get/host/urn:xmpp:ping:ping", ping_handler); prosody-13.0.1/plugins/PaxHeaders/mod_posix.lua0000644000000000000000000000011714773555365016565 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.627710677 prosody-13.0.1/plugins/mod_posix.lua0000644000175000017500000000044714773555365020771 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- module:set_global(); -- we're a global module -- TODO delete this whole concept prosody-13.0.1/plugins/PaxHeaders/mod_presence.lua0000644000000000000000000000011714773555365017227 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.627710677 prosody-13.0.1/plugins/mod_presence.lua0000644000175000017500000004005614773555365021433 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local log = module._log; local require = require; local pairs = pairs; local s_find = string.find; local tonumber = tonumber; local core_post_stanza = prosody.core_post_stanza; local core_process_stanza = prosody.core_process_stanza; 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 "prosody.core.rostermanager"; local sessionmanager = require "prosody.core.sessionmanager"; local recalc_resource_map = require "prosody.util.presence".recalc_resource_map; local ignore_presence_priority = module:get_option_boolean("ignore_presence_priority", false); local pre_approval_stream_feature = st.stanza("sub", {xmlns="urn:xmpp:features:pre-approval"}); module:hook("stream-features", function(event) local origin, features = event.origin, event.features; if origin.username then features:add_child(pre_approval_stream_feature); end end); function handle_normal_presence(origin, stanza) if ignore_presence_priority then local priority = stanza:get_child("priority"); if priority and priority[1] ~= "0" then for i=#priority.tags,1,-1 do priority.tags[i] = nil; end for i=#priority,2,-1 do priority[i] = nil; end priority[1] = "0"; end end local priority = stanza:get_child_text("priority"); if priority and s_find(priority, "^[+-]?[0-9]+$") then priority = tonumber(priority); if priority < -128 then priority = -128 end if priority > 127 then priority = 127 end else priority = 0; end local node, host = origin.username, origin.host; local roster = origin.roster; if full_sessions[origin.full_jid] then -- if user is still connected origin.send(stanza); -- reflect their presence back to them end local user = bare_sessions[node.."@"..host]; for _, res in pairs(user and user.sessions or NULL) do -- broadcast to all resources if res ~= origin and res.presence then -- to resource stanza.attr.to = res.full_jid; core_post_stanza(origin, stanza, true); end end for jid, item in pairs(roster) do -- broadcast to all interested contacts if item.subscription == "both" or item.subscription == "from" then stanza.attr.to = jid; core_post_stanza(origin, stanza, true); end end -- It's possible that after the network activity above, the origin -- has been disconnected (particularly if something happened while -- sending the reflection). So we abort further presence processing -- in that case. if not origin.type then return; end stanza.attr.to = nil; if stanza.attr.type == nil and not origin.presence then -- initial presence module:fire_event("presence/initial", { origin = origin, stanza = stanza } ); origin.presence = stanza; -- FIXME repeated later local probe = st.presence({from = origin.full_jid, type = "probe"}); for jid, item in pairs(roster) do -- probe all contacts we are subscribed to if item.subscription == "both" or item.subscription == "to" then probe.attr.to = jid; core_post_stanza(origin, probe, true); end end for _, res in pairs(user and user.sessions or NULL) do -- broadcast from all available resources if res ~= origin and res.presence then res.presence.attr.to = origin.full_jid; core_post_stanza(res, res.presence, true); res.presence.attr.to = nil; end end for jid, pending_request in pairs(roster[false].pending) do -- resend incoming subscription requests if type(pending_request) == "table" then local subscribe = st.deserialize(pending_request); subscribe.attr.type, subscribe.attr.from = "subscribe", jid; origin.send(subscribe); else origin.send(st.presence({type="subscribe", from=jid})); end end local request = st.presence({type="subscribe", from=origin.username.."@"..origin.host}); for jid, item in pairs(roster) do -- resend outgoing subscription requests if item.ask then request.attr.to = jid; core_post_stanza(origin, request, true); end end if priority >= 0 then local event = { origin = origin } module:fire_event('message/offline/broadcast', event); end end if stanza.attr.type == "unavailable" then origin.presence = nil; if origin.priority then origin.priority = nil; recalc_resource_map(user); end if origin.directed then for jid in pairs(origin.directed) do stanza.attr.to = jid; core_post_stanza(origin, stanza, true); end origin.directed = nil; end else origin.presence = stanza; stanza:tag("delay", { xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime() }):up(); if origin.priority ~= priority then origin.priority = priority; recalc_resource_map(user); end end stanza.attr.to = nil; -- reset it end -- luacheck: ignore 212/recipient_session -- TODO This argument is used in 3rd party modules function send_presence_of_available_resources(user, host, jid, recipient_session, stanza) local h = hosts[host]; local count = 0; if h and h.type == "local" then local u = h.sessions[user]; if u then for _, session in pairs(u.sessions) do local pres = session.presence; if pres then if stanza then pres = stanza; pres.attr.from = session.full_jid; end pres.attr.to = jid; core_post_stanza(session, pres, true); pres.attr.to = nil; count = count + 1; end end end end log("debug", "broadcasted presence of %d resources from %s@%s to %s", count, user, host, jid); return count; end function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare) local node, host = jid_split(from_bare); if to_bare == from_bare then return; end -- No self contacts local st_from, st_to = stanza.attr.from, stanza.attr.to; stanza.attr.from, stanza.attr.to = from_bare, to_bare; log("debug", "outbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare); if stanza.attr.type == "probe" then stanza.attr.from, stanza.attr.to = st_from, st_to; return; elseif stanza.attr.type == "subscribe" then -- 1. route stanza -- 2. roster push (subscription = none, ask = subscribe) if rostermanager.set_contact_pending_out(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); end -- else file error core_post_stanza(origin, stanza); elseif stanza.attr.type == "unsubscribe" then -- 1. route stanza -- 2. roster push (subscription = none or from) if rostermanager.unsubscribe(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed? end -- else file error core_post_stanza(origin, stanza); elseif stanza.attr.type == "subscribed" then -- 1. route stanza -- 2. roster_push () -- 3. send_presence_of_available_resources if rostermanager.subscribed(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); end if rostermanager.is_contact_subscribed(node, host, to_bare) then core_post_stanza(origin, stanza); send_presence_of_available_resources(node, host, to_bare, origin); end if rostermanager.is_user_subscribed(node, host, to_bare) then core_post_stanza(origin, st.presence({ type = "probe", from = from_bare, to = to_bare })); end elseif stanza.attr.type == "unsubscribed" then -- 1. send unavailable -- 2. route stanza -- 3. roster push (subscription = from or both) -- luacheck: ignore 211/pending_in -- Is pending_in meant to be used? local success, pending_in, subscribed = rostermanager.unsubscribed(node, host, to_bare); if success then if subscribed then rostermanager.roster_push(node, host, to_bare); end core_post_stanza(origin, stanza); if subscribed then send_presence_of_available_resources(node, host, to_bare, origin, st.presence({ type = "unavailable" })); end end else origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type")); end stanza.attr.from, stanza.attr.to = st_from, st_to; return true; end function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare) local node, host = jid_split(to_bare); local st_from, st_to = stanza.attr.from, stanza.attr.to; stanza.attr.from, stanza.attr.to = from_bare, to_bare; log("debug", "inbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare); if stanza.attr.type == "probe" then local result, err = rostermanager.is_contact_subscribed(node, host, from_bare); if result then if 0 == send_presence_of_available_resources(node, host, st_from, origin) then core_post_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"}), true); -- TODO send last activity end elseif not err then core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}), true); end elseif stanza.attr.type == "subscribe" then if rostermanager.is_contact_subscribed(node, host, from_bare) then core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"}), true); -- already subscribed -- Sending presence is not clearly stated in the RFC, but it seems appropriate if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- TODO send last activity end elseif rostermanager.is_contact_preapproved(node, host, from_bare) then if not rostermanager.is_contact_pending_in(node, host, from_bare) then if rostermanager.set_contact_pending_in(node, host, from_bare, stanza) then core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"}), true); end -- TODO else return error, unable to save end else core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt if not rostermanager.is_contact_pending_in(node, host, from_bare) then if rostermanager.set_contact_pending_in(node, host, from_bare, stanza) then sessionmanager.send_to_available_resources(node, host, stanza); end -- TODO else return error, unable to save end end elseif stanza.attr.type == "unsubscribe" then if rostermanager.process_inbound_unsubscribe(node, host, from_bare) then sessionmanager.send_to_interested_resources(node, host, stanza); rostermanager.roster_push(node, host, from_bare); end elseif stanza.attr.type == "subscribed" then if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then sessionmanager.send_to_interested_resources(node, host, stanza); rostermanager.roster_push(node, host, from_bare); end elseif stanza.attr.type == "unsubscribed" then if rostermanager.process_inbound_subscription_cancellation(node, host, from_bare) then sessionmanager.send_to_interested_resources(node, host, stanza); rostermanager.roster_push(node, host, from_bare); end else origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type")); end stanza.attr.from, stanza.attr.to = st_from, st_to; return true; end local outbound_presence_handler = function(data) -- outbound presence received local origin, stanza = data.origin, data.stanza; local to = stanza.attr.to; if to then local t = stanza.attr.type; if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes return handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); end local to_bare = jid_bare(to); local roster = origin.roster; if roster and not(roster[to_bare] and (roster[to_bare].subscription == "both" or roster[to_bare].subscription == "from")) then -- directed presence origin.directed = origin.directed or {}; if t then -- removing from directed presence list on sending an error or unavailable origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to? else origin.directed[to] = true; -- FIXME does it make more sense to add to_bare rather than to? end end end -- TODO maybe handle normal presence here, instead of letting it pass to incoming handlers? end module:hook("pre-presence/full", outbound_presence_handler); module:hook("pre-presence/bare", outbound_presence_handler); module:hook("pre-presence/host", outbound_presence_handler); module:hook("presence/bare", function(data) -- inbound presence to bare JID received local origin, stanza = data.origin, data.stanza; local to = stanza.attr.to; local t = stanza.attr.type; if to then if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); end local user = bare_sessions[to]; if user then for _, session in pairs(user.sessions) do if session.presence then -- only send to available resources session.send(stanza); end end end -- no resources not online, discard elseif not t or t == "unavailable" then handle_normal_presence(origin, stanza); else origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type")); end return true; end); module:hook("presence/full", function(data) -- inbound presence to full JID received local origin, stanza = data.origin, data.stanza; local t = stanza.attr.type; if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); end local session = full_sessions[stanza.attr.to]; if session then -- TODO fire post processing event session.send(stanza); end -- resource not online, discard return true; end); module:hook("presence/host", function(data) -- inbound presence to the host local stanza = data.stanza; local from_bare = jid_bare(stanza.attr.from); local t = stanza.attr.type; if t == "probe" then core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); elseif t == "subscribe" then core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" })); core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); end return true; end); module:hook("resource-unbind", function(event) local session, err = event.session, event.error; -- Send unavailable presence if session.presence then local pres = st.presence{ type = "unavailable" }; if err then pres:tag("status"):text("Disconnected: "..err):up(); end core_process_stanza(session, pres); elseif session.directed then local pres = st.presence{ type = "unavailable", from = session.full_jid }; if err then pres:tag("status"):text("Disconnected: "..err):up(); end for jid in pairs(session.directed) do pres.attr.to = jid; core_post_stanza(session, pres, true); end session.directed = nil; end end); module:hook("roster-item-removed", function (event) local username = event.username; local session = event.origin; local roster = event.roster or session and session.roster; local jid = event.jid; local item = event.item; local from_jid = session.full_jid or (username .. "@" .. module.host); local subscription = item and item.subscription or "none"; local ask = item and item.ask; local pending = roster and roster[false].pending[jid]; if subscription == "both" or subscription == "from" or pending then core_post_stanza(session, st.presence({type="unsubscribed", from=from_jid, to=jid})); end if subscription == "both" or subscription == "to" or ask then send_presence_of_available_resources(username, module.host, jid, session, st.presence({type="unavailable"})); core_post_stanza(session, st.presence({type="unsubscribe", from=from_jid, to=jid})); end end, -1); prosody-13.0.1/plugins/PaxHeaders/mod_private.lua0000644000000000000000000000011714773555365017075 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.627710677 prosody-13.0.1/plugins/mod_private.lua0000644000175000017500000000265714773555365021306 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "prosody.util.stanza" local private_storage = module:open_store("private", "map"); module:add_feature("jabber:iq:private"); module:hook("iq/self/jabber:iq:private:query", function(event) local origin, stanza = event.origin, event.stanza; local query = stanza.tags[1]; if #query.tags ~= 1 then origin.send(st.error_reply(stanza, "modify", "bad-format")); return true; end local tag = query.tags[1]; local key = tag.name..":"..tag.attr.xmlns; if stanza.attr.type == "get" then local data, err = private_storage:get(origin.username, key); if data then origin.send(st.reply(stanza):query("jabber:iq:private"):add_child(st.deserialize(data))); elseif err then origin.send(st.error_reply(stanza, "wait", "internal-server-error", err)); else origin.send(st.reply(stanza):add_child(query)); end return true; else -- stanza.attr.type == "set" local data; if #tag ~= 0 then data = st.preserialize(tag); end -- TODO delete datastore if empty local ok, err = private_storage:set(origin.username, key, data); if not ok then origin.send(st.error_reply(stanza, "wait", "internal-server-error", err)); return true; end origin.send(st.reply(stanza)); return true; end end); prosody-13.0.1/plugins/PaxHeaders/mod_proxy65.lua0000644000000000000000000000011714773555365016757 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.631710694 prosody-13.0.1/plugins/mod_proxy65.lua0000644000175000017500000001647714773555365021175 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2011 Matthew Wild -- Copyright (C) 2008-2011 Waqas Hussain -- Copyright (C) 2009 Thilo Cestonaro -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- module:set_global(); 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"); local max_buffer_size = 4096; local listener = {}; function listener.onincoming(conn, data) local session = sessions[conn] or {}; local transfer = transfers[session.sha]; if transfer and transfer.activated then -- copy data between initiator and target local initiator, target = transfer.initiator, transfer.target; (conn == initiator and target or initiator):write(data); return; end -- FIXME server.link should be doing this? if not session.greeting_done then local nmethods = data:byte(2) or 0; if data:byte(1) == 0x05 and nmethods > 0 and #data == 2 + nmethods then -- check if we have all the data if data:find("%z") then -- 0x00 = 'No authentication' is supported session.greeting_done = true; sessions[conn] = session; conn:write("\5\0"); -- send (SOCKS version 5, No authentication) module:log("debug", "SOCKS5 greeting complete"); return; end end -- else error, unexpected input conn:write("\5\255"); -- send (SOCKS version 5, no acceptable method) conn:close(); module:log("debug", "Invalid SOCKS5 greeting received: %q", data:sub(1, 300)); else -- connection request --local head = string.char( 0x05, 0x01, 0x00, 0x03, 40 ); -- ( VER=5=SOCKS5, CMD=1=CONNECT, RSV=0=RESERVED, ATYP=3=DOMAIMNAME, SHA-1 size ) if #data == 47 and data:sub(1,5) == "\5\1\0\3\40" and data:sub(-2) == "\0\0" then local sha = data:sub(6, 45); conn:pause(); conn:write("\5\0\0\3\40" .. sha .. "\0\0"); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte) if not transfers[sha] then transfers[sha] = {}; transfers[sha].target = conn; session.sha = sha; module:log("debug", "SOCKS5 target connected for session %s", sha); else -- transfers[sha].target ~= nil transfers[sha].initiator = conn; session.sha = sha; module:log("debug", "SOCKS5 initiator connected for session %s", sha); server.link(conn, transfers[sha].target, max_buffer_size); server.link(transfers[sha].target, conn, max_buffer_size); end else -- error, unexpected input conn:write("\5\1\0\3\0\0\0"); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte) conn:close(); module:log("debug", "Invalid SOCKS5 negotiation received: %q", data:sub(1, 300)); end end end function listener.ondisconnect(conn) local session = sessions[conn]; if session then if transfers[session.sha] then local initiator, target = transfers[session.sha].initiator, transfers[session.sha].target; if initiator == conn and target ~= nil then target:close(); elseif target == conn and initiator ~= nil then initiator:close(); end transfers[session.sha] = nil; end -- Clean up any session-related stuff here sessions[conn] = nil; end end function module.add_host(module) local host, name = module:get_host(), module:get_option_string("name", "SOCKS5 Bytestreams Service"); local proxy_address = module:get_option_string("proxy65_address", host); local proxy_acl = module:get_option_array("proxy65_acl"); local proxy_open_access = module:get_option_boolean("proxy65_open_access", false); -- COMPAT w/pre-0.9 where proxy65_port was specified in the components section of the config local legacy_config = module:get_option_number("proxy65_port"); if legacy_config then module:log("warn", "proxy65_port is deprecated, please put proxy65_ports = { %d } into the global section instead", legacy_config); end module:depends("disco"); module:add_identity("proxy", "bytestreams", name); module:add_feature("http://jabber.org/protocol/bytestreams"); module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event) local origin, stanza = event.origin, event.stanza; -- check ACL -- using 'while' instead of 'if' so we can break out of it local allow; if proxy_acl and #proxy_acl > 0 then local jid = stanza.attr.from; for _, acl in ipairs(proxy_acl) do if jid_compare(jid, acl) then allow = true; break; end end elseif proxy_open_access or origin.type == "c2s" then allow = true; end if not allow then module:log("warn", "Denying use of proxy for %s", stanza.attr.from); origin.send(st.error_reply(stanza, "auth", "forbidden")); return true; end local proxy_port = next(portmanager.get_active_services():search("proxy65", nil)[1] or {}); if not proxy_port then module:log("warn", "Not listening on any port"); origin.send(st.error_reply(stanza, "wait", "item-not-found", "Not listening on any port")); return true; end local sid = stanza.tags[1].attr.sid; origin.send(st.reply(stanza):tag("query", {xmlns="http://jabber.org/protocol/bytestreams", sid=sid}) :tag("streamhost", {jid=host, host=proxy_address, port=("%d"):format(proxy_port)})); return true; end); module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event) local origin, stanza = event.origin, event.stanza; local query = stanza.tags[1]; local sid = query.attr.sid; local from = stanza.attr.from; local to = query:get_child_text("activate"); local prepped_to = jid_prep(to); local info = "sid: "..tostring(sid)..", initiator: "..tostring(from)..", target: "..tostring(prepped_to or to); if prepped_to and sid then local sha = sha1(sid .. from .. prepped_to, true); if not transfers[sha] then module:log("debug", "Activation request has unknown session id; activation failed (%s)", info); origin.send(st.error_reply(stanza, "modify", "item-not-found")); elseif not transfers[sha].initiator then module:log("debug", "The sender was not connected to the proxy; activation failed (%s)", info); origin.send(st.error_reply(stanza, "cancel", "not-allowed", "The sender (you) is not connected to the proxy")); --elseif not transfers[sha].target then -- can't happen, as target is set when a transfer object is created -- module:log("debug", "The recipient was not connected to the proxy; activation failed (%s)", info); -- origin.send(st.error_reply(stanza, "cancel", "not-allowed", "The recipient is not connected to the proxy")); else -- if transfers[sha].initiator ~= nil and transfers[sha].target ~= nil then module:log("debug", "Transfer activated (%s)", info); transfers[sha].activated = true; transfers[sha].target:resume(); transfers[sha].initiator:resume(); origin.send(st.reply(stanza)); end elseif to and sid then module:log("debug", "Malformed activation jid; activation failed (%s)", info); origin.send(st.error_reply(stanza, "modify", "jid-malformed")); else module:log("debug", "Bad request; activation failed (%s)", info); origin.send(st.error_reply(stanza, "modify", "bad-request")); end return true; end); end module:provides("net", { default_port = 5000; listener = listener; multiplex = { pattern = "^\5"; }; }); prosody-13.0.1/plugins/PaxHeaders/mod_pubsub0000644000000000000000000000013014773555365016136 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.631710694 29 ctime=1743706869.63571071 prosody-13.0.1/plugins/mod_pubsub/0000755000175000017500000000000014773555365020417 5ustar00prosodyprosody00000000000000prosody-13.0.1/plugins/mod_pubsub/PaxHeaders/commands.lib.lua0000644000000000000000000000011714773555365021271 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.631710694 prosody-13.0.1/plugins/mod_pubsub/commands.lib.lua0000644000175000017500000001642614773555365023501 0ustar00prosodyprosody00000000000000local it = require "prosody.util.iterators"; local st = require "prosody.util.stanza"; local pubsub_lib = module:require("mod_pubsub/pubsub"); local function add_commands(get_service) 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; }); module:add_item("shell-command", { section = "pubsub"; section_desc = "Manage publish/subscribe nodes"; name = "list_items"; desc = "List items on a pubsub node"; 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 -- luacheck: ignore 431/service local service = get_service(service_jid); local items = select(2, assert(service:get_items(node_name, true))); local count = 0; for item_name in pairs(items) do count = count + 1; self.session.print(item_name); end return true, ("%d items"):format(count); end; }); module:add_item("shell-command", { section = "pubsub"; section_desc = "Manage publish/subscribe nodes"; name = "get_item"; desc = "Show item content on a pubsub node"; args = { { name = "service_jid", type = "string" }; { name = "node_name", type = "string" }; { name = "item_name", type = "string" }; }; host_selector = "service_jid"; handler = function (self, service_jid, node_name, item_name) --luacheck: ignore 212/self -- luacheck: ignore 431/service local service = get_service(service_jid); local items = select(2, assert(service:get_items(node_name, true))); if not items[item_name] then return false, "Item not found"; end self.session.print(items[item_name]); return true; end; }); module:add_item("shell-command", { section = "pubsub"; section_desc = "Manage publish/subscribe nodes"; name = "get_node_config"; desc = "Get the current configuration for a node"; args = { { name = "service_jid", type = "string" }; { name = "node_name", type = "string" }; { name = "option_name", type = "string" }; }; host_selector = "service_jid"; handler = function (self, service_jid, node_name, option_name) --luacheck: ignore 212/self -- luacheck: ignore 431/service local service = get_service(service_jid); local config = select(2, assert(service:get_node_config(node_name, true))); local config_form = pubsub_lib.node_config_form:form(config, "submit"); local count = 0; if option_name then count = 1; local field = config_form:get_child_with_attr("field", nil, "var", option_name); if not field then return false, "option not found"; end self.session.print(field:get_child_text("value")); else local opts = {}; for field in config_form:childtags("field") do opts[field.attr.var] = field:get_child_text("value"); end for k, v in it.sorted_pairs(opts) do count = count + 1; self.session.print(k, v); end end return true, ("Showing %d config options"):format(count); end; }); module:add_item("shell-command", { section = "pubsub"; section_desc = "Manage publish/subscribe nodes"; name = "set_node_config_option"; desc = "Set a config option on a pubsub node"; args = { { name = "service_jid", type = "string" }; { name = "node_name", type = "string" }; { name = "option_name", type = "string" }; { name = "option_value", type = "string" }; }; host_selector = "service_jid"; handler = function (self, service_jid, node_name, option_name, option_value) --luacheck: ignore 212/self -- luacheck: ignore 431/service local service = get_service(service_jid); local config = select(2, assert(service:get_node_config(node_name, true))); local new_config_form = st.stanza("x", { xmlns = "jabber:x:data" }) :tag("field", { var = option_name }) :text_tag("value", option_value) :up(); local new_config = pubsub_lib.node_config_form:data(new_config_form, config); assert(service:set_node_config(node_name, true, new_config)); local applied_config = select(2, assert(service:get_node_config(node_name, true))); local applied_config_form = pubsub_lib.node_config_form:form(applied_config, "submit"); local applied_field = applied_config_form:get_child_with_attr("field", nil, "var", option_name); if not applied_field then return false, "Unknown config field: "..option_name; end return true, "Applied config: "..applied_field:get_child_text("value"); end; }); module:add_item("shell-command", { section = "pubsub"; section_desc = "Manage publish/subscribe nodes"; name = "delete_item"; desc = "Delete a single item from a node"; args = { { name = "service_jid", type = "string" }; { name = "node_name", type = "string" }; { name = "item_name", type = "string" }; }; host_selector = "service_jid"; handler = function (self, service_jid, node_name, item_name) --luacheck: ignore 212/self -- luacheck: ignore 431/service local service = get_service(service_jid); return assert(service:retract(node_name, true, item_name)); end; }); module:add_item("shell-command", { section = "pubsub"; section_desc = "Manage publish/subscribe nodes"; name = "delete_all_items"; desc = "Delete all items from a node"; args = { { name = "service_jid", type = "string" }; { name = "node_name", type = "string" }; { name = "notify_subscribers", type = "string" }; }; host_selector = "service_jid"; handler = function (self, service_jid, node_name, notify_subscribers) --luacheck: ignore 212/self -- luacheck: ignore 431/service local service = get_service(service_jid); return assert(service:purge(node_name, true, notify_subscribers == "true")); end; }); module:add_item("shell-command", { section = "pubsub"; section_desc = "Manage publish/subscribe nodes"; name = "create_node"; desc = "Create a new node"; 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 -- luacheck: ignore 431/service local service = get_service(service_jid); return assert(service:create(node_name, true)); end; }); module:add_item("shell-command", { section = "pubsub"; section_desc = "Manage publish/subscribe nodes"; name = "delete_node"; desc = "Delete a node entirely"; 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 -- luacheck: ignore 431/service local service = get_service(service_jid); return assert(service:delete(node_name, true)); end; }); end return { add_commands = add_commands; } prosody-13.0.1/plugins/mod_pubsub/PaxHeaders/mod_pubsub.lua0000644000000000000000000000011714773555365021062 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.631710694 prosody-13.0.1/plugins/mod_pubsub/mod_pubsub.lua0000644000175000017500000002135414773555365023266 0ustar00prosodyprosody00000000000000local 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"; 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 service_expose_publisher = module:get_option_boolean("expose_publisher") local service; local lib_pubsub = module:require "pubsub"; module:depends("disco"); module:add_identity("pubsub", "service", pubsub_disco_name); module:add_feature("http://jabber.org/protocol/pubsub"); function handle_pubsub_iq(event) return lib_pubsub.handle_pubsub_iq(event, service); end -- An itemstore supports the following methods: -- items(): iterator over (id, item) -- get(id): return item with id -- set(id, item): set id to item -- clear(): clear all items -- resize(n): set new limit and trim oldest items -- tail(): return the latest item -- A nodestore supports the following methods: -- set(node_name, node_data) -- get(node_name) -- users(): iterator over (node_name) local max_max_items = module:get_option_integer("pubsub_max_items", 256, 1); local function tonumber_max_items(n) if n == "max" then return max_max_items; end return tonumber(n); end for _, field in ipairs(lib_pubsub.node_config_form) do if field.var == "pubsub#max_items" then field.range_max = max_max_items; break; end end local node_store = module:open_store(module.name.."_nodes"); local function create_simple_itemstore(node_config, node_name) --> util.cache like object local driver = storagemanager.get_driver(module.host, "pubsub_data"); local archive = driver:open("pubsub_"..node_name, "archive"); local max_items = tonumber_max_items(node_config["max_items"]); return lib_pubsub.archive_itemstore(archive, max_items, nil, node_name); end function simple_broadcast(kind, node, jids, item, actor, node_obj, service) --luacheck: ignore 431/service if node_obj then if node_obj.config["notify_"..kind] == false then return; end end if kind == "retract" then kind = "items"; -- XEP-0060 signals retraction in an container end if item then item = st.clone(item); item.attr.xmlns = nil; -- Clear the pubsub namespace if kind == "items" then if node_obj and node_obj.config.include_payload == false then item:maptags(function () return nil; end); end 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); end end end local id = new_id(); local msg_type = node_obj and node_obj.config.notification_type or "headline"; local message = st.message({ from = module.host, type = msg_type, id = id }) :tag("event", { xmlns = xmlns_pubsub_event }) :tag(kind, { node = node }); if item then message:add_child(item); end local summary; if item and item.tags[1] then local payload = item.tags[1]; local payload_type = node_obj and node_obj.config.payload_type or payload.attr.xmlns; summary = module:fire_event("pubsub-summary/"..payload_type, { kind = kind, node = node, jids = jids, actor = actor, item = item, payload = payload, }); end for jid, options in pairs(jids) do local new_stanza = st.clone(message); if summary and type(options) == "table" and options["pubsub#include_body"] then new_stanza:body(summary); end new_stanza.attr.to = jid; module:send(new_stanza); end end function check_node_config(node, actor, new_config) -- luacheck: ignore 212/node 212/actor if (tonumber_max_items(new_config["max_items"]) or 1) > max_max_items then return false; end if new_config["access_model"] ~= "whitelist" and new_config["access_model"] ~= "open" then return false; end return true; end function is_item_stanza(item) return st.is_stanza(item) and item.attr.xmlns == xmlns_pubsub and item.name == "item" and #item.tags == 1; end -- Compose a textual representation of Atom payloads local summary_templates = module:get_option("pubsub_summary_templates", { ["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 module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq); module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq); local function add_disco_features_from_service(service) --luacheck: ignore 431/service for feature in lib_pubsub.get_feature_set(service) do module:add_feature(xmlns_pubsub.."#"..feature); end end module:hook("host-disco-info-node", function (event) return lib_pubsub.handle_disco_info_node(event, service); end); module:hook("host-disco-items-node", function (event) return lib_pubsub.handle_disco_items_node(event, service); end); module:hook("host-disco-items", function (event) local stanza, reply = event.stanza, event.reply; local ok, ret = service:get_nodes(stanza.attr.from); if not ok then return; end for node in pairs(ret) do local ok, meta = service:get_node_metadata(node, stanza.attr.from); if ok then reply:tag("item", { jid = module.host, node = node, name = meta.title }):up(); end end end); 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 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:could(":service-admin", bare_jid) then return admin_aff; end end function get_service() return service; end 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 = service_expose_publisher; service.config.nodestore = node_store; service.config.itemstore = create_simple_itemstore; service.config.broadcaster = simple_broadcast; service.config.itemcheck = is_item_stanza; service.config.check_node_config = check_node_config; service.config.get_affiliation = get_affiliation; module.environment.service = service; add_disco_features_from_service(service); end function module.save() return { service = service }; end function module.restore(data) set_service(data.service); end function module.load() if module.reloading then return; end set_service(pubsub.new({ autocreate_on_publish = autocreate_on_publish; autocreate_on_subscribe = autocreate_on_subscribe; expose_publisher = service_expose_publisher; node_defaults = { ["persist_items"] = true; }; max_items = max_max_items; nodestore = node_store; itemstore = create_simple_itemstore; 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:require("commands").add_commands(get_service); prosody-13.0.1/plugins/mod_pubsub/PaxHeaders/pubsub.lib.lua0000644000000000000000000000011614773555365020767 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.63571071 prosody-13.0.1/plugins/mod_pubsub/pubsub.lib.lua0000644000175000017500000006560114773555365023177 0ustar00prosodyprosody00000000000000local time_now = os.time; 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"; local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; local _M = {}; local handlers = {}; _M.handlers = handlers; 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" }; ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" }; ["item-not-found"] = { "cancel", "item-not-found" }; ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" }; ["invalid-options"] = { "modify", "bad-request", nil, "invalid-options" }; ["forbidden"] = { "auth", "forbidden" }; ["not-allowed"] = { "cancel", "not-allowed" }; ["not-acceptable"] = { "modify", "not-acceptable" }; ["internal-server-error"] = { "wait", "internal-server-error" }; ["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, 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; local function dataform_error_message(err) -- ({ string : string }) -> string? local out = {}; for field, errmsg in pairs(err) do table.insert(out, ("%s: %s"):format(field, errmsg)) end return table.concat(out, "; "); end -- Note: If any config options are added that are of complex types, -- (not simply strings/numbers) then the publish-options code will -- need to be revisited local node_config_form = dataform { { type = "hidden"; var = "FORM_TYPE"; value = "http://jabber.org/protocol/pubsub#node_config"; }; { type = "text-single"; name = "title"; var = "pubsub#title"; label = "Title"; }; { type = "text-single"; name = "description"; var = "pubsub#description"; label = "Description"; }; { type = "text-single"; name = "payload_type"; var = "pubsub#type"; label = "The type of node data, usually specified by the namespace of the payload (if any)"; }; { type = "text-single"; datatype = "pubsub:integer-or-max"; name = "max_items"; range_min = 1; var = "pubsub#max_items"; label = "Max # of items to persist"; }; { type = "boolean"; name = "persist_items"; var = "pubsub#persist_items"; label = "Persist items to storage"; }; { type = "list-single"; name = "access_model"; var = "pubsub#access_model"; label = "Specify the subscriber model"; options = { "authorize", "open", "presence", "roster", "whitelist", }; }; { 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"; label = "Specify the publisher model"; options = { "publishers"; "subscribers"; "open"; }; }; { type = "list-single"; var = "pubsub#send_last_published_item"; name = "send_last_published_item"; options = { "never"; "on_sub"; "on_sub_and_presence" }; }; { type = "boolean"; value = true; label = "Whether to deliver event notifications"; name = "notify_items"; var = "pubsub#deliver_notifications"; }; { type = "boolean"; value = true; label = "Whether to deliver payloads with event notifications"; name = "include_payload"; var = "pubsub#deliver_payloads"; }; { type = "list-single"; name = "notification_type"; var = "pubsub#notification_type"; label = "Specify the delivery style for notifications"; options = { { label = "Messages of type normal", value = "normal" }, { label = "Messages of type headline", value = "headline", default = true }, }; }; { type = "boolean"; label = "Whether to notify subscribers when the node is deleted"; name = "notify_delete"; var = "pubsub#notify_delete"; value = true; }; { type = "boolean"; label = "Whether to notify subscribers when items are removed from the node"; name = "notify_retract"; 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; local subscribe_options_form = dataform { { type = "hidden"; var = "FORM_TYPE"; value = "http://jabber.org/protocol/pubsub#subscribe_options"; }; { type = "boolean"; name = "pubsub#include_body"; label = "Receive message body in addition to payload?"; }; }; _M.subscribe_options_form = subscribe_options_form; local node_metadata_form = dataform { { type = "hidden"; var = "FORM_TYPE"; value = "http://jabber.org/protocol/pubsub#meta-data"; }; { type = "text-single"; name = "title"; var = "pubsub#title"; }; { type = "text-single"; name = "description"; var = "pubsub#description"; }; { type = "text-single"; name = "payload_type"; var = "pubsub#type"; }; { type = "text-single"; name = "access_model"; var = "pubsub#access_model"; }; { type = "text-single"; name = "publish_model"; var = "pubsub#publish_model"; }; }; _M.node_metadata_form = node_metadata_form; local service_method_feature_map = { add_subscription = { "subscribe", "subscription-options" }; create = { "create-nodes", "instant-nodes", "item-ids", "create-and-configure" }; delete = { "delete-nodes" }; get_items = { "retrieve-items" }; get_subscriptions = { "retrieve-subscriptions" }; node_defaults = { "retrieve-default" }; publish = { "publish", "multi-items", "publish-options" }; purge = { "purge-nodes" }; retract = { "delete-items", "retract-items" }; set_node_config = { "config-node", "meta-data" }; set_affiliation = { "modify-affiliations" }; }; local service_config_feature_map = { autocreate_on_publish = { "auto-create" }; }; function _M.get_feature_set(service) local supported_features = set.new(); for method, features in pairs(service_method_feature_map) do if service[method] then for _, feature in ipairs(features) do if feature then supported_features:add(feature); end end end end for option, features in pairs(service_config_feature_map) do if service.config[option] then for _, feature in ipairs(features) do if feature then supported_features:add(feature); end end end end for affiliation in pairs(service.config.capabilities) do if affiliation ~= "none" and affiliation ~= "owner" then supported_features:add(affiliation.."-affiliation"); end end if service.node_defaults.access_model then supported_features:add("access-"..service.node_defaults.access_model); end if service.node_defaults.send_last_published_item ~= "never" then supported_features:add("last-published"); end if rawget(service.config, "itemstore") and rawget(service.config, "nodestore") then supported_features:add("persistent-items"); end if true --[[ node_metadata_form[max_items].datatype == "pubsub:integer-or-max" ]] then supported_features:add("config-node-max"); end return supported_features; end function _M.handle_disco_info_node(event, service) local stanza, reply, node = event.stanza, event.reply, event.node; local ok, meta = service:get_node_metadata(node, stanza.attr.from); if not ok then event.origin.send(pubsub_error_reply(stanza, meta)); return true; end event.exists = true; reply:tag("identity", { category = "pubsub", type = "leaf" }):up(); reply:add_child(node_metadata_form:form(meta, "result")); end function _M.handle_disco_items_node(event, service) local stanza, reply, node = event.stanza, event.reply, event.node; local ok, ret = service:get_items(node, stanza.attr.from); if not ok then event.origin.send(pubsub_error_reply(stanza, ret)); return true; end for _, id in ipairs(ret) do reply:tag("item", { jid = service.config.jid or module.host, name = id }):up(); end event.exists = true; end function _M.handle_pubsub_iq(event, service) local origin, stanza = event.origin, event.stanza; local pubsub_tag = stanza.tags[1]; local action = pubsub_tag.tags[1]; if not action then return origin.send(st.error_reply(stanza, "cancel", "bad-request")); end local prefix = ""; if pubsub_tag.attr.xmlns == xmlns_pubsub_owner then prefix = "owner_"; end local handler = handlers[prefix..stanza.attr.type.."_"..action.name]; if handler then handler(origin, stanza, action, service); return true; end end function handlers.get_items(origin, stanza, items, service) local node = items.attr.node; local requested_items = {}; for item in items:childtags("item") do table.insert(requested_items, item.attr.id); end if requested_items[1] == nil then requested_items = nil; end if not node then 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) }; end local ok, results = service:get_items(node, stanza.attr.from, requested_items, resultspec); if not ok then origin.send(pubsub_error_reply(stanza, results)); return true; 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); if not requested_items then -- XXX Hack to preserve order of explicitly requested items. iter, v, i = it.reverse(iter, v, i); end for _, id in iter, v, i do local item = results[id]; if not expose_publisher then item = st.clone(item); item.attr.publisher = nil; end data:add_child(item); end local reply = st.reply(stanza) :tag("pubsub", { xmlns = xmlns_pubsub }) :add_child(data); origin.send(reply); return true; end function handlers.get_subscriptions(origin, stanza, subscriptions, service) local node = subscriptions.attr.node; local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from); if not ok then origin.send(pubsub_error_reply(stanza, ret)); return true; end local reply = st.reply(stanza) :tag("pubsub", { xmlns = xmlns_pubsub }) :tag("subscriptions"); for _, sub in ipairs(ret) do reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); end origin.send(reply); return true; end function handlers.owner_get_subscriptions(origin, stanza, subscriptions, service) local node = subscriptions.attr.node; local ok, ret = service:get_subscriptions(node, stanza.attr.from); if not ok then origin.send(pubsub_error_reply(stanza, ret)); return true; end local reply = st.reply(stanza) :tag("pubsub", { xmlns = xmlns_pubsub_owner }) :tag("subscriptions"); for _, sub in ipairs(ret) do reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); end origin.send(reply); return true; end function handlers.owner_set_subscriptions(origin, stanza, subscriptions, service) local node = subscriptions.attr.node; if not node then origin.send(pubsub_error_reply(stanza, "nodeid-required")); return true; end if not service:may(node, stanza.attr.from, "subscribe_other") then origin.send(pubsub_error_reply(stanza, "forbidden")); 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 for subscription_tag in subscriptions:childtags("subscription") do if subscription_tag.attr.subscription == 'subscribed' then local ok, err = service:add_subscription(node, stanza.attr.from, subscription_tag.attr.jid); if not ok then origin.send(pubsub_error_reply(stanza, err)); return true; end elseif subscription_tag.attr.subscription == 'none' then local ok, err = service:remove_subscription(node, stanza.attr.from, subscription_tag.attr.jid); if not ok then origin.send(pubsub_error_reply(stanza, err)); return true; end end end local reply = st.reply(stanza); origin.send(reply); return true; end function handlers.set_create(origin, stanza, create, service) local node = create.attr.node; local ok, ret, reply; local config; local configure = stanza.tags[1]:get_child("configure"); if configure then local config_form = configure:get_child("x", "jabber:x:data"); if not config_form then origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform")); return true; end local form_data, err = node_config_form:data(config_form); if err then origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err))); return true; end config = form_data; end if node then ok, ret = service:create(node, stanza.attr.from, config); if ok then reply = st.reply(stanza); else reply = pubsub_error_reply(stanza, ret); end else repeat node = uuid_generate(); ok, ret = service:create(node, stanza.attr.from, config); until ok or ret ~= "conflict"; if ok then reply = st.reply(stanza) :tag("pubsub", { xmlns = xmlns_pubsub }) :tag("create", { node = node }); else reply = pubsub_error_reply(stanza, ret); end end origin.send(reply); return true; end function handlers.owner_set_delete(origin, stanza, delete, service) local node = delete.attr.node; local reply; if not node then origin.send(pubsub_error_reply(stanza, "nodeid-required")); return true; end local ok, ret = service:delete(node, stanza.attr.from); if ok then reply = st.reply(stanza); else reply = pubsub_error_reply(stanza, ret); end origin.send(reply); return true; end function handlers.set_subscribe(origin, stanza, subscribe, service) local node, jid = subscribe.attr.node, subscribe.attr.jid; jid = jid_prep(jid); if not (node and jid) then origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); return true; end local options_tag, options = stanza.tags[1]:get_child("options"), nil; if options_tag then -- FIXME form parsing errors ignored here, why? local err options, err = subscribe_options_form:data(options_tag.tags[1]); if err then origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err))); return true end end local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options); local reply; if ok then reply = st.reply(stanza) :tag("pubsub", { xmlns = xmlns_pubsub }) :tag("subscription", { node = node, jid = jid, subscription = "subscribed" }):up(); if options_tag then reply:add_child(options_tag); end else reply = pubsub_error_reply(stanza, ret); end origin.send(reply); local ok, config = service:get_node_config(node, true); if ok and config.send_last_published_item ~= "never" then 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); end end function handlers.set_unsubscribe(origin, stanza, unsubscribe, service) local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid; jid = jid_prep(jid); if not (node and jid) then origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); return true; end local ok, ret = service:remove_subscription(node, stanza.attr.from, jid); local reply; if ok then reply = st.reply(stanza) :tag("pubsub", { xmlns = xmlns_pubsub }) :tag("subscription", { node = node, jid = jid, subscription = "none" }):up(); else reply = pubsub_error_reply(stanza, ret); end origin.send(reply); return true; end function handlers.get_options(origin, stanza, options, service) local node, jid = options.attr.node, options.attr.jid; jid = jid_prep(jid); if not (node and jid) then origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); return true; end local ok, ret = service:get_subscription(node, stanza.attr.from, jid); if not ok then origin.send(pubsub_error_reply(stanza, "not-subscribed")); return true; end if ret == true then ret = {} end origin.send(st.reply(stanza) :tag("pubsub", { xmlns = xmlns_pubsub }) :tag("options", { node = node, jid = jid }) :add_child(subscribe_options_form:form(ret))); return true; end function handlers.set_options(origin, stanza, options, service) local node, jid = options.attr.node, options.attr.jid; jid = jid_prep(jid); if not (node and jid) then origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); return true; end local ok, ret = service:get_subscription(node, stanza.attr.from, jid); if not ok then origin.send(pubsub_error_reply(stanza, ret)); return true; elseif not ret then origin.send(pubsub_error_reply(stanza, "not-subscribed")); return true; end local old_subopts = ret; local new_subopts, err = subscribe_options_form:data(options.tags[1], old_subopts); if err then origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err))); return true; end local ok, err = service:add_subscription(node, stanza.attr.from, jid, new_subopts); if not ok then origin.send(pubsub_error_reply(stanza, err)); return true; end origin.send(st.reply(stanza)); return true; end function handlers.set_publish(origin, stanza, publish, service) local node = publish.attr.node; if not node then origin.send(pubsub_error_reply(stanza, "nodeid-required")); return true; end local required_config = nil; local publish_options = stanza.tags[1]:get_child("publish-options"); if publish_options then -- Ensure that the node configuration matches the values in publish-options local publish_options_form = publish_options:get_child("x", "jabber:x:data"); local err; required_config, err = node_config_form:data(publish_options_form); if err then origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err))); return true end end local item = publish:get_child("item"); local id = (item and item.attr.id); if not id then id = uuid_generate(); if item then item.attr.id = id; end end if item then item.attr.publisher = service.config.normalize_jid(stanza.attr.from); end local ok, ret, context = service:publish(node, stanza.attr.from, id, item, required_config); local reply; if ok then if type(ok) == "string" then id = ok; end reply = st.reply(stanza) :tag("pubsub", { xmlns = xmlns_pubsub }) :tag("publish", { node = node }) :tag("item", { id = id }); else reply = pubsub_error_reply(stanza, ret, context); end origin.send(reply); return true; 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 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; end local reply, notifier; if notify then notifier = st.stanza("retract", { id = id }); end local ok, ret = service:retract(node, stanza.attr.from, id, notifier); if ok then reply = st.reply(stanza); else reply = pubsub_error_reply(stanza, ret); end origin.send(reply); return true; end function handlers.owner_set_purge(origin, stanza, purge, service) local node = purge.attr.node; local reply; if not node then origin.send(pubsub_error_reply(stanza, "nodeid-required")); return true; end local ok, ret = service:purge(node, stanza.attr.from, true); if ok then reply = st.reply(stanza); else reply = pubsub_error_reply(stanza, ret); end origin.send(reply); return true; end function handlers.owner_get_configure(origin, stanza, config, service) local node = config.attr.node; if not node then origin.send(pubsub_error_reply(stanza, "nodeid-required")); return true; end local ok, node_config = service:get_node_config(node, stanza.attr.from); if not ok then origin.send(pubsub_error_reply(stanza, node_config)); return true; end local reply = st.reply(stanza) :tag("pubsub", { xmlns = xmlns_pubsub_owner }) :tag("configure", { node = node }) :add_child(node_config_form:form(node_config)); origin.send(reply); return true; end function handlers.owner_set_configure(origin, stanza, config, service) local node = config.attr.node; if not node then origin.send(pubsub_error_reply(stanza, "nodeid-required")); return true; end if not service:may(node, stanza.attr.from, "configure") then origin.send(pubsub_error_reply(stanza, "forbidden")); return true; end local config_form = config:get_child("x", "jabber:x:data"); if not config_form then origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform")); return true; end local ok, old_config = service:get_node_config(node, stanza.attr.from); if not ok then origin.send(pubsub_error_reply(stanza, old_config)); return true; end local new_config, err = node_config_form:data(config_form, old_config); if err then origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(err))); return true; end local ok, err = service:set_node_config(node, stanza.attr.from, new_config); if not ok then origin.send(pubsub_error_reply(stanza, err)); return true; end origin.send(st.reply(stanza)); return true; end function handlers.owner_get_default(origin, stanza, default, service) -- luacheck: ignore 212/default local reply = st.reply(stanza) :tag("pubsub", { xmlns = xmlns_pubsub_owner }) :tag("default") :add_child(node_config_form:form(service.node_defaults)); origin.send(reply); return true; end function handlers.owner_get_affiliations(origin, stanza, affiliations, service) local node = affiliations.attr.node; if not node then origin.send(pubsub_error_reply(stanza, "nodeid-required")); return true; end if not service:may(node, stanza.attr.from, "set_affiliation") then origin.send(pubsub_error_reply(stanza, "forbidden")); 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 reply = st.reply(stanza) :tag("pubsub", { xmlns = xmlns_pubsub_owner }) :tag("affiliations", { node = node }); for jid, affiliation in pairs(node_obj.affiliations) do reply:tag("affiliation", { jid = jid, affiliation = affiliation }):up(); end origin.send(reply); return true; end function handlers.owner_set_affiliations(origin, stanza, affiliations, service) local node = affiliations.attr.node; if not node then origin.send(pubsub_error_reply(stanza, "nodeid-required")); return true; end if not service:may(node, stanza.attr.from, "set_affiliation") then origin.send(pubsub_error_reply(stanza, "forbidden")); 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 for affiliation_tag in affiliations:childtags("affiliation") do local jid = affiliation_tag.attr.jid; local affiliation = affiliation_tag.attr.affiliation; jid = jid_prep(jid); if affiliation == "none" then affiliation = nil; end local ok, err = service:set_affiliation(node, stanza.attr.from, jid, affiliation); if not ok then -- FIXME Incomplete error handling, -- see XEP 60 8.9.2.4 Multiple Simultaneous Modifications origin.send(pubsub_error_reply(stanza, err)); return true; end end local reply = st.reply(stanza); origin.send(reply); return true; end local function create_encapsulating_item(id, payload, publisher) local item = st.stanza("item", { id = id, publisher = publisher, xmlns = xmlns_pubsub }); item:add_child(payload); return item; end local function archive_itemstore(archive, max_items, user, node) module:log("debug", "Creation of archive itemstore for node %s with limit %d", node, max_items); local get_set = {}; function get_set:items() -- luacheck: ignore 212/self local data, err = archive:find(user, { limit = tonumber(max_items); reverse = true; }); if not data then module:log("error", "Unable to get items: %s", err); return true; end module:log("debug", "Listed items %s", data); return function() -- luacheck: ignore 211/when local id, payload, when, publisher = data(); if id == nil then return; end local item = create_encapsulating_item(id, payload, publisher); return id, item; end; end function get_set:get(key) -- luacheck: ignore 212/self local data, err = archive:find(user, { key = key; -- Get the last item with that key, if the archive doesn't deduplicate reverse = true, limit = 1; }); if not data then module:log("error", "Unable to get item: %s", err); return nil, err; end local id, payload, when, publisher = data(); module:log("debug", "Get item %s (published at %s by %s)", id, when, publisher); if id == nil then return nil; end return create_encapsulating_item(id, payload, publisher); end function get_set:set(key, value) -- luacheck: ignore 212/self local data, err; if value ~= nil then local publisher = value.attr.publisher; local payload = value.tags[1]; data, err = archive:append(user, key, payload, time_now(), publisher); else data, err = archive:delete(user, { key = key; }); end -- TODO archive support for maintaining maximum items archive:delete(user, { truncate = max_items; }); if not data then module:log("error", "Unable to set item: %s", err); return nil, err; end return data; end function get_set:clear() -- luacheck: ignore 212/self return archive:delete(user); end function get_set:resize(size) -- luacheck: ignore 212/self max_items = size; return archive:delete(user, { truncate = size; }); end function get_set:head() -- This should conveniently return the most recent item local item = self:get(nil); if item then return item.attr.id, item; end end return get_set; end _M.archive_itemstore = archive_itemstore; return _M; prosody-13.0.1/plugins/PaxHeaders/mod_register.lua0000644000000000000000000000011614773555365017246 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.63571071 prosody-13.0.1/plugins/mod_register.lua0000644000175000017500000000067614773555365021457 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local allow_registration = module:get_option_boolean("allow_registration", false); if allow_registration then module:depends("register_ibr"); module:depends("watchregistrations"); end module:depends("user_account_management"); prosody-13.0.1/plugins/PaxHeaders/mod_register_ibr.lua0000644000000000000000000000011614773555365020102 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.63571071 prosody-13.0.1/plugins/mod_register_ibr.lua0000644000175000017500000002037714773555365022313 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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); local account_details = module:open_store("account_details"); local field_map = { FORM_TYPE = { name = "FORM_TYPE", type = "hidden", value = "jabber:iq:register" }; username = { name = "username", type = "text-single", label = "Username", required = true }; password = { name = "password", type = "text-private", label = "Password", required = true }; nick = { name = "nick", type = "text-single", label = "Nickname" }; name = { name = "name", type = "text-single", label = "Full Name" }; first = { name = "first", type = "text-single", label = "Given Name" }; last = { name = "last", type = "text-single", label = "Family Name" }; email = { name = "email", type = "text-single", label = "Email" }; address = { name = "address", type = "text-single", label = "Street" }; city = { name = "city", type = "text-single", label = "City" }; state = { name = "state", type = "text-single", label = "State" }; zip = { name = "zip", type = "text-single", label = "Postal code" }; phone = { name = "phone", type = "text-single", label = "Telephone number" }; url = { name = "url", type = "text-single", label = "Webpage" }; date = { name = "date", type = "text-single", label = "Birth date" }; }; local title = module:get_option_string("registration_title", "Creating a new account"); local instructions = module:get_option_string("registration_instructions", "Choose a username and password for use with this service."); local registration_form = dataform_new{ title = title; instructions = instructions; field_map.FORM_TYPE; field_map.username; field_map.password; }; local registration_query = st.stanza("query", {xmlns = "jabber:iq:register"}) :tag("instructions"):text(instructions):up() :tag("username"):up() :tag("password"):up(); for _, field in ipairs(additional_fields) do if type(field) == "table" then registration_form[#registration_form + 1] = field; elseif field_map[field] or field_map[field:sub(1, -2)] then if field:match("%+$") then field = field:sub(1, -2); field_map[field].required = true; end registration_form[#registration_form + 1] = field_map[field]; registration_query:tag(field):up(); else module:log("error", "Unknown field %q", field); end end registration_query:add_child(registration_form:form()); local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up(); module:hook("stream-features", function(event) local session, features = event.origin, event.features; -- Advertise registration to unauthorized clients only. if session.type ~= "c2s_unauthed" or (require_encryption and not session.secure) then return end features:add_child(register_stream_feature); end); local function parse_response(query) local form = query:get_child("x", "jabber:x:data"); if form then return registration_form:data(form); else local data = {}; local errors = {}; for _, field in ipairs(registration_form) do local name, required = field.name, field.required; if field_map[name] then data[name] = query:get_child_text(name); if (not data[name] or #data[name] == 0) and required then errors[name] = "Required value missing"; end end end if next(errors) then return data, errors; end return data; end end -- In-band registration module:hook("stanza/iq/jabber:iq:register:query", function(event) local session, stanza = event.origin, event.stanza; local log = session.log or module._log; if session.type ~= "c2s_unauthed" then log("debug", "Attempted registration when disabled or already authenticated"); session.send(st.error_reply(stanza, "cancel", "service-unavailable")); return true; end if require_encryption and not session.secure then session.send(st.error_reply(stanza, "modify", "policy-violation", "Encryption is required")); return true; end local query = stanza.tags[1]; if stanza.attr.type == "get" then local reply = st.reply(stanza); reply:add_child(registration_query); session.send(reply); return true; end -- stanza.attr.type == "set" if query.tags[1] and query.tags[1].name == "remove" then session.send(st.error_reply(stanza, "auth", "registration-required")); return true; end local data, errors = parse_response(query); if errors then log("debug", "Error parsing registration form:"); local textual_errors = {}; for field, err in pairs(errors) do log("debug", "Field %q: %s", field, err); table.insert(textual_errors, ("%s: %s"):format(field:gsub("^%a", string.upper), err)); end session.send(st.error_reply(stanza, "modify", "not-acceptable", table.concat(textual_errors, "\n"))); return true; end local username, password = nodeprep(data.username, true), data.password; data.username, data.password = nil, nil; local host = module.host; if not username or username == "" then log("debug", "The requested username is invalid."); session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is invalid.")); return true; end 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; local err = user.error; if err then error_type, error_condition, reason = err.type, err.condition, err.text; else -- COMPAT pre-util.error error_type, error_condition, reason = user.error_type, user.error_condition, user.reason; end log("debug", "Registration disallowed by module: %s", reason or "no reason given"); session.send(st.error_reply(stanza, error_type or "modify", error_condition or "not-acceptable", reason)); return true; end if usermanager_user_exists(username, host) then if user.allow_reset == username then local ok, err = util_error.coerce(usermanager_set_password(username, password, host)); if ok then module:fire_event("user-password-reset", user); session.send(st.reply(stanza)); -- reset ok! else session.log("error", "Unable to reset password for %s@%s: %s", username, host, err); session.send(st.error_reply(stanza, err.type, err.condition, err.text)); end return true; else log("debug", "Attempt to register with existing username"); session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists.")); return true; end end 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 log("debug", "Could not store extra details"); usermanager_delete_user(username, host); session.send(st.error_reply(stanza, "wait", "internal-server-error", "Failed to write data to disk.")); return true; end session.send(st.reply(stanza)); -- user created! log("info", "User account created: %s@%s", username, host); module:fire_event("user-registered", { username = username, host = host, source = "mod_register", session = session }); else log("debug", "Could not create user", err); session.send(st.error_reply(stanza, "cancel", "feature-not-implemented", err)); end return true; end); prosody-13.0.1/plugins/PaxHeaders/mod_register_limits.lua0000644000000000000000000000011614773555365020627 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.63571071 prosody-13.0.1/plugins/mod_register_limits.lua0000644000175000017500000000736414773555365023041 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 "prosody.util.error"; -- COMPAT drop old option names 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, 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)); local throttle_cache = new_cache(throttle_cache_size, blocklist_overflow and function (ip, throttle) if not throttle:peek() then module:log("info", "Adding ip %s to registration blocklist", ip); blocklisted_ips[ip] = true; end end or nil); local function check_throttle(ip) if not throttle_max then return true end local throttle = throttle_cache:get(ip); if not throttle then throttle = create_throttle(throttle_max, throttle_period); end throttle_cache:set(ip, throttle); return throttle:poll(1); end local function ip_in_set(set, ip) if set[ip] then return true; end ip = new_ip(ip); for in_set in pairs(set) do if match_ip(ip, parse_cidr(in_set)) then return true; end end return false; end local err_registry = { blocklisted = { text = "Your IP address is blocklisted"; type = "auth"; condition = "forbidden"; }; not_allowlisted = { text = "Your IP address is not allowlisted"; type = "auth"; condition = "forbidden"; }; throttled = { text = "Too many registrations from this IP address recently"; type = "wait"; condition = "policy-violation"; }; } module:hook("user-registering", function (event) local session = event.session; local ip = event.ip or session and session.ip; local log = session and session.log or module._log; if not ip then log("warn", "IP not known; can't apply blocklist/allowlist"); elseif ip_in_set(blocklisted_ips, ip) then log("debug", "Registration disallowed by blocklist"); event.allowed = false; event.error = errors.new("blocklisted", event, err_registry); elseif (allowlist_only and not ip_in_set(allowlisted_ips, ip)) then log("debug", "Registration disallowed by allowlist"); event.allowed = false; event.error = errors.new("not_allowlisted", event, err_registry); elseif throttle_max and not ip_in_set(allowlisted_ips, ip) then if not check_throttle(ip) then log("debug", "Registrations over limit for ip %s", ip or "?"); event.allowed = false; event.error = errors.new("throttled", event, err_registry); end end if event.error then -- COMPAT pre-util.error event.reason = event.error.text; event.error_type = event.error.type; event.error_condition = event.error.condition; end end); prosody-13.0.1/plugins/PaxHeaders/mod_roster.lua0000644000000000000000000000011614773555365016740 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.63571071 prosody-13.0.1/plugins/mod_roster.lua0000644000175000017500000002427614773555365021153 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "prosody.util.stanza" 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 rostermanager = require "prosody.core.rostermanager"; local rm_load_roster = rostermanager.load_roster; local rm_remove_from_roster = rostermanager.remove_from_roster; local rm_add_to_roster = rostermanager.add_to_roster; local rm_roster_push = rostermanager.roster_push; module:add_feature("jabber:iq:roster"); local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"}); module:hook("stream-features", function(event) local origin, features = event.origin, event.features; if origin.username then features:add_child(rosterver_stream_feature); end end); module:hook("iq/self/jabber:iq:roster:query", function(event) local session, stanza = event.origin, event.stanza; if stanza.attr.type == "get" then local roster = st.reply(stanza); local client_ver = tonumber(stanza.tags[1].attr.ver); local server_ver = tonumber(session.roster[false].version or 1); if not (client_ver and server_ver) or client_ver ~= server_ver then roster:query("jabber:iq:roster"); -- Client does not support versioning, or has stale roster for jid, item in pairs(session.roster) do if jid then roster:tag("item", { jid = jid, subscription = item.subscription, ask = item.ask, name = item.name, }); for group in pairs(item.groups) do roster:text_tag("group", group); end roster:up(); -- move out from item end end roster.tags[1].attr.ver = tostring(server_ver); end session.send(roster); session.interested = true; -- resource is interested in roster updates else -- stanza.attr.type == "set" local query = stanza.tags[1]; if #query.tags == 1 and query.tags[1].name == "item" and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid then local item = query.tags[1]; local from_node, from_host = jid_split(stanza.attr.from); local jid = jid_prep(item.attr.jid); if jid and not jid_resource(jid) then if jid ~= from_node.."@"..from_host then if item.attr.subscription == "remove" then local roster = session.roster; local r_item = roster[jid]; if r_item then module:fire_event("roster-item-removed", { username = from_node, jid = jid, item = r_item, origin = session, roster = roster, }); local success, err_type, err_cond, err_msg = rm_remove_from_roster(session, jid); if success then session.send(st.reply(stanza)); rm_roster_push(from_node, from_host, jid); else session.send(st.error_reply(stanza, err_type, err_cond, err_msg)); end else session.send(st.error_reply(stanza, "modify", "item-not-found")); end else local r_item = {name = item.attr.name, groups = {}}; if r_item.name == "" then r_item.name = nil; end if session.roster[jid] then r_item.subscription = session.roster[jid].subscription; r_item.ask = session.roster[jid].ask; else r_item.subscription = "none"; end for group in item:childtags("group") do local text = group:get_text(); if text then r_item.groups[text] = true; end end local success, err_type, err_cond, err_msg = rm_add_to_roster(session, jid, r_item); if success then -- Ok, send success session.send(st.reply(stanza)); -- and push change to all resources rm_roster_push(from_node, from_host, jid); else -- Adding to roster failed session.send(st.error_reply(stanza, err_type, err_cond, err_msg)); end end else -- Trying to add self to roster session.send(st.error_reply(stanza, "cancel", "not-allowed")); end else -- Invalid JID added to roster session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error? end else -- Roster set didn't include a single item, or its name wasn't 'item' session.send(st.error_reply(stanza, "modify", "bad-request")); end end return true; end); module:hook_global("user-deleted", function(event) local username, host = event.username, event.host; local origin = event.origin or prosody.hosts[host]; if host ~= module.host then return end local roster = rm_load_roster(username, host); for jid, item in pairs(roster) do if jid then module:fire_event("roster-item-removed", { username = username, jid = jid, item = item, roster = roster, origin = origin, }); else for pending_jid in pairs(item.pending) do module:fire_event("roster-item-removed", { username = username, jid = pending_jid, roster = roster, origin = origin, }); end end end end, 300); -- API/commands -- Make a *one-way* subscription. User will see when contact is online, -- contact will not see when user is online. function subscribe(user_jid, contact_jid) local user_username, user_host = jid_split(user_jid); local contact_username, contact_host = jid_split(contact_jid); -- Update user's roster to say subscription request is pending. Bare hosts (e.g. components) don't have rosters. if user_username ~= nil then rostermanager.set_contact_pending_out(user_username, user_host, contact_jid); end if prosody.hosts[contact_host] and prosody.hosts[contact_host].type == "local" then -- Sending to a local host? -- Update contact's roster to say subscription request is pending... rostermanager.set_contact_pending_in(contact_username, contact_host, user_jid); -- Update contact's roster to say subscription request approved... rostermanager.subscribed(contact_username, contact_host, user_jid); -- Update user's roster to say subscription request approved. Bare hosts (e.g. components) don't have rosters. if user_username ~= nil then rostermanager.process_inbound_subscription_approval(user_username, user_host, contact_jid); end else -- Send a subscription request local sub_request = st.presence({ from = user_jid, to = contact_jid, type = "subscribe" }); module:send(sub_request); end return true; end -- Make a mutual subscription between jid1 and jid2. Each JID will see -- when the other one is online. function subscribe_both(jid1, jid2) local ok1, err1 = subscribe(jid1, jid2); local ok2, err2 = subscribe(jid2, jid1); return ok1 and ok2, err1 or err2; end -- Unsubscribes user from contact (not contact from user, if subscribed). function unsubscribe(user_jid, contact_jid) local user_username, user_host = jid_split(user_jid); local contact_username, contact_host = jid_split(contact_jid); -- Update user's roster to say subscription is cancelled... rostermanager.unsubscribe(user_username, user_host, contact_jid); if prosody.hosts[contact_host] then -- Local host? -- Update contact's roster to say subscription is cancelled... rostermanager.unsubscribed(contact_username, contact_host, user_jid); end return true; end -- Cancel any subscription in either direction. function unsubscribe_both(jid1, jid2) local ok1 = unsubscribe(jid1, jid2); local ok2 = unsubscribe(jid2, jid1); return ok1 and ok2; end module:add_item("shell-command", { section = "roster"; section_desc = "View and manage user rosters (contact lists)"; name = "show"; desc = "Show a user's current roster"; args = { { name = "jid", type = "string" }; { name = "sub", type = "string" }; }; host_selector = "jid"; handler = function(self, jid, sub) --luacheck: ignore 212/self local print = self.session.print; local it = require "prosody.util.iterators"; local roster = assert(rm_load_roster(jid_split(jid))); local function sort_func(a, b) if type(a) == "string" and type(b) == "string" then return a < b; else return a == false; end end local count = 0; if sub == "pending" then local pending_subs = roster[false].pending or {}; for pending_jid in it.sorted_pairs(pending_subs) do print(pending_jid); end else for contact, item in it.sorted_pairs(roster, sort_func) do if contact and (not sub or sub == item.subscription) then count = count + 1; print(contact, ("sub=%s\task=%s"):format(item.subscription or "none", item.ask or "none")); end end end return true, ("Showing %d entries"):format(count); end; }); module:add_item("shell-command", { section = "roster"; section_desc = "View and manage user rosters (contact lists)"; name = "subscribe"; desc = "Subscribe a user to another JID"; args = { { name = "jid", type = "string" }; { name = "contact", type = "string" }; }; host_selector = "jid"; handler = function(self, jid, contact) --luacheck: ignore 212/self return subscribe(jid, contact); end; }); module:add_item("shell-command", { section = "roster"; section_desc = "View and manage user rosters (contact lists)"; name = "subscribe_both"; desc = "Subscribe a user and a contact JID to each other"; args = { { name = "jid", type = "string" }; { name = "contact", type = "string" }; }; host_selector = "jid"; handler = function(self, jid, contact) --luacheck: ignore 212/self return subscribe_both(jid, contact); end; }); module:add_item("shell-command", { section = "roster"; section_desc = "View and manage user rosters (contact lists)"; name = "unsubscribe"; desc = "Unsubscribe a user from another JID"; args = { { name = "jid", type = "string" }; { name = "contact", type = "string" }; }; host_selector = "jid"; handler = function(self, jid, contact) --luacheck: ignore 212/self return unsubscribe(jid, contact); end; }); module:add_item("shell-command", { section = "roster"; section_desc = "View and manage user rosters (contact lists)"; name = "unsubscribe_both"; desc = "Unubscribe a user and a contact JID from each other"; args = { { name = "jid", type = "string" }; { name = "contact", type = "string" }; }; host_selector = "jid"; handler = function(self, jid, contact) --luacheck: ignore 212/self return unsubscribe_both(jid, contact); end; }); prosody-13.0.1/plugins/PaxHeaders/mod_s2s.lua0000644000000000000000000000011714773555365016132 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.639710726 prosody-13.0.1/plugins/mod_s2s.lua0000644000175000017500000011613514773555365020340 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- module:set_global(); local prosody = prosody; local hosts = prosody.hosts; local core_process_stanza = prosody.core_process_stanza; local tostring, type = tostring, type; local traceback = debug.traceback; 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 "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_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", "", "Established incoming s2s connections", {"host", "type", "ip_family"} ); local measure_connections_outbound = module:metric( "gauge", "connections_outbound", "", "Established outgoing s2s connections", {"host", "type", "ip_family"} ); local m_accepted_tcp_connections = module:metric( "counter", "accepted_tcp", "", "Accepted incoming connections on the TCP layer" ); local m_authn_connections = module:metric( "counter", "authenticated", "", "Authenticated incoming connections", {"host", "direction", "mechanism"} ); local m_initiated_connections = module:metric( "counter", "initiated", "", "Initiated outbound connections", {"host"} ); local m_closed_connections = module:metric( "counter", "closed", "", "Closed connections", {"host", "direction", "error"} ); local m_tls_params = module:metric( "counter", "encrypted", "", "Encrypted connections", {"protocol"; "cipher"} ); local sessions = module:shared("sessions"); local runner_callbacks = {}; local session_events = {}; local listener = {}; local log = module._log; local s2s_service_options = { default_port = 5269; use_ipv4 = module:get_option_boolean("use_ipv4", true); use_ipv6 = module:get_option_boolean("use_ipv6", true); use_dane = module:get_option_boolean("use_dane", false); }; 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() -- TODO: init all expected metrics once? -- or maybe create/delete them in host-activate/host-deactivate? requires -- extra API in openmetrics.lua tho for _, session in pairs(sessions) do local is_inbound = string.sub(session.type, 4, 5) == "in" local metric_family = is_inbound and measure_connections_inbound or measure_connections_outbound local host = is_inbound and session.to_host or session.from_host or "" local type_ = session.type or "other" -- we want to expose both v4 and v6 counters in all cases to make -- queries smoother local is_ipv6 = session.ip and session.ip:match(":") and 1 or 0 local is_ipv4 = 1 - is_ipv6 metric_family:with_labels(host, type_, "ipv4"):add(is_ipv4) metric_family:with_labels(host, type_, "ipv6"):add(is_ipv6) end end); --- Handle stanzas to remote domains 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.count(), session.to_host); local dummy = { type = "s2sin"; send = function () (session.log or log)("error", "Replying to to an s2s error reply, please report this! Traceback: %s", traceback()); end; dummy = true; close = function () (session.log or log)("error", "Attempting to close the dummy origin of s2s error replies, please report this! Traceback: %s", traceback()); end; }; -- FIXME Allow for more specific error conditions -- TODO use util.error ? local error_type = "cancel"; local condition = "remote-server-not-found"; local reason_text; 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_error(reason) then error_type, condition, reason_text = reason.type, reason.condition, reason.text; elseif type(reason) == "string" then reason_text = reason; 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 end session.sendq = nil; end -- Handles stanzas to existing s2s sessions function route_to_existing_session(event) local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza; if not hosts[from_host] then log("warn", "Attempt to send stanza from %s - a host we don't serve", from_host); return false; end if hosts[to_host] then log("warn", "Attempt to route stanza to a remote %s - a host we do serve?!", from_host); return false; end local host = hosts[from_host].s2sout[to_host]; if not host then return end -- We have a connection to this host already if host.type == "s2sout_unauthed" and (stanza.name ~= "db:verify" or not host.dialback_key) then (host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host); -- Queue stanza until we are able to send it if not host.sendq then -- luacheck: ignore 122 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; elseif host.type == "local" or host.type == "component" then log("error", "Trying to send a stanza to ourselves??") log("error", "Traceback: %s", traceback()); log("error", "Stanza: %s", stanza); return false; else if host.sends2s(stanza) then return true; end end end -- Create a new outgoing session for a stanza function route_to_new_session(event) local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza; log("debug", "opening a new outgoing connection for this stanza"); local host_session = s2s_new_outgoing(from_host, to_host); host_session.version = 1; -- Store in buffer host_session.bounce_sendq = bounce_sendq; 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 module:context(from_host):fire_event("s2sout-created", { session = host_session }); local xmpp_extra = setmetatable({}, s2s_service_options_mt); local resolver = service.new(to_host, "xmpp-server", "tcp", xmpp_extra); if host_session.ssl_ctx then local sslctx = host_session.ssl_ctx; local xmpps_extra = setmetatable({ default_port = false; servername = to_host; sslctx = sslctx }, s2s_service_options_mt); resolver = resolver_chain.new({ service.new(to_host, "xmpps-server", "tcp", xmpps_extra); resolver; }); end local pre_event = { session = host_session; resolver = resolver }; module:context(from_host):fire_event("s2sout-pre-connect", pre_event); resolver = pre_event.resolver; connect(resolver, listener, nil, { session = host_session }); m_initiated_connections:with_labels(from_host):add(1) return true; end local function keepalive(event) local session = event.session; if not session.notopen then return event.session.sends2s(' '); end end module:hook("s2s-read-timeout", keepalive, -1); function module.add_host(module) if module:get_option_boolean("disallow_s2s", false) then module:log("warn", "The 'disallow_s2s' config option is deprecated, please see https://prosody.im/doc/s2s#disabling"); return nil, "This host has disallow_s2s set"; 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 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 mark_connected(session); return true; elseif require_encryption and not session.secure then session.log("warn", "Encrypted server-to-server communication is required but was not offered by %s", session.to_host); session:close({ condition = "policy-violation", text = "Encrypted server-to-server communication is required but was not offered", }, nil, "Could not establish encrypted connection to remote server"); return true; elseif not session.dialback_verifying then session.log("warn", "No SASL EXTERNAL offer and Dialback doesn't seem to be enabled, giving up"); session:close({ condition = "unsupported-feature", text = "No viable authentication method offered", }, nil, "No viable authentication method offered by remote server"); return true; end end, -1); function module.unload() if module.reloading then return end for _, session in pairs(sessions) do if session.host == module.host then session:close("host-gone"); end end end end -- Stream is authorised, and ready for normal stanzas function mark_connected(session) local sendq = session.sendq; local from, to = session.from_host, session.to_host; session.log("info", "%s s2s connection %s->%s complete", session.direction:gsub("^.", string.upper), from, to); local event_data = { session = session }; if session.type == "s2sout" then module:fire_event("s2sout-established", event_data); module:context(from):fire_event("s2sout-established", event_data); if session.incoming then session.send = function(stanza) return module:context(from):fire_event("route/remote", { from_host = from, to_host = to, stanza = stanza }); end; end else if session.outgoing and not hosts[to].s2sout[from] then session.log("debug", "Setting up to handle route from %s to %s", to, from); hosts[to].s2sout[from] = session; -- luacheck: ignore 122 end local host_session = hosts[to]; session.send = function(stanza) return host_session.events.fire_event("route/remote", { from_host = to, to_host = from, stanza = stanza }); end; module:fire_event("s2sin-established", event_data); module:context(to):fire_event("s2sin-established", event_data); end if session.direction == "outgoing" then if sendq then session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", sendq.count(), session.to_host); local send = session.sends2s; for stanza in sendq:consume() do -- TODO check send success send(stanza); end session.sendq = nil; end end if session.connect_timeout then stop_timer(session.connect_timeout); session.connect_timeout = nil; end end function make_authenticated(event) local session, host = event.session, event.host; if not session.secure then if require_encryption or (secure_auth and not(insecure_domains[host])) or secure_domains[host] then session:close({ condition = "policy-violation", text = "Encrypted server-to-server communication is required but was not " ..((session.direction == "outgoing" and "offered") or "used") }, nil, "Could not establish encrypted connection to remote server"); end end if session.type == "s2sout_unauthed" and not session.authenticated_remote and secure_auth and not insecure_domains[host] then session:close({ condition = "policy-violation"; text = "Failed to verify certificate (internal error)"; }); return; end if hosts[host] then session:close({ condition = "undefined-condition", text = "Attempt to authenticate as a host we serve" }); end if session.type == "s2sout_unauthed" then session.type = "s2sout"; elseif session.type == "s2sin_unauthed" then session.type = "s2sin"; elseif session.type ~= "s2sin" and session.type ~= "s2sout" then return false; end if session.incoming and host then if not session.hosts[host] then session.hosts[host] = {}; end session.hosts[host].authed = true; end session.log("debug", "connection %s->%s is now authenticated for %s", session.from_host, session.to_host, host); local local_host = session.direction == "incoming" and session.to_host or session.from_host m_authn_connections:with_labels(local_host, session.direction, event.mechanism or "other"):add(1) if (session.type == "s2sout" and session.external_auth ~= "succeeded") or session.type == "s2sin" then -- Stream either used dialback for authentication or is an incoming stream. mark_connected(session); end return true; 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 local cert if conn.ssl_peercertificate then cert = conn:ssl_peercertificate() end return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert }); end --- XMPP stream event handlers local function session_secure(session) session.secure = true; session.encrypted = true; 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; m_tls_params:with_labels(info.protocol, info.cipher):add(1) else (session.log or log)("info", "Stream encrypted"); end end local stream_callbacks = { default_ns = "jabber:server" }; function stream_callbacks.handlestanza(session, stanza) stanza = session.filter("stanzas/in", stanza); session.thread:run(stanza); end 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({ event = "streamopened", attr = attr }); end 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 -- Set by mod_tls during STARTTLS handshake session.starttls = "completed"; session_secure(session); end if session.direction == "incoming" then -- Send a reply stream header -- Validate to/from local to, from = attr.to, attr.from; if to then to = nameprep(attr.to); end if from then from = nameprep(attr.from); end if not to and attr.to then -- COMPAT: Some servers do not reliably set 'to' (especially on stream restarts) session:close({ condition = "improper-addressing", text = "Invalid 'to' address" }); return; end if not from and attr.from then -- COMPAT: Some servers do not reliably set 'from' (especially on stream restarts) session:close({ condition = "improper-addressing", text = "Invalid 'from' address" }); return; end -- Set session.[from/to]_host if they have not been set already and if -- this session isn't already authenticated if session.type == "s2sin_unauthed" and from and not session.from_host then session.from_host = from; elseif from ~= session.from_host then session:close({ condition = "improper-addressing", text = "New stream 'from' attribute does not match original" }); return; end if session.type == "s2sin_unauthed" and to and not session.to_host then session.to_host = to; session.host = to; elseif to ~= session.to_host then session:close({ condition = "improper-addressing", text = "New stream 'to' attribute does not match original" }); return; end -- For convenience we'll put the sanitised values into these variables to, from = session.to_host, session.from_host; session.streamid = uuid_gen(); (session.log or log)("debug", "Incoming s2s received %s", st.stanza("stream:stream", attr):top_tag()); if to then if not hosts[to] then -- Attempting to connect to a host we don't serve session:close({ condition = "host-unknown"; text = "This host does not serve "..to }); return; elseif not hosts[to].modules.s2s then -- Attempting to connect to a host that disallows s2s session:close({ condition = "policy-violation"; text = "Server-to-server communication is disabled for this host"; }); return; end end if hosts[from] then session:close({ condition = "undefined-condition", text = "Attempt to connect from a host we serve" }); return; end if session.secure and not session.cert_chain_status then if check_cert_status(session) == false then return; end end session:open_stream(session.to_host, session.from_host) if session.destroyed then -- sending the stream opening could have failed during an opportunistic write return end session.notopen = nil; if session.version >= 1.0 then local features = st.stanza("stream:features"); if to then module:context(to):fire_event("s2s-stream-features", { origin = session, features = features }); else (session.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", from or session.ip or "unknown host"); module:fire_event("s2s-stream-features-legacy", { origin = session, features = features }); 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 (session.log or log)("warn", "No stream features to offer, giving up"); session:close({ condition = "undefined-condition", text = "No stream features to offer" }); end end elseif session.direction == "outgoing" then session.notopen = nil; if not attr.id then log("warn", "Stream response did not give us a stream id!"); session:close({ condition = "undefined-condition", text = "Missing stream ID" }); return; end session.streamid = attr.id; if session.secure and not session.cert_chain_status then if check_cert_status(session) == false then return; else session.authenticated_remote = true; end end -- If server is pre-1.0, don't wait for features, just do dialback if session.version < 1.0 then if not session.dialback_verifying then module:context(session.from_host):fire_event("s2sout-authenticate-legacy", { origin = session }); else mark_connected(session); end end end end function session_events.streamclosed(session) (session.log or log)("debug", "Received "); 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({ event = "streamclosed", attr = attr }); end -- Some stream conditions indicate a problem on our end, e.g. that we sent -- something invalid. Those should be investigated. Others are problems or -- events in the remote host that don't affect us, or simply that the -- connection was closed for being idle. local stream_condition_severity = { ["bad-format"] = "warn"; ["bad-namespace-prefix"] = "warn"; ["conflict"] = "warn"; ["connection-timeout"] = "debug"; ["host-gone"] = "info"; ["host-unknown"] = "info"; ["improper-addressing"] = "warn"; ["internal-server-error"] = "warn"; ["invalid-from"] = "warn"; ["invalid-namespace"] = "warn"; ["invalid-xml"] = "warn"; ["not-authorized"] = "warn"; ["not-well-formed"] = "warn"; ["policy-violation"] = "warn"; ["remote-connection-failed"] = "warn"; ["reset"] = "info"; ["resource-constraint"] = "info"; ["restricted-xml"] = "warn"; ["see-other-host"] = "info"; ["system-shutdown"] = "info"; ["undefined-condition"] = "warn"; ["unsupported-encoding"] = "warn"; ["unsupported-feature"] = "warn"; ["unsupported-stanza-type"] = "warn"; ["unsupported-version"] = "warn"; } 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}"))); session:close("invalid-namespace"); elseif error == "parse-error" then session.log("debug", "Server-to-server XML parse error: %s", error); session:close("not-well-formed"); elseif error == "stream-error" then local condition, text = "undefined-condition"; for child in data:childtags(nil, xmlns_xmpp_streams) do if child.name ~= "text" then condition = child.name; else text = child:get_text(); end if condition ~= "undefined-condition" and text then break; end end text = condition .. (text and (" ("..text..")") or ""); session.log(stream_condition_severity[condition] or "info", "Session closed by remote with error: %s", text); session:close(nil, text); end end --- Session methods local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; -- reason: stream error to send to the remote server -- remote_reason: stream error received from the remote server -- bounce_reason: stanza error to pass to bounce_sendq because stream- and stanza errors are different local function session_close(session, reason, remote_reason, bounce_reason) local log = session.log or log; if not session.conn then log("debug", "Attempt to close without associated connection with reason %q", reason); return end local conn = session.conn; conn:pause_writes(); -- until :close if session.notopen then if session.direction == "incoming" then session:open_stream(session.to_host, session.from_host); else session:open_stream(session.from_host, session.to_host); end end local this_host = session.direction == "outgoing" and session.from_host or session.to_host if not hosts[this_host] then this_host = ":unknown"; end if reason then -- nil == no err, initiated by us, false == initiated by remote local stream_error; local condition, text, extra if type(reason) == "string" then -- assume stream error condition = reason elseif type(reason) == "table" and not st.is_stanza(reason) then condition = reason.condition or "undefined-condition" text = reason.text extra = reason.extra end if condition then stream_error = st.stanza("stream:error"):tag(condition, stream_xmlns_attr):up(); if text then stream_error:tag("text", stream_xmlns_attr):text(text):up(); end if extra then stream_error:add_child(extra); end end if this_host and condition then m_closed_connections:with_labels(this_host, session.direction, condition):add(1) end if st.is_stanza(stream_error) then -- to and from are never unknown on outgoing connections log("debug", "Disconnecting %s->%s[%s], is: %s", session.from_host or "(unknown host)" or session.ip, session.to_host or "(unknown host)", session.type, stream_error); session.sends2s(stream_error); end else m_closed_connections:with_labels(this_host or ":unknown", session.direction, reason == false and ":remote-choice" or ":local-choice"):add(1) end session.sends2s(""); function session.sends2s() return false; end -- luacheck: ignore 422/reason 412/reason -- FIXME reason should be managed in a place common to c2s, s2s, bosh, component etc local reason = remote_reason or (reason and (reason.text or reason.condition)) or reason; session.log("info", "%s s2s stream %s->%s closed: %s", session.direction:gsub("^.", string.upper), session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed"); conn:resume_writes(); if session.connect_timeout then stop_timer(session.connect_timeout); session.connect_timeout = nil; end -- Authenticated incoming stream may still be sending us stanzas, so wait for from remote if reason == nil and not session.notopen and session.direction == "incoming" then add_task(stream_close_timeout, function () if not session.destroyed then session.log("warn", "Failed to receive a stream close response, closing connection anyway..."); s2s_destroy_session(session, reason, bounce_reason); conn:close(); end end); else s2s_destroy_session(session, reason, bounce_reason); conn:close(); -- Close immediately, as this is an outgoing connection or is not authed end end function session_stream_attrs(session, from, to, attr) -- luacheck: ignore 212/session if not from or (hosts[from] and hosts[from].modules.dialback) then attr["xmlns:db"] = 'jabber:server:dialback'; end if not from then attr.from = ''; end if not to then attr.to = ''; end end -- Session initialization logic shared by incoming and outgoing local function initialize_session(session) local stream = new_xmpp_stream(session, stream_callbacks, stanza_size_limit); 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); local log = session.log or log; session.stream = stream; session.notopen = true; function session.reset_stream() session.notopen = true; session.streamid = nil; session.stream:reset(); end session.stream_attrs = session_stream_attrs; local filter = initialize_filters(session); local conn = session.conn; local w = conn.write; if conn:ssl() then -- Direct TLS was used session_secure(session); end function session.sends2s(t) log("debug", "Sending[%s]: %s", session.type, t.top_tag and t:top_tag() or t:match("^[^>]*>?")); if t.name then t = filter("stanzas/out", t); 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 end end function session.data(data) data = filter("bytes/in", data); if data then local ok, err = stream:feed(data); if ok then return; end log("debug", "Received invalid XML (%s) %d bytes: %q", err, #data, data:sub(1, 300)); if err == "stanza-too-large" then session:close({ condition = "policy-violation", text = "XML stanza is too big", extra = st.stanza("stanza-too-big", { xmlns = 'urn:xmpp:errors' }), }, nil, "Received invalid XML from remote server"); else session:close("not-well-formed", nil, "Received invalid XML from remote server"); end end end session.close = session_close; local handlestanza = stream_callbacks.handlestanza; function session.dispatch_stanza(session, stanza) -- luacheck: ignore 432/session return handlestanza(session, stanza); end module:fire_event("s2s-created", { session = session }); session.connect_timeout = add_task(connect_timeout, function () if session.type == "s2sin" or session.type == "s2sout" then return; -- Ok, we're connected elseif session.type == "s2s_destroyed" then return; -- Session already destroyed end -- Not connected, need to close session and clean up (session.log or log)("debug", "Destroying incomplete session %s->%s due to inactivity", session.from_host or "(unknown)", session.to_host or "(unknown)"); session:close("connection-timeout"); end); end function runner_callbacks:ready() self.data.log("debug", "Runner %s ready (%s)", self.thread, coroutine.status(self.thread)); self.data.conn:resume(); end function runner_callbacks:waiting() self.data.log("debug", "Runner %s waiting (%s)", self.thread, coroutine.status(self.thread)); self.data.conn:pause(); end function runner_callbacks:error(err) (self.data.log or log)("error", "Traceback[s2s]: %s", err); end function listener.onconnect(conn) conn:setoption("keepalive", opt_keepalives); local session = sessions[conn]; if not session then -- New incoming connection session = s2s_new_incoming(conn); sessions[conn] = session; session.log("debug", "Incoming s2s connection"); module:fire_event("s2sin-connected", { session = session }) initialize_session(session); m_accepted_tcp_connections:with_labels():add(1) else -- Outgoing session connected module:fire_event("s2sout-connected", { session = session }) session:open_stream(session.from_host, session.to_host); end module:fire_event("s2s-connected", { session = session }) session.ip = conn:ip(); end function listener.onincoming(conn, data) local session = sessions[conn]; if session then session.data(data); end end function listener.onstatus(conn, status) if status == "ssl-handshake-complete" then local session = sessions[conn]; if session and session.direction == "outgoing" then session.log("debug", "Sending stream header..."); session:open_stream(session.from_host, session.to_host); end end end function listener.ondisconnect(conn, err) local session = sessions[conn]; if session then sessions[conn] = nil; (session.log or log)("debug", "s2s disconnected: %s->%s (%s)", session.from_host, session.to_host, err or "connection closed"); if session.secure == false and err then -- TODO util.error-ify this err = "Error during negotiation of encrypted connection: "..err; end s2s_destroy_session(session, err); end module:fire_event("s2s-closed", { session = session; conn = conn }); end function listener.onfail(data, err) local session = data and data.session; if session then if err and session.direction == "outgoing" and session.notopen then (session.log or log)("debug", "s2s connection attempt failed: %s", err); end (session.log or log)("debug", "s2s disconnected: %s->%s (%s)", session.from_host, session.to_host, err or "connection closed"); s2s_destroy_session(session, err); end end function listener.onreadtimeout(conn) local session = sessions[conn]; if session then return (hosts[session.host] or prosody).events.fire_event("s2s-read-timeout", { session = session }); end end function listener.ondrain(conn) local session = sessions[conn]; if session then return (hosts[session.host] or prosody).events.fire_event("s2s-ondrain", { session = session }); end end function listener.onpredrain(conn) local session = sessions[conn]; if session then return (hosts[session.host] or prosody).events.fire_event("s2s-pre-ondrain", { session = session }); end end function listener.register_outgoing(conn, session) sessions[conn] = session; initialize_session(session); end function listener.ondetach(conn) sessions[conn] = nil; end function listener.onattach(conn, data) local session = data and data.session; if session then session.conn = conn; sessions[conn] = session; initialize_session(session); end end -- Complete the sentence "Your certificate " with what's wrong local function friendly_cert_error(session) --> string if session.cert_chain_status == "invalid" then local cert_errors = set.new(); if type(session.cert_chain_errors) == "table" then cert_errors:add_list(session.cert_chain_errors[1]); elseif type(session.cert_chain_errors) == "string" then cert_errors:add(session.cert_chain_errors); end if cert_errors:contains("certificate has expired") then return "has expired"; elseif cert_errors:contains("self signed certificate") or 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 if type(session.cert_chain_errors) == "table" then local chain_errors = set.new(session.cert_chain_errors[2]); for i, e in pairs(session.cert_chain_errors) do if i > 2 then chain_errors:add_list(e); end end if chain_errors:contains("certificate has expired") then return "has an expired certificate chain"; elseif chain_errors:contains("no matching DANE TLSA records") then return "does not match any DANE TLSA records"; end end return "is not trusted"; -- for some other reason elseif session.cert_identity_status == "invalid" then return "is not valid for this name"; end -- this should normally be unreachable except if no s2s auth module was loaded return "could not be validated"; end function check_auth_policy(event) local host, session = event.host, event.session; local must_secure = secure_auth; if not must_secure and secure_domains[host] then must_secure = true; elseif must_secure and insecure_domains[host] then must_secure = false; end if must_secure and (session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid") then local reason = friendly_cert_error(session); session.log("warn", "Forbidding insecure connection to/from %s because its certificate %s", host or session.ip or "(unknown host)", reason); -- XEP-0178 recommends closing outgoing connections without warning -- but does not give a rationale for this. -- 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; end end module:hook("s2s-check-certificate", check_auth_policy, -1); module:hook("server-stopping", function(event) -- Close ports local pm = require "prosody.core.portmanager"; for _, netservice in pairs(module.items["net-provider"]) do pm.unregister_service(netservice.name, netservice); end -- Stop opening new connections for host in pairs(prosody.hosts) do if prosody.hosts[host].modules.s2s then module:context(host):unhook("route/remote", route_to_new_session); end end local wait, done = async.waiter(1, true); module:hook("s2s-closed", function () if next(sessions) == nil then done(); end end, 1) -- Close sessions local reason = event.reason; for _, session in pairs(sessions) do session:close{ condition = "system-shutdown", text = reason }; end -- Wait for them to close properly if they haven't already if next(sessions) ~= nil then module:log("info", "Waiting for sessions to close"); add_task(stream_close_timeout + 1, function () done() end); wait(); end end, -200); module:provides("net", { name = "s2s"; listener = listener; default_port = 5269; encryption = "starttls"; ssl_config = { -- FIXME This only applies to Direct TLS, which we don't use yet. -- This gets applied for real in mod_tls verify = { "peer", "client_once", }; verifyext = { "lsec_continue", -- Continue past certificate verification errors "lsec_ignore_purpose", -- Validate client certificates as if they were server certificates }; }; multiplex = { protocol = "xmpp-server"; pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:server%1.*>"; }; }); module:provides("net", { name = "s2s_direct_tls"; listener = listener; encryption = "ssl"; ssl_config = { verify = { "peer", "client_once", }; verifyext = { "lsec_continue", -- Continue past certificate verification errors "lsec_ignore_purpose", -- Validate client certificates as if they were server certificates }; }; multiplex = { protocol = "xmpp-server"; pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:server%1.*>"; }; }); prosody-13.0.1/plugins/PaxHeaders/mod_s2s_auth_certs.lua0000644000000000000000000000011714773555365020353 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.639710726 prosody-13.0.1/plugins/mod_s2s_auth_certs.lua0000644000175000017500000000405214773555365022553 0ustar00prosodyprosody00000000000000module:set_global(); 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", { "chain"; "identity" }) module:hook("s2s-check-certificate", function(event) local session, host, cert = event.session, event.host, event.cert; 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 = 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"); 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; else log("debug", "certificate chain validation result: valid"); session.cert_chain_status = "valid"; -- We'll go ahead and verify the asserted identity if the -- connecting server specified one. if host then if cert_verify_identity(host, "xmpp-server", cert) then session.cert_identity_status = "valid" else session.cert_identity_status = "invalid" 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); prosody-13.0.1/plugins/PaxHeaders/mod_s2s_auth_dane_in.lua0000644000000000000000000000011714773555365020630 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.639710726 prosody-13.0.1/plugins/mod_s2s_auth_dane_in.lua0000644000175000017500000000640614773555365023035 0ustar00prosodyprosody00000000000000module: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); prosody-13.0.1/plugins/PaxHeaders/mod_s2s_bidi.lua0000644000000000000000000000011714773555365017121 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.639710726 prosody-13.0.1/plugins/mod_s2s_bidi.lua0000644000175000017500000000360214773555365021321 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2019 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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); module:hook_tag("http://etherx.jabber.org/streams", "features", function (session, stanza) if session.type == "s2sout_unauthed" and (not require_encryption or session.secure) then local bidi = stanza:get_child("bidi", xmlns_bidi_feature); if bidi then session.incoming = true; session.log("debug", "Requesting bidirectional stream"); 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); 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); prosody-13.0.1/plugins/PaxHeaders/mod_saslauth.lua0000644000000000000000000000011714773555365017247 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.643710742 prosody-13.0.1/plugins/mod_saslauth.lua0000644000175000017500000004457514773555365021465 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: ignore 431/log 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; local xmlns_sasl ='urn:ietf:params:xml:ns:xmpp-sasl'; local xmlns_bind ='urn:ietf:params:xml:ns:xmpp-bind'; local function build_reply(status, ret, err_msg) local reply = st.stanza(status, {xmlns = xmlns_sasl}); if status == "failure" then reply:tag(ret):up(); if err_msg then reply:tag("text"):text(err_msg); end elseif status == "challenge" or status == "success" then if ret == "" then reply:text("=") elseif ret then reply:text(base64.encode(ret)); end else module:log("error", "Unknown sasl status: %s", status); end return reply; end local function handle_status(session, status, ret, err_msg) if not session.sasl_handler then return "failure", "temporary-auth-failure", "Connection gone"; end if status == "failure" then 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.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(); else module:log("warn", "SASL succeeded but username was invalid"); module:fire_event("authentication-failure", { session = session, condition = "not-authorized", text = err }); session.sasl_handler = session.sasl_handler:clean_clone(); return "failure", "not-authorized", "User authenticated successfully, but username was invalid"; end end return status, ret, err_msg; end local function sasl_process_cdata(session, stanza) local text = stanza[1]; if text then text = base64.decode(text); if not text then session.sasl_handler = nil; session.send(build_reply("failure", "incorrect-encoding")); return true; end end 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 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 module:hook_tag(xmlns_sasl, "success", function (session) if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end module:log("debug", "SASL EXTERNAL with %s succeeded", session.to_host); session.external_auth = "succeeded" session:reset_stream(); session:open_stream(session.from_host, session.to_host); module:fire_event("s2s-authenticated", { session = session, host = session.to_host, mechanism = "EXTERNAL" }); return true; end) module:hook_tag(xmlns_sasl, "failure", function (session, stanza) if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end local text = stanza:get_child_text("text"); local condition = "unknown-condition"; for child in stanza:childtags() do if child.name ~= "text" then condition = child.name; break; end end local err = errors.new({ -- TODO type = what? text = text, condition = condition, }, { session = session, stanza = stanza, }); module:log("info", "SASL EXTERNAL with %s failed: %s", session.to_host, err); session.external_auth = "failed" session.external_auth_failure_reason = err; end, 500) module:hook_tag(xmlns_sasl, "failure", function (session, stanza) -- luacheck: ignore 212/stanza session.log("debug", "No fallback from SASL EXTERNAL failure, giving up"); session:close(nil, session.external_auth_failure_reason, errors.new({ type = "wait", condition = "remote-server-timeout", text = "Could not authenticate to remote server", }, { session = session, sasl_failure = session.external_auth_failure_reason, })); return true; end, 90) module:hook_tag("http://etherx.jabber.org/streams", "features", function (session, stanza) if session.type ~= "s2sout_unauthed" or not session.secure then return; end local mechanisms = stanza:get_child("mechanisms", xmlns_sasl) if mechanisms then for mech in mechanisms:childtags() do if mech[1] == "EXTERNAL" then module:log("debug", "Initiating SASL EXTERNAL with %s", session.to_host); local reply = st.stanza("auth", {xmlns = xmlns_sasl, mechanism = "EXTERNAL"}); reply:text(base64.encode(session.from_host)) session.sends2s(reply) session.external_auth = "attempting" return true end end end end, 150); local function s2s_external_auth(session, stanza) if session.external_auth ~= "offered" then return end -- Unexpected request local mechanism = stanza.attr.mechanism; if mechanism ~= "EXTERNAL" then session.sends2s(build_reply("failure", "invalid-mechanism")); return true; end if not session.secure then session.sends2s(build_reply("failure", "encryption-required")); return true; end local text = stanza[1]; if not text then session.sends2s(build_reply("failure", "malformed-request")); return true; end text = base64.decode(text); if not text then session.sends2s(build_reply("failure", "incorrect-encoding")); return true; end -- The text value is either "" or equals session.from_host if not ( text == "" or text == session.from_host ) then session.sends2s(build_reply("failure", "invalid-authzid")); return true; end -- We've already verified the external cert identity before offering EXTERNAL if session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid" then session.sends2s(build_reply("failure", "not-authorized")); session:close(); return true; end -- Success! session.external_auth = "succeeded"; session.sends2s(build_reply("success")); module:log("info", "Accepting SASL EXTERNAL identity from %s", session.from_host); module:fire_event("s2s-authenticated", { session = session, host = session.from_host, mechanism = mechanism }); session:reset_stream(); return true; end module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event) local session, stanza = event.origin, event.stanza; if session.type == "s2sin_unauthed" then return s2s_external_auth(session, stanza) end 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 if not session.sasl_handler then session.sasl_handler = usermanager_get_sasl_handler(module.host, session); end local mechanism = stanza.attr.mechanism; if not session.secure and (secure_auth_only or insecure_mechanisms:contains(mechanism)) then session.send(build_reply("failure", "encryption-required")); return true; elseif disabled_mechanisms:contains(mechanism) then session.send(build_reply("failure", "invalid-mechanism")); return true; end local valid_mechanism = session.sasl_handler:select(mechanism); if not valid_mechanism then session.send(build_reply("failure", "invalid-mechanism")); return true; end return sasl_process_cdata(session, stanza); end); module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:response", function(event) local session = event.origin; if not(session.sasl_handler and session.sasl_handler.selected) then session.send(build_reply("failure", "not-authorized", "Out of order SASL element")); return true; end return sasl_process_cdata(session, event.stanza); end); module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event) local session = event.origin; session.sasl_handler = nil; session.send(build_reply("failure", "aborted")); return true; end); local function tls_unique(self) 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' }; local bind_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-bind' }; local xmpp_session_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-session' }; module:hook("stream-features", function(event) local origin, features = event.origin, event.features; local log = origin.log or log; if not origin.username then if secure_auth_only and not origin.secure then log("debug", "Not offering authentication on insecure connection"); return; 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 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"); 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"] = 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"); end end local mechanisms = st.stanza("mechanisms", mechanisms_attr); local sasl_mechanisms = sasl_handler:mechanisms() local available_mechanisms = set.new(); for mechanism in pairs(sasl_mechanisms) do available_mechanisms:add(mechanism); end log("debug", "SASL mechanisms supported by handler: %s", available_mechanisms); local usable_mechanisms = available_mechanisms - disabled_mechanisms; local available_disabled = set.intersection(available_mechanisms, disabled_mechanisms); if not available_disabled:empty() then log("debug", "Not offering disabled mechanisms: %s", available_disabled); end local available_insecure = set.intersection(available_mechanisms, insecure_mechanisms); if not origin.secure and not available_insecure:empty() then log("debug", "Session is not secure, not offering insecure mechanisms: %s", available_insecure); usable_mechanisms = usable_mechanisms - insecure_mechanisms; end if not usable_mechanisms:empty() then log("debug", "Offering usable mechanisms: %s", usable_mechanisms); for mechanism in usable_mechanisms do 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 local authmod = module:get_option_string("authentication", "internal_hashed"); if available_mechanisms:empty() then log("warn", "No available SASL mechanisms, verify that the configured authentication module '%s' is loaded and configured correctly", authmod); return; end if not origin.secure and not available_insecure:empty() then if not available_disabled:empty() then log("warn", "All SASL mechanisms provided by authentication module '%s' are forbidden on insecure connections (%s) or disabled (%s)", authmod, available_insecure, available_disabled); else log("warn", "All SASL mechanisms provided by authentication module '%s' are forbidden on insecure connections (%s)", authmod, available_insecure); end elseif not available_disabled:empty() then log("warn", "All SASL mechanisms provided by authentication module '%s' are disabled (%s)", authmod, available_disabled); end 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 end); module:hook("s2s-stream-features", function(event) local origin, features = event.origin, event.features; if origin.secure and origin.type == "s2sin_unauthed" then -- Offer EXTERNAL only if both chain and identity is valid. if origin.cert_chain_status == "valid" and origin.cert_identity_status == "valid" then module:log("debug", "Offering SASL EXTERNAL"); origin.external_auth = "offered" features:tag("mechanisms", { xmlns = xmlns_sasl }) :tag("mechanism"):text("EXTERNAL") :up():up(); end end end); module:hook("stanza/iq/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event) local origin, stanza = event.origin, event.stanza; 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)); origin.log("debug", "Resource bound: %s", origin.full_jid); else origin.send(st.error_reply(stanza, err_type, err, err_msg)); origin.log("debug", "Resource bind failed: %s", err_msg or err); end return true; end); local function handle_legacy_session(event) event.origin.send(st.reply(event.stanza)); return true; end module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session); module:hook("iq/host/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session); prosody-13.0.1/plugins/PaxHeaders/mod_scansion_record.lua0000644000000000000000000000011714773555365020576 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.643710742 prosody-13.0.1/plugins/mod_scansion_record.lua0000644000175000017500000000703214773555365022777 0ustar00prosodyprosody00000000000000local names = { "Romeo", "Juliet", "Mercutio", "Tybalt", "Benvolio" }; local devices = { "", "phone", "laptop", "tablet", "toaster", "fridge", "shoe" }; local users = {}; 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(); local header_file = dm.getpath(record_id, "scansion", record_date, "scs", true); local record_file = dm.getpath(record_id, "scansion", record_date, "log", true); local head = io.open(header_file, "w"); local scan = io.open(record_file, "w+"); local function record(string) scan:write(string); scan:flush(); end local function record_header(string) head:write(string); head:flush(); end local function record_object(class, name, props) head:write(("[%s] %s\n"):format(class, name)); for k,v in pairs(props) do head:write(("\t%s: %s\n"):format(k, v)); end head:write("\n"); head:flush(); end local function record_event(session, event) record(session.scansion_id.." "..event.."\n\n"); end local function record_stanza(stanza, session, verb) local flattened = tostring(stanza:indent(2, "\t")); record(session.scansion_id.." "..verb..":\n\t"..flattened.."\n\n"); end local function record_stanza_in(stanza, session) if stanza.attr.xmlns == nil then local copy = st.clone(stanza); copy.attr.from = nil; record_stanza(copy, session, "sends") end return stanza; end local function record_stanza_out(stanza, session) if stanza.attr.xmlns == nil then if not (stanza.name == "iq" and stanza:get_child("bind", "urn:ietf:params:xml:ns:xmpp-bind")) then local copy = st.clone(stanza); if copy.attr.to == session.full_jid then copy.attr.to = nil; end record_stanza(copy, session, "receives"); end end return stanza; end module:hook("resource-bind", function (event) local session = event.session; if not users[session.username] then users[session.username] = { character = table.remove(names, 1) or id.short(); devices = {}; n_devices = 0; }; end local user = users[session.username]; local device = user.devices[session.resource]; if not device then user.n_devices = user.n_devices + 1; device = devices[user.n_devices] or ("device"..id.short()); user.devices[session.resource] = device; end session.scansion_character = user.character; session.scansion_device = device; session.scansion_id = user.character..(device ~= "" and "'s "..device or device); record_object("Client", session.scansion_id, { jid = session.full_jid, password = "password", }); module:log("info", "Connected: %s", session.scansion_id); record_event(session, "connects"); filters.add_filter(session, "stanzas/in", record_stanza_in); filters.add_filter(session, "stanzas/out", record_stanza_out); end); module:hook("resource-unbind", function (event) local session = event.session; if session.scansion_id then record_event(session, "disconnects"); end end) record_header("# mod_scansion_record on host '"..module.host.."' recording started "..dt.datetime().."\n\n"); record[[ ----- ]] module:hook_global("server-stopping", function () record("# recording ended on "..dt.datetime().."\n"); module:log("info", "Scansion recording available in %s", header_file); end); prosody.events.add_handler("server-cleanup", function () scan:seek("set", 0); for line in scan:lines() do head:write(line, "\n"); end scan:close(); os.remove(record_file); head:close() end); prosody-13.0.1/plugins/PaxHeaders/mod_server_contact_info.lua0000644000000000000000000000011714773555365021457 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.643710742 prosody-13.0.1/plugins/mod_server_contact_info.lua0000644000175000017500000000246614773555365023666 0ustar00prosodyprosody00000000000000-- XEP-0157: Contact Addresses for XMPP Services for Prosody -- -- Copyright (C) 2011-2018 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 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", {}); local contact_config = module:get_option("contact_info", { admin = array.collect(admins / jid.prep / function(admin) return url.build({scheme = "xmpp"; path = admin}); end); }); 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); prosody-13.0.1/plugins/PaxHeaders/mod_server_info.lua0000644000000000000000000000011714773555365017744 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.643710742 prosody-13.0.1/plugins/mod_server_info.lua0000644000175000017500000000267514773555365022155 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/plugins/PaxHeaders/mod_smacks.lua0000644000000000000000000000011714773555365016704 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.647710757 prosody-13.0.1/plugins/mod_smacks.lua0000644000175000017500000007171514773555365021116 0ustar00prosodyprosody00000000000000-- XEP-0198: Stream Management for Prosody IM -- -- Copyright (C) 2010-2015 Matthew Wild -- Copyright (C) 2010 Waqas Hussain -- Copyright (C) 2012-2022 Kim Alvefur -- Copyright (C) 2012 Thijs Alkemade -- Copyright (C) 2014 Florian Zeitz -- Copyright (C) 2016-2020 Thilo Molitor -- -- 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; local os_time = os.time; -- These metrics together allow to calculate an instantaneous -- "unacked stanzas" metric in the graphing frontend, without us having to -- iterate over all the queues. local tx_queued_stanzas = module:measure("tx_queued_stanzas", "counter"); local tx_dropped_stanzas = module:metric( "histogram", "tx_dropped_stanzas", "", "number of stanzas in a queue which got dropped", {}, {buckets = {0, 1, 2, 4, 8, 16, 32}} ):with_labels(); local tx_acked_stanzas = module:metric( "histogram", "tx_acked_stanzas", "", "number of items acked per ack received", {}, {buckets = {0, 1, 2, 4, 8, 16, 32}} ):with_labels(); -- number of session resumptions attempts where the session had expired local resumption_expired = module:measure("session_resumption_expired", "counter"); local resumption_age = module:metric( "histogram", "resumption_age", "seconds", "time the session had been hibernating at the time of a resumption", {}, {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 "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 "prosody.core.sessionmanager"; local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas"; local xmlns_delay = "urn:xmpp:delay"; local xmlns_mam2 = "urn:xmpp:mam:2"; local xmlns_sm2 = "urn:xmpp:sm:2"; 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_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_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; local function format_h(h) if h then return string.format("%d", h) end end 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 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) -- fire event only if configured to do so and our session is not already hibernated or destroyed if delayed_ack_timeout > 0 and session.awaiting_ack and not session.hibernating and not session.destroyed then session.log("debug", "Firing event 'smacks-ack-delayed', queue = %d", session.outgoing_stanza_queue and session.outgoing_stanza_queue:count_unacked() or 0); module:fire_event("smacks-ack-delayed", {origin = session, queue = session.outgoing_stanza_queue:table(), stanza = stanza}); end session.delayed_ack_timer = nil; end local function can_do_smacks(session, advertise_only) 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, 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, enable_errors.new("unavailable"); end module:hook("stream-features", function (event) if can_do_smacks(event.origin, true) then event.features:tag("sm", sm2_attr):tag("optional"):up():up(); event.features:tag("sm", sm3_attr):tag("optional"):up():up(); end end); module:hook("s2s-stream-features", function (event) if can_do_smacks(event.origin, true) then event.features:tag("sm", sm2_attr):tag("optional"):up():up(); event.features:tag("sm", sm3_attr):tag("optional"):up():up(); end end); local function should_ack(session, force) if not session then return end -- shouldn't be possible if session.destroyed then return end -- gone if not session.smacks then return end -- not using if session.hibernating then return end -- can't ack when asleep if session.awaiting_ack then return end -- already waiting if force then return force end local queue = session.outgoing_stanza_queue; local expected_h = queue:count_acked() + queue:count_unacked(); local max_unacked = max_unacked_stanzas; if session.state == "inactive" then max_unacked = max_inactive_unacked_stanzas; end -- this check of last_requested_h prevents ack-loops if misbehaving clients report wrong -- stanza counts. it is set when an is really sent (e.g. inside timer), preventing any -- further requests until a higher h-value would be expected. return queue:count_unacked() > max_unacked and expected_h ~= session.last_requested_h; end local function request_ack(session, reason) local queue = session.outgoing_stanza_queue; session.log("debug", "Sending from %s - #queue=%d", reason, queue:count_unacked()); session.awaiting_ack = true; (session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks })) if session.destroyed then return end -- sending something can trigger destruction -- expected_h could be lower than this expression e.g. more stanzas added to the queue meanwhile) session.last_requested_h = queue:count_acked() + queue:count_unacked(); 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 end); end end local function request_ack_now_if_needed(session, force, reason) if should_ack(session, force) then request_ack(session, reason); end end local function outgoing_stanza_filter(stanza, session) -- XXX: Normally you wouldn't have to check the xmlns for a stanza as it's -- 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 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') and not stanza.name:find":"; if is_stanza then local queue = session.outgoing_stanza_queue; local cached_stanza = st.clone(stanza); if cached_stanza.name ~= "iq" and cached_stanza:get_child("delay", xmlns_delay) == nil then cached_stanza = cached_stanza:tag("delay", { xmlns = xmlns_delay, from = jid.bare(session.full_jid or session.host), stamp = datetime.datetime() }); end queue:push(cached_stanza); tx_queued_stanzas(1); if session.hibernating then session.log("debug", "hibernating since %s, stanza queued", datetime.datetime(session.hibernating)); -- FIXME queue implementation changed, anything depending on it being an array will break module:fire_event("smacks-hibernation-stanza-queued", {origin = session, queue = queue:table(), stanza = cached_stanza}); return nil; end end return stanza; end local function count_incoming_stanzas(stanza, session) if not stanza.attr.xmlns then session.handled_stanza_count = session.handled_stanza_count + 1; session.log("debug", "Handled %d incoming stanzas", session.handled_stanza_count); end return stanza; end local function wrap_session_out(session, resume) if not resume then session.outgoing_stanza_queue = smqueue.new(queue_size); end add_filter(session, "stanzas/out", outgoing_stanza_filter, -999); return session; end module:hook("pre-session-close", function(event) local session = event.session; if session.smacks == nil then return end if session.resumption_token then session.log("debug", "Revoking resumption token"); clear_old_session(session); session.resumption_token = nil; else session.log("debug", "Session not resumable"); end if session.hibernating_watchdog then session.log("debug", "Removing sleeping watchdog"); -- If the session is being replaced instead of resume, we don't want the -- old session around to time out and cause trouble for the new session session.hibernating_watchdog:cancel(); session.hibernating_watchdog = nil; else session.log("debug", "No watchdog set"); end -- send out last ack as per revision 1.5.2 of XEP-0198 if session.smacks and session.conn and session.handled_stanza_count then (session.sends2s or session.send)(st.stanza("a", { xmlns = session.smacks; h = format_h(session.handled_stanza_count); })); end end); local function wrap_session_in(session, resume) if not resume then sessions_started(1); session.handled_stanza_count = 0; end add_filter(session, "stanzas/in", count_incoming_stanzas, 999); return session; end local function wrap_session(session, resume) wrap_session_out(session, resume); wrap_session_in(session, resume); return session; end 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? return nil, err; end if session.username then local old_sessions, err = all_old_sessions:get(session.username); 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) return (old_sessions[a].t or 0) > (old_sessions[b].t or 0); end) do count = count + 1; if count > max_old_sessions then break end keep[token] = info; end all_old_sessions:set(session.username, keep); elseif err then session.log("error", "Unable to retrieve old resumption counters: %s", err); end end 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(); end 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) 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 session.log("debug", "Enabling stream management"); session.smacks = xmlns_sm; wrap_session_in(session, false); -- FIXME Resume? return true; end module:hook_tag(xmlns_sm2, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm2); end, 100); module:hook_tag(xmlns_sm3, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm3); end, 100); function handle_r(origin, stanza, xmlns_sm) -- luacheck: ignore 212/stanza if not origin.smacks then origin.log("debug", "Received ack request from non-smack-enabled session"); return; end origin.log("debug", "Received ack request, acking for %d", origin.handled_stanza_count); -- Reply with (origin.sends2s or origin.send)(st.stanza("a", { xmlns = xmlns_sm, h = format_h(origin.handled_stanza_count) })); -- piggyback our own ack request if needed (see request_ack_if_needed() for explanation of last_requested_h) request_ack_now_if_needed(origin, false, "piggybacked by handle_r", nil); return true; end module:hook_tag(xmlns_sm2, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm2); end); module:hook_tag(xmlns_sm3, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm3); end); function handle_a(origin, stanza) if not origin.smacks then return; end origin.awaiting_ack = nil; if origin.awaiting_ack_timer then timer.stop(origin.awaiting_ack_timer); origin.awaiting_ack_timer = nil; end if origin.delayed_ack_timer then timer.stop(origin.delayed_ack_timer) origin.delayed_ack_timer = nil; end -- Remove handled stanzas from outgoing_stanza_queue local h = tonumber(stanza.attr.h); if not h then origin:close{ condition = "invalid-xml"; text = "Missing or invalid 'h' attribute"; }; return; end local queue = origin.outgoing_stanza_queue; local handled_stanza_count = h-queue:count_acked(); local acked, err = ack_errors.coerce(queue:ack(h)); -- luacheck: ignore 211/acked if err then origin.log("warn", "The client says it handled %d new stanzas, but we sent %d :)", handled_stanza_count, queue:count_unacked()); origin.log("debug", "Client h: %d, our h: %d", tonumber(stanza.attr.h), queue:count_acked()); for i, item in queue._queue:items() do origin.log("debug", "Q item %d: %s", i, item); end origin:close(err); return; end tx_acked_stanzas:sample(handled_stanza_count); origin.log("debug", "#queue = %d (acked: %d)", queue:count_unacked(), handled_stanza_count); request_ack_now_if_needed(origin, false, "handle_a", nil) return true; end module:hook_tag(xmlns_sm2, "a", handle_a); module:hook_tag(xmlns_sm3, "a", handle_a); 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", nil, error_from); module:send(reply); end end end end end -- don't send delivery errors for messages which will be delivered by mam later on -- check if stanza was archived --> this will allow us to send back errors for stanzas not archived -- because the user configured the server to do so ("no-archive"-setting for one special contact for example) module:hook("delivery/failure", function(event) local session, stanza = event.session, event.stanza; -- Only deal with authenticated (c2s) sessions if session.username then if stanza.name == "message" and stanza.attr.xmlns == nil and ( stanza.attr.type == "chat" or ( stanza.attr.type or "normal" ) == "normal" ) then -- don't store messages in offline store if they are mam results local mam_result = stanza:get_child("result", xmlns_mam2); if mam_result ~= nil then return true; -- stanza already "handled", don't send an error and don't add it to offline storage end -- do nothing here for normal messages and don't send out "message delivery errors", -- because messages are already in MAM at this point (no need to frighten users) local stanza_id = stanza:get_child_with_attr("stanza-id", "urn:xmpp:sid:0", "by", jid.bare(session.full_jid)); stanza_id = stanza_id and stanza_id.attr.id; if session.mam_requested and stanza_id ~= nil then session.log("debug", "mod_smacks delivery/failure returning true for mam-handled stanza: mam-archive-id=%s", tostring(stanza_id)); return true; -- stanza handled, don't send an error end -- store message in offline store, if this client does not use mam *and* was the last client online local sessions = local_sessions[session.username] and local_sessions[session.username].sessions or nil; if sessions and next(sessions) == session.resource and next(sessions, session.resource) == nil then local ok = module:fire_event("message/offline/handle", { origin = session, username = session.username, stanza = stanza }); session.log("debug", "mod_smacks delivery/failure returning %s for offline-handled stanza", tostring(ok)); return ok; -- if stanza was handled, don't send an error end end end end); module:hook("pre-resource-unbind", function (event) local session = event.session; if not session.smacks then return end if not session.resumption_token then local queue = session.outgoing_stanza_queue; if queue:count_unacked() > 0 then session.log("debug", "Destroying session with %d unacked stanzas", queue:count_unacked()); handle_unacked_stanzas(session); end return end if session.hibernating then return end session.hibernating = os_time(); if session.hibernating_watchdog then session.log("debug", "Session already has a sleeping watchdog, replacing it"); session.hibernating_watchdog:cancel(); end session.hibernating_watchdog = watchdog.new(resume_timeout, function(this_dog) if this_dog ~= session.hibernating_watchdog then -- This really shouldn't happen? session.log("debug", "Releasing a stray watchdog"); return end session.log("debug", "mod_smacks hibernation timeout reached..."); if session.destroyed then session.log("debug", "The session has already been destroyed"); return elseif not session.resumption_token then -- This should normally not happen, the watchdog should be canceled from session:close() session.log("debug", "The session has already been resumed or replaced"); return 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; c2s_sessions[conn] = nil; session.conn = nil; conn:close(); end session.log("debug", "Session going into hibernation (not being destroyed)") module:fire_event("smacks-hibernation-start", { origin = session; queue = session.outgoing_stanza_queue:table() }); return true; -- Postpone destruction for now end); local function handle_s2s_destroyed(event) local session = event.session; local queue = session.outgoing_stanza_queue; if queue and queue:count_unacked() > 0 then session.log("warn", "Destroying session with %d unacked stanzas", queue:count_unacked()); if s2s_resend then for stanza in queue:consume() do module:send(stanza); end session.outgoing_stanza_queue = nil; else handle_unacked_stanzas(session); end end end module:hook("s2sout-destroyed", handle_s2s_destroyed); module:hook("s2sin-destroyed", handle_s2s_destroyed); function do_resume(session, stanza) if session.full_jid then session.log("warn", "Tried to resume after resource binding"); return nil, enable_errors.new("already_bound"); end local id = stanza.attr.previd; 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); clear_old_session(session, id); resumption_expired(1); 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 if original_session.hibernating_watchdog then original_session.log("debug", "Letting the watchdog go"); original_session.hibernating_watchdog:cancel(); original_session.hibernating_watchdog = nil; elseif session.hibernating then original_session.log("error", "Hibernating session has no watchdog!") end -- zero age = was not hibernating yet local age = 0; if original_session.hibernating then local now = os_time(); age = now - original_session.hibernating; end session.log("debug", "mod_smacks resuming existing session %s...", original_session.id); local queue = original_session.outgoing_stanza_queue; local h = tonumber(stanza.attr.h); original_session.log("debug", "Pre-resumption #queue = %d", queue:count_unacked()) local acked, err = ack_errors.coerce(queue:ack(h)); -- luacheck: ignore 211/acked if not err and not queue:resumable() then err = ack_errors.new("overflow"); end if err then session.log("debug", "Resumption failed: %s", err); return nil, err; end -- Update original_session with the parameters (connection, etc.) from the new session sessionmanager.update_session(original_session, session); 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); -- Events when it's sensible to request an ack -- Could experiment with forcing (ignoring max_unacked) , but when and why? local request_ack_events = { ["csi-client-active"] = true; ["csi-flushing"] = false; ["c2s-pre-ondrain"] = false; ["s2s-pre-ondrain"] = false; }; for event_name, force in pairs(request_ack_events) do module:hook(event_name, function(event) local session = event.session or event.origin; request_ack_now_if_needed(session, force, event_name); end); end local function handle_read_timeout(event) local session = event.session; if session.smacks then if session.awaiting_ack then if session.awaiting_ack_timer then timer.stop(session.awaiting_ack_timer); session.awaiting_ack_timer = nil; end if session.delayed_ack_timer then timer.stop(session.delayed_ack_timer); session.delayed_ack_timer = nil; end return false; -- Kick the session end request_ack_now_if_needed(session, true, "read timeout"); return true; end end module:hook("s2s-read-timeout", handle_read_timeout); module:hook("c2s-read-timeout", handle_read_timeout); module:hook_global("server-stopping", function(event) if not local_sessions then -- not a VirtualHost, no user sessions return end local reason = event.reason; -- Close smacks-enabled sessions ourselves instead of letting mod_c2s close -- it, which invalidates the smacks session. This allows preserving the -- counter value, so it can be communicated to the client when it tries to -- resume the lost session after a restart. for _, user in pairs(local_sessions) do for _, session in pairs(user.sessions) do if session.resumption_token then if save_old_session(session) then session.resumption_token = nil; -- Deal with unacked stanzas if session.outgoing_stanza_queue then handle_unacked_stanzas(session); end if session.conn then session.conn:close() session.conn = nil; -- Now when mod_c2s gets here, it will immediately destroy the -- session since it is unconnected. end -- And make sure nobody tries to send anything session:close{ condition = "system-shutdown", text = reason }; end end end end end, -90); prosody-13.0.1/plugins/PaxHeaders/mod_stanza_debug.lua0000644000000000000000000000011714773555365020071 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.647710757 prosody-13.0.1/plugins/mod_stanza_debug.lua0000644000175000017500000000116314773555365022271 0ustar00prosodyprosody00000000000000module:set_global(); local filters = require "prosody.util.filters"; local function log_send(t, session) if t and t ~= "" and t ~= " " then session.log("debug", "SEND: %s", t); end return t; end local function log_recv(t, session) if t and t ~= "" and t ~= " " then session.log("debug", "RECV: %s", t); end return t; end local function init_raw_logging(session) filters.add_filter(session, "stanzas/in", log_recv, -10000); filters.add_filter(session, "stanzas/out", log_send, 10000); end filters.add_filter_hook(init_raw_logging); function module.unload() filters.remove_filter_hook(init_raw_logging); end prosody-13.0.1/plugins/PaxHeaders/mod_storage_internal.lua0000644000000000000000000000011714773555365020763 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.647710757 prosody-13.0.1/plugins/mod_storage_internal.lua0000644000175000017500000002711214773555365023165 0ustar00prosodyprosody00000000000000local 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 uuid_v7 = require "prosody.util.uuid".v7; 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_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 = {}; function driver:open(store, typ) local mt = self[typ or "keyval"] if not mt then return nil, "unsupported-store"; end return setmetatable({ store = store, type = typ }, mt); end function driver:stores(username) -- luacheck: ignore 212/self return datamanager.stores(username, host); end function driver:purge(user) -- luacheck: ignore 212/self return datamanager.purge(user, host); end local keyval = { }; driver.keyval = { __index = keyval }; function keyval:get(user) return datamanager.load(user, host, self.store); end function keyval:set(user, data) return datamanager.store(user, host, self.store, data); end function keyval:users() return datamanager.users(host, self.store, self.type); end local archive = {}; driver.archive = { __index = archive }; archive.caps = { total = true; quota = archive_item_limit; truncate = true; full_id_range = true; ids = true; }; function archive:append(username, key, value, when, with) when = when or now(); if not st.is_stanza(value) then return nil, "unsupported-datatype"; end value = st.preserialize(st.clone(value)); value.when = when; value.with = with; value.attr.stamp = datetime.datetime(when); local cache_key = jid_join(username, host, self.store); local item_count = archive_item_count_cache:get(cache_key); if key then local items, err = datamanager.list_load(username, host, self.store); if not items and err then return items, err; end -- Check the quota item_count = items and #items or 0; archive_item_count_cache:set(cache_key, item_count); if item_count >= archive_item_limit then module:log("debug", "%s reached or over quota, not adding to store", username); return nil, "quota-limit"; end if items then -- Filter out any item with the same key as the one being added items = array(items); items:filter(function (item) return item.key ~= key; end); value.key = key; items:push(value); local ok, err = datamanager.list_store(username, host, self.store, items); if not ok then return ok, err; end archive_item_count_cache:set(cache_key, #items); return key; end else if not item_count then -- Item count not cached? -- We need to load the list to get the number of items currently stored local items, err = datamanager.list_load(username, host, self.store); if not items and err then return items, err; end item_count = items and #items or 0; archive_item_count_cache:set(cache_key, item_count); end if item_count >= archive_item_limit then module:log("debug", "%s reached or over quota, not adding to store", username); return nil, "quota-limit"; end key = uuid_v7(); end module:log("debug", "%s has %d items out of %d limit in store %s", username, item_count, archive_item_limit, self.store); value.key = key; local ok, err = datamanager.list_append(username, host, self.store, value); if not ok then return ok, err; end archive_item_count_cache:set(cache_key, item_count+1); 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 list, err = datamanager.list_open(username, host, self.store); if not list then if err then 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; end end return function() end; end local i = 0; local iter = function() i = i + 1; return list[i] end if query then 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 iter = it.filter(function(item) return item.key == query.key; end, iter); end if query.ids then local ids = set.new(query.ids); iter = it.filter(function(item) return ids:contains(item.key); end, iter); end if query.with then iter = it.filter(function(item) return item.with == query.with; end, iter); end if query.start then 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 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 end if query.after then local found = false; iter = it.filter(function(item) local found_after = found; if item.key == query.after then found = true end return found_after; end, iter); end 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() local item = iter(); if item == nil then if list.close then list:close(); end return end 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; -- COMPAT Stored data may still contain legacy XEP-0091 timestamp item.attr.stamp_legacy = nil; item = st.deserialize(item); return key, item, when, with; end end function archive:get(username, wanted_key) local iter, err = self:find(username, { key = wanted_key }) if not iter then return iter, err; end for key, stanza, when, with in iter do if key == wanted_key then return stanza, when, with; end end return nil, "item-not-found"; end function archive:set(username, key, new_value, new_when, new_with) local items, err = datamanager.list_load(username, host, self.store); if not items then if err then return items, err; else return nil, "item-not-found"; end end for i = 1, #items do local old_item = items[i]; if old_item.key == key then local item = st.preserialize(st.clone(new_value)); local when = new_when or old_item.when or datetime.parse(old_item.attr.stamp); item.key = key; item.when = when; item.with = new_with or old_item.with; item.attr.stamp = datetime.datetime(when); items[i] = item; return datamanager.list_store(username, host, self.store, items); end end return nil, "item-not-found"; end function archive:dates(username) local items, err = datamanager.list_load(username, host, self.store); if not items then return items, err; end return array(items):pluck("when"):map(datetime.date):unique(); end function archive:summary(username, query) local iter, err = self:find(username, query) if not iter then return iter, err; end local counts = {}; local earliest = {}; local latest = {}; local body = {}; for _, stanza, when, with in iter do counts[with] = (counts[with] or 0) + 1; if earliest[with] == nil then earliest[with] = when; end latest[with] = when; body[with] = stanza:get_child_text("body") or body[with]; end return { counts = counts; earliest = earliest; latest = latest; body = body; }; end 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); -- 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 return items, err; end archive_item_count_cache:set(cache_key, 0); -- Store is empty return 0; end items = array(items); local count_before = #items; if query then if query.key then items:filter(function (item) return item.key ~= query.key; end); end if query.with then items:filter(function (item) return item.with ~= query.with; end); end if query.start then items:filter(function (item) return item.when < query.start; end); end if query["end"] then items:filter(function (item) return item.when > query["end"]; end); end if query.truncate and #items > query.truncate then if query.reverse then -- Before: { 1, 2, 3, 4, 5, } -- After: { 1, 2, 3 } for i = #items, query.truncate + 1, -1 do items[i] = nil; end else -- Before: { 1, 2, 3, 4, 5, } -- After: { 3, 4, 5 } local offset = #items - query.truncate; for i = 1, #items do items[i] = items[i+offset]; end end end end local count = count_before - #items; if count == 0 then return 0; -- No changes, skip write end local ok, err = datamanager.list_store(username, host, self.store, items); if not ok then return ok, err; end archive_item_count_cache:set(cache_key, #items); return count; end module:provides("storage", driver); prosody-13.0.1/plugins/PaxHeaders/mod_storage_memory.lua0000644000000000000000000000011714773555365020457 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.647710757 prosody-13.0.1/plugins/mod_storage_memory.lua0000644000175000017500000002007314773555365022660 0ustar00prosodyprosody00000000000000local 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 "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_integer("storage_archive_item_limit", 1000, 0); local memory = setmetatable({}, { __index = function(t, k) local store = module:shared(k) t[k] = store; return store; end }); local function NULL() return nil end local function _purge_store(self, username) self.store[username or NULL] = nil; return true; end local function _users(self) return next, self.store, nil; end local keyval_store = {}; keyval_store.__index = keyval_store; function keyval_store:get(username) return (self.store[username or NULL] or NULL)(); end function keyval_store:set(username, data) if data ~= nil then data = envload("return "..serialize(data), "=(data)", {}); end self.store[username or NULL] = data; return true; end keyval_store.purge = _purge_store; keyval_store.users = _users; local archive_store = {}; archive_store.__index = archive_store; archive_store.users = _users; archive_store.caps = { total = true; quota = archive_item_limit; truncate = true; full_id_range = true; ids = true; }; function archive_store:append(username, key, value, when, with) if is_stanza(value) then value = st.preserialize(value); value = envload("return xml"..serialize(value), "=(stanza)", { xml = st.deserialize }) else value = envload("return "..serialize(value), "=(data)", {}); end local a = self.store[username or NULL]; if not a then a = {}; self.store[username or NULL] = a; end local v = { key = key, when = when, with = with, value = value }; if not key then key = new_id(); v.key = key; end if a[key] then table.remove(a, a[key]); elseif #a >= archive_item_limit then return nil, "quota-limit"; end local i = #a+1; a[i] = v; a[key] = i; return key; end function archive_store:find(username, query) local items = self.store[username or NULL]; if not items then if query then if query.before or query.after then return nil, "item-not-found"; end if query.total then return function () end, 0; end end return function () end; end local count = nil; local i, last_key = 0; if query then items = array():append(items); if query.key then items:filter(function (item) return item.key == query.key; end); end if query.ids then local ids = set.new(query.ids); items:filter(function (item) return ids:contains(item.key); end); end if query.with then items:filter(function (item) return item.with == query.with; end); end if query.start then items:filter(function (item) return item.when >= query.start; end); end if query["end"] then items:filter(function (item) return item.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"; end end last_key = query.after; elseif 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; end end if not found then return nil, "item-not-found"; end last_key = query.before; elseif query.before then last_key = query.before; end if query.limit and #items - i > query.limit then items[i+query.limit+1] = nil; end end return function () i = i + 1; local item = items[i]; if not item or (last_key and item.key == last_key) then return; end return item.key, item.value(), item.when, item.with; end, count; end function archive_store:get(username, wanted_key) local items = self.store[username or NULL]; if not items then return nil, "item-not-found"; end local i = items[wanted_key]; if not i then return nil, "item-not-found"; end local item = items[i]; return item.value(), item.when, item.with; end function archive_store:set(username, wanted_key, new_value, new_when, new_with) local items = self.store[username or NULL]; if not items then return nil, "item-not-found"; end local i = items[wanted_key]; if not i then return nil, "item-not-found"; end local item = items[i]; if is_stanza(new_value) then new_value = st.preserialize(new_value); item.value = envload("return xml"..serialize(new_value), "=(stanza)", { xml = st.deserialize }) else item.value = envload("return "..serialize(new_value), "=(data)", {}); end if new_when then item.when = new_when; end if new_with then item.with = new_when; end return true; end function archive_store:summary(username, query) local iter, err = self:find(username, query) if not iter then return iter, err; end local counts = {}; local earliest = {}; local latest = {}; for _, _, when, with in iter do counts[with] = (counts[with] or 0) + 1; if earliest[with] == nil then earliest[with] = when; end latest[with] = when; end return { counts = counts; earliest = earliest; latest = latest; }; end function archive_store:delete(username, query) if not query or next(query) == nil then self.store[username or NULL] = nil; return true; end local items = self.store[username or NULL]; if not items then -- Store is empty return 0; end items = array(items); local count_before = #items; if query then if query.key then items:filter(function (item) return item.key ~= query.key; end); end if query.with then items:filter(function (item) return item.with ~= query.with; end); end if query.start then items:filter(function (item) return item.when < query.start; end); end if query["end"] then items:filter(function (item) return item.when > query["end"]; end); end if query.truncate and #items > query.truncate then if query.reverse then -- Before: { 1, 2, 3, 4, 5, } -- After: { 1, 2, 3 } for i = #items, query.truncate + 1, -1 do items[i] = nil; end else -- Before: { 1, 2, 3, 4, 5, } -- After: { 3, 4, 5 } local offset = #items - query.truncate; for i = 1, #items do items[i] = items[i+offset]; end end end end local count = count_before - #items; if count == 0 then return 0; -- No changes, skip write end setmetatable(items, nil); do -- re-index by key for k in pairs(items) do if type(k) == "string" then items[k] = nil; end end for i = 1, #items do items[ items[i].key ] = i; end end return count; end archive_store.purge = _purge_store; local stores = { keyval = keyval_store; archive = archive_store; } local driver = {}; function driver:open(store, typ) -- luacheck: ignore 212/self local store_mt = stores[typ or "keyval"]; if store_mt then return setmetatable({ store = memory[store] }, store_mt); end return nil, "unsupported-store"; end function driver:purge(user) -- luacheck: ignore 212/self for _, store in pairs(memory) do store[user] = nil; end end if auto_purge_enabled then module:hook("resource-unbind", function (event) local user_bare_jid = event.session.username.."@"..event.session.host; if not prosody.bare_sessions[user_bare_jid] then -- User went offline module:log("debug", "Clearing store for offline user %s", user_bare_jid); local f, s, v; if auto_purge_stores:empty() then f, s, v = pairs(memory); else f, s, v = auto_purge_stores:items(); end for store_name in f, s, v do if memory[store_name] then memory[store_name][event.session.username] = nil; end end end end); end module:provides("storage", driver); prosody-13.0.1/plugins/PaxHeaders/mod_storage_none.lua0000644000000000000000000000011714773555365020106 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.647710757 prosody-13.0.1/plugins/mod_storage_none.lua0000644000175000017500000000127314773555365022310 0ustar00prosodyprosody00000000000000-- luacheck: ignore 212 local driver = {}; local driver_mt = { __index = driver }; function driver:open(store, typ) if typ and typ ~= "keyval" and typ ~= "archive" then return nil, "unsupported-store"; end return setmetatable({ store = store, type = typ }, driver_mt); end function driver:get(user) return {}; end function driver:set(user, data) return nil, "Storage disabled"; end function driver:stores(username) return { "roster" }; end function driver:purge(user) return true; end function driver:append() return nil, "Storage disabled"; end function driver:find() return function () end, 0; end function driver:delete() return true; end module:provides("storage", driver); prosody-13.0.1/plugins/PaxHeaders/mod_storage_sql.lua0000644000000000000000000000011714773555365017746 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.651710774 prosody-13.0.1/plugins/mod_storage_sql.lua0000644000175000017500000010146214773555365022151 0ustar00prosodyprosody00000000000000 -- luacheck: ignore 212/self 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: %s", dbisql) dbisql = nil; end if not have_sqlite then module:log("debug", "Could not load LuaSQLite3: %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; local function iterator(result) return function(result_) local row = result_(); if row ~= nil then return unpack(row); end end, result, nil; end -- COMPAT Support for UPSERT is not in all versions of all compatible databases. local function has_upsert(engine) if engine.params.driver == "SQLite3" then -- SQLite3 >= 3.24.0 return engine.sqlite_version and (engine.sqlite_version[2] or 0) >= 24; elseif engine.params.driver == "PostgreSQL" then -- PostgreSQL >= 9.5 -- Versions without support have long since reached end of life. return true; end -- We don't support UPSERT on MySQL/MariaDB, they seem to have a completely different syntax, uncertaint from which versions. return false end local default_params = { driver = "SQLite3" }; local engine; local function serialize(value) local t = type(value); if t == "string" or t == "boolean" or t == "number" then return t, tostring(value); elseif is_stanza(value) then return "xml", tostring(value); elseif t == "table" then local encoded,err = json.encode(value); if encoded then return "json", encoded; end return nil, err; end return nil, "Unhandled value type: "..t; end local function deserialize(t, value) if t == "string" then return value; elseif t == "boolean" then if value == "true" then return true; elseif value == "false" then return false; end return nil, "invalid-boolean"; elseif t == "number" then value = tonumber(value); if value then return value; end return nil, "invalid-number"; elseif t == "json" then return json.decode(value); elseif t == "xml" then return xml_parse(value); end return nil, "Unhandled value type: "..t; end local host = module.host; local function keyval_store_get(user, store) local haveany; local result = {}; local select_sql = [[ SELECT "key","type","value" FROM "prosody" WHERE "host"=? AND "user"=? AND "store"=?; ]] for row in engine:select(select_sql, host, user or "", store) do haveany = true; local k = row[1]; local v, e = deserialize(row[2], row[3]); assert(v ~= nil, e); if k and v then if k ~= "" then result[k] = v; elseif type(v) == "table" then for a,b in pairs(v) do result[a] = b; end end end end if haveany then return result; end end local function keyval_store_set(data, user, store) local delete_sql = [[ DELETE FROM "prosody" WHERE "host"=? AND "user"=? AND "store"=? ]]; engine:delete(delete_sql, host, user or "", store); local insert_sql = [[ INSERT INTO "prosody" ("host","user","store","key","type","value") VALUES (?,?,?,?,?,?); ]] if data and next(data) ~= nil then local extradata = {}; for key, value in pairs(data) do if type(key) == "string" and key ~= "" then local t, encoded_value = assert(serialize(value)); engine:insert(insert_sql, host, user or "", store, key, t, encoded_value); else extradata[key] = value; end end if next(extradata) ~= nil then local t, encoded_extradata = assert(serialize(extradata)); engine:insert(insert_sql, host, user or "", store, "", t, encoded_extradata); end end return true; end --- Key/value store API (default store type) local keyval_store = {}; keyval_store.__index = keyval_store; function keyval_store:get(username) 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", self.store, username or "", result); return nil, result; end return result; end function keyval_store:set(username, data) return engine:transaction(keyval_store_set, data, username, self.store); end function keyval_store:users() local ok, result = engine:transaction(function() local select_sql = [[ SELECT DISTINCT "user" FROM "prosody" WHERE "host"=? AND "store"=?; ]]; return engine:select(select_sql, host, self.store); end); if not ok then error(result); end return iterator(result); end --- Archive store API 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") -- luacheck: ignore 512 431/user 431/store 431/err local map_store = {}; map_store.__index = map_store; map_store.remove = {}; function map_store:get(username, key) local ok, result = engine:transaction(function() local query = [[ SELECT "type", "value" FROM "prosody" WHERE "host"=? AND "user"=? AND "store"=? AND "key"=? LIMIT 1 ]]; local data, err; if type(key) == "string" and key ~= "" then for row in engine:select(query, host, username or "", self.store, key) do data, err = deserialize(row[1], row[2]); assert(data ~= nil, err); end return data; else for row in engine:select(query, host, username or "", self.store, "") do data, err = deserialize(row[1], row[2]); assert(data ~= nil, err); end return data and data[key] or nil; end end); if not ok then return nil, result; end return result; end function map_store:set(username, key, data) if data == nil then data = self.remove; end return self:set_keys(username, { [key] = data }); end function map_store:set_keys(username, keydatas) local ok, result = engine:transaction(function() local delete_sql = [[ DELETE FROM "prosody" WHERE "host"=? AND "user"=? AND "store"=? AND "key"=?; ]]; local insert_sql = [[ INSERT INTO "prosody" ("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" WHERE "host"=? AND "user"=? AND "store"=? AND "key"=? LIMIT 1; ]]; for key, data in pairs(keydatas) do if type(key) == "string" and key ~= "" and has_upsert(engine) 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 local t, value = assert(serialize(data)); engine:insert(insert_sql, host, username or "", self.store, key, t, value); end else local extradata, err = {}; for row in engine:select(select_extradata_sql, host, username or "", self.store, "") do extradata, err = deserialize(row[1], row[2]); assert(extradata ~= nil, err); end engine:delete(delete_sql, host, username or "", self.store, ""); extradata[key] = data; local t, value = assert(serialize(extradata)); engine:insert(insert_sql, host, username or "", self.store, "", t, value); end end return true; end); if not ok then return nil, result; end return result; end function map_store:get_all(key) if type(key) ~= "string" or key == "" then return nil, "get_all only supports non-empty string keys"; end local ok, result = engine:transaction(function() local query = [[ SELECT "user", "type", "value" FROM "prosody" WHERE "host"=? AND "store"=? AND "key"=? ]]; local data; for row in engine:select(query, host, self.store, key) do local key_data, err = deserialize(row[2], row[3]); assert(key_data ~= nil, err); if data == nil then data = {}; end data[row[1]] = key_data; end return data; end); if not ok then return nil, result; end return result; end function map_store:delete_all(key) if type(key) ~= "string" or key == "" then return nil, "delete_all only supports non-empty string keys"; end local ok, result = engine:transaction(function() local delete_sql = [[ DELETE FROM "prosody" WHERE "host"=? AND "store"=? AND "key"=?; ]]; engine:delete(delete_sql, host, self.store, key); return true; end); if not ok then return nil, result; end return result; end local archive_store = {} archive_store.caps = { total = true; quota = archive_item_limit; truncate = true; full_id_range = true; ids = true; wildcard_delete = true; }; archive_store.__index = archive_store 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 archive_item_limit then if not item_count then item_count_cache_miss(); local ok, ret = engine:transaction(function() local count_sql = [[ SELECT COUNT(*) FROM "prosodyarchive" WHERE "host"=? AND "user"=? AND "store"=?; ]]; local result = engine:select(count_sql, host, user, store); if result then for row in result do item_count = row[1]; end end end); if not ok or not item_count then module:log("error", "Failed while checking quota for %s: %s", username, ret); return nil, "Failure while checking quota"; end archive_item_count_cache:set(cache_key, item_count); else item_count_cache_hit(); end module:log("debug", "%s has %d items out of %d limit", username, item_count, archive_item_limit); if item_count >= archive_item_limit then return nil, "quota-limit"; end end -- FIXME update the schema to allow precision timestamps when = when or os.time(); if engine.params.driver ~= "SQLite3" then -- SQLite3 doesn't enforce types :) when = math.floor(when); end with = with or ""; local ok, ret = engine:transaction(function() local delete_sql = [[ DELETE FROM "prosodyarchive" WHERE "host"=? AND "user"=? AND "store"=? AND "key"=?; ]]; local insert_sql = [[ INSERT INTO "prosodyarchive" ("host", "user", "store", "when", "with", "key", "type", "value") VALUES (?,?,?,?,?,?,?,?); ]]; if key then -- TODO use UPSERT like map store local result = engine:delete(delete_sql, host, user or "", store, key); if result and item_count then item_count = item_count - result:affected(); end else 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); 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 return ret; -- the key end -- Helpers for building the WHERE clause local function archive_where(query, args, where) -- Time range, inclusive if query.start then args[#args+1] = math.floor(query.start); where[#where+1] = "\"when\" >= ?" end if query["end"] then args[#args+1] = math.floor(query["end"]); if query.start then where[#where] = "\"when\" BETWEEN ? AND ?" -- is this inclusive? else where[#where+1] = "\"when\" <= ?" end end -- Related name if query.with then where[#where+1] = "\"with\" = ?"; args[#args+1] = query.with end -- Unique id if query.key then where[#where+1] = "\"key\" = ?"; args[#args+1] = query.key end -- Set of ids if query.ids then local nids, nargs = #query.ids, #args; where[#where + 1] = "\"key\" IN (" .. string.rep("?", nids, ",") .. ")"; for i, id in ipairs(query.ids) do args[nargs+i] = id; end end end local function archive_where_id_range(query, args, where) -- Before or after specific item, exclusive local id_lookup_sql = [[ SELECT "sort_id" FROM "prosodyarchive" WHERE "key" = ? AND "host" = ? AND "user" = ? AND "store" = ? LIMIT 1; ]]; if query.after then -- keys better be unique! local after_id = nil; for row in engine:select(id_lookup_sql, query.after, args[1], args[2], args[3]) do after_id = row[1]; end if not after_id then return nil, "item-not-found"; end where[#where+1] = '"sort_id" > ?'; args[#args+1] = after_id; end if query.before then local before_id = nil; for row in engine:select(id_lookup_sql, query.before, args[1], args[2], args[3]) do before_id = row[1]; end if not before_id then return nil, "item-not-found"; end where[#where+1] = '"sort_id" < ?'; args[#args+1] = before_id; end return true; end function archive_store:find(username, query) query = query or {}; local user,store = username,self.store; local cache_key = jid_join(username, host, self.store); local total = archive_item_count_cache:get(cache_key); (total and item_count_cache_hit or item_count_cache_miss)(); if query.start == nil and query.with == nil and query["end"] == nil and query.key == nil and query.ids == nil then -- the query is for the whole archive, so a cached 'total' should be a -- relatively accurate response if that's all that is requested if total ~= nil and query.limit == 0 then return noop, total; end else -- not usable, so refresh it later if needed total = nil; end local ok, result, err = engine:transaction(function() local sql_query = [[ SELECT "key", "type", "value", "when", "with" FROM "prosodyarchive" WHERE %s ORDER BY "sort_id" %s%s; ]]; local args = { host, user or "", store, }; local where = { "\"host\" = ?", "\"user\" = ?", "\"store\" = ?", }; archive_where(query, args, where); -- Total matching if query.total and not total then local stats = engine:select("SELECT COUNT(*) FROM \"prosodyarchive\" WHERE " .. t_concat(where, " AND "), unpack(args)); if stats then for row in stats do total = row[1]; end end if query.start == nil and query.with == nil and query["end"] == nil and query.key == nil and query.ids == nil then archive_item_count_cache:set(cache_key, total); end if query.limit == 0 then -- Skip the real query return noop, total; end end local ok, err = archive_where_id_range(query, args, where); if not ok then return ok, err; end sql_query = sql_query:format(t_concat(where, " AND "), query.reverse and "DESC" or "ASC", query.limit and " LIMIT " .. query.limit or ""); return engine:select(sql_query, unpack(args)); end); if not ok then return ok, result; end if not result then return nil, err; end return function() local row = result(); if row ~= nil then local value, err = deserialize(row[2], row[3]); assert(value ~= nil, err); return row[1], value, row[4], row[5]; end end, total; end function archive_store:get(username, key) local iter, err = self:find(username, { key = key }) if not iter then return iter, err; end for _, stanza, when, with in iter do return stanza, when, with; end return nil, "item-not-found"; end function archive_store:set(username, key, new_value, new_when, new_with) local user,store = username,self.store; local ok, result = engine:transaction(function () local update_query = [[ UPDATE "prosodyarchive" SET %s WHERE %s ]]; local args = { host, user or "", store, key }; local setf = {}; local where = { "\"host\" = ?", "\"user\" = ?", "\"store\" = ?", "\"key\" = ?"}; if new_value then table.insert(setf, '"type" = ?') table.insert(setf, '"value" = ?') local t, value = serialize(new_value); table.insert(args, 1, t); table.insert(args, 2, value); end if new_when then table.insert(setf, 1, '"when" = ?') table.insert(args, 1, new_when); end if new_with then table.insert(setf, 1, '"with" = ?') table.insert(args, 1, new_with); end update_query = update_query:format(t_concat(setf, ", "), t_concat(where, " AND ")); return engine:update(update_query, unpack(args)); end); if not ok then return ok, result; end return result:affected() == 1; end function archive_store:summary(username, query) query = query or {}; local user,store = username,self.store; local ok, result = engine:transaction(function() local sql_query = [[ SELECT DISTINCT "with", COUNT(*), MIN("when"), MAX("when") FROM "prosodyarchive" WHERE %s GROUP BY "with"; ]]; local args = { host, user or "", store, }; local where = { "\"host\" = ?", "\"user\" = ?", "\"store\" = ?", }; archive_where(query, args, where); archive_where_id_range(query, args, where); if query.limit then args[#args+1] = query.limit; end sql_query = sql_query:format(t_concat(where, " AND ")); return engine:select(sql_query, unpack(args)); end); if not ok then return ok, result end local counts = {}; local earliest, latest = {}, {}; for row in result do local with, count = row[1], row[2]; counts[with] = count; earliest[with] = row[3]; latest[with] = row[4]; end return { counts = counts; earliest = earliest; latest = latest; }; end function archive_store:delete(username, query) query = query or {}; local user,store = username,self.store; local ok, stmt = engine:transaction(function() local sql_query = "DELETE FROM \"prosodyarchive\" WHERE %s;"; local args = { host, user or "", store, }; local where = { "\"host\" = ?", "\"user\" = ?", "\"store\" = ?", }; if user == true then table.remove(args, 2); table.remove(where, 2); end archive_where(query, args, where); local ok, err = archive_where_id_range(query, args, where); if not ok then return ok, err; end if query.truncate == nil then sql_query = sql_query:format(t_concat(where, " AND ")); elseif engine.params.driver == "MySQL" then sql_query = [[ DELETE result FROM prosodyarchive AS result JOIN ( SELECT sort_id FROM prosodyarchive WHERE %s ORDER BY "sort_id" %s LIMIT 18446744073709551615 OFFSET %s ) AS limiter on result.sort_id = limiter.sort_id;]]; sql_query = string.format(sql_query, t_concat(where, " AND "), query.reverse and "ASC" or "DESC", query.truncate); else args[#args+1] = query.truncate; local unlimited = "ALL"; sql_query = [[ DELETE FROM "prosodyarchive" WHERE "sort_id" IN ( SELECT "sort_id" FROM "prosodyarchive" WHERE %s ORDER BY "sort_id" %s LIMIT %s OFFSET ? );]]; if engine.params.driver == "SQLite3" then if engine.sqlite_compile_options.enable_update_delete_limit then sql_query = [[ DELETE FROM "prosodyarchive" WHERE %s ORDER BY "sort_id" %s LIMIT %s OFFSET ?; ]]; end unlimited = "-1"; end sql_query = string.format(sql_query, t_concat(where, " AND "), query.reverse and "ASC" or "DESC", unlimited); end return engine:delete(sql_query, unpack(args)); end); if username == true then archive_item_count_cache:clear(); else local cache_key = jid_join(username, host, self.store); 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 function archive_store:users() local ok, result = engine:transaction(function() local select_sql = [[ SELECT DISTINCT "user" FROM "prosodyarchive" WHERE "host"=? AND "store"=?; ]]; return engine:select(select_sql, host, self.store); end); if not ok then error(result); end 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 -- FIXME: Some of these operations need to operate on the archive store(s) too local driver = {}; function driver:open(store, typ) local store_mt = stores[typ or "keyval"]; if store_mt then return setmetatable({ store = store }, store_mt); end return nil, "unsupported-store"; end function driver:stores(username) local query = "SELECT DISTINCT \"store\" FROM \"prosody\" WHERE \"host\"=? AND \"user\"" .. (username == true and "!=?" or "=?"); if username == true or not username then username = ""; end local ok, result = engine:transaction(function() return engine:select(query, host, username); end); if not ok then return ok, result end return iterator(result); end function driver:purge(username) return engine:transaction(function() engine:delete("DELETE FROM \"prosody\" WHERE \"host\"=? AND \"user\"=?", host, username); engine:delete("DELETE FROM \"prosodyarchive\" WHERE \"host\"=? AND \"user\"=?", host, username); end); end --- Initialization 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 { name = "prosody"; Column { name="host", type="TEXT", nullable=false }; Column { name="user", type="TEXT", nullable=false }; Column { name="store", type="TEXT", nullable=false }; Column { name="key", type="TEXT", nullable=false }; Column { name="type", type="TEXT", nullable=false }; Column { name="value", type="MEDIUMTEXT", nullable=false }; Index { name = "prosody_unique_index"; unique = engine.params.driver ~= "MySQL"; "host"; "user"; "store"; "key" }; }; engine:transaction(function() ProsodyTable:create(engine); end); local ProsodyArchiveTable = Table { name="prosodyarchive"; Column { name="sort_id", type="INTEGER", primary_key=true, auto_increment=true }; Column { name="host", type="TEXT", nullable=false }; Column { name="user", type="TEXT", nullable=false }; Column { name="store", type="TEXT", nullable=false }; Column { name="key", type="TEXT", nullable=false }; -- item id Column { name="when", type="INTEGER", nullable=false }; -- timestamp Column { name="with", type="TEXT", nullable=false }; -- related id Column { name="type", type="TEXT", nullable=false }; Column { name="value", type="MEDIUMTEXT", nullable=false }; Index { name="prosodyarchive_index", unique = engine.params.driver ~= "MySQL", "host", "user", "store", "key" }; Index { name="prosodyarchive_with_when", "host", "user", "store", "with", "when" }; Index { name="prosodyarchive_when", "host", "user", "store", "when" }; Index { name="prosodyarchive_sort", "host", "user", "store", "sort_id" }; }; engine:transaction(function() ProsodyArchiveTable:create(engine); end); 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'")); if result:rowcount() > 0 then changes = true; if apply_changes then module:log("info", "Upgrading database schema (value column size)..."); assert(engine:execute("ALTER TABLE \"prosody\" MODIFY COLUMN \"value\" MEDIUMTEXT")); module:log("info", "Database table automatically upgraded"); end end end do -- Ensure index is not unique (issue #1073) local result = assert(engine:execute([[SHOW INDEX FROM prosodyarchive WHERE key_name='prosodyarchive_index' and non_unique=0]])); if result:rowcount() > 0 then changes = true; if apply_changes then module:log("info", "Upgrading database schema (prosodyarchive_index)..."); assert(engine:execute[[ALTER TABLE "prosodyarchive" DROP INDEX prosodyarchive_index;]]); local new_index = sql.Index { table = "prosodyarchive", name="prosodyarchive_index", "host", "user", "store", "key" }; assert(engine:_create_index(new_index)); module:log("info", "Database table automatically upgraded"); end end end return true; end); if not success then module:log("error", "Failed to check/upgrade database schema (%s), please see " .."https://prosody.im/doc/mysql for help", err or "unknown error"); return false; end -- COMPAT w/pre-0.10: Upgrade table to UTF-8 if not already local check_encoding_query = [[ SELECT "COLUMN_NAME","COLUMN_TYPE","TABLE_NAME" FROM "information_schema"."columns" WHERE "TABLE_NAME" LIKE 'prosody%%' AND "TABLE_SCHEMA" = ? AND ( "CHARACTER_SET_NAME"!=? OR "COLLATION_NAME"!=?); ]]; -- FIXME Is it ok to ignore the return values from this? engine:transaction(function() local result = assert(engine:execute(check_encoding_query, params.database, engine.charset, engine.charset.."_bin")); local n_bad_columns = result:rowcount(); if n_bad_columns > 0 then changes = true; if apply_changes then module:log("warn", "Found %d columns in prosody table requiring encoding change, updating now...", n_bad_columns); local fix_column_query1 = "ALTER TABLE \"%s\" CHANGE \"%s\" \"%s\" BLOB;"; local fix_column_query2 = "ALTER TABLE \"%s\" CHANGE \"%s\" \"%s\" %s CHARACTER SET '%s' COLLATE '%s_bin';"; for row in result:rows() do local column_name, column_type, table_name = unpack(row); module:log("debug", "Fixing column %s in table %s", column_name, table_name); engine:execute(fix_column_query1:format(table_name, column_name, column_name)); engine:execute(fix_column_query2:format(table_name, column_name, column_name, column_type, engine.charset, engine.charset)); end module:log("info", "Database encoding upgrade complete!"); end end end); 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 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 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 end end return changes; end local function normalize_database(driver, database) -- luacheck: ignore 431/driver if driver == "SQLite3" and database ~= ":memory:" then return resolve_relative_path(prosody.paths.data or ".", database or "prosody.sqlite"); end return database; end local function normalize_params(params) return { driver = assert(params.driver, "Configuration error: Both the SQL driver and the database need to be specified"); database = assert(normalize_database(params.driver, params.database), "Configuration error: Both the SQL driver and the database need to be specified"); username = params.username; password = params.password; host = params.host; port = params.port; }; 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_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); -- Check whether the table needs upgrading if upgrade_table(engine, params, false) then module:log("error", "Old database format detected. Please run: prosodyctl mod_%s upgrade", module.name); return false, "database upgrade needed"; end end if engine.params.driver == "SQLite3" then local compile_options = {} for row in engine:select("PRAGMA compile_options") do local option = row[1]:lower(); local opt, val = option:match("^([^=]+)=(.*)$"); compile_options[opt or option] = tonumber(val) or val or true; end -- COMPAT Need to check SQLite3 version because SQLCipher 3.x was based on SQLite3 prior to 3.24.0 when UPSERT was introduced for row in engine:select("SELECT sqlite_version()") do local version = {}; for n in row[1]:gmatch("%d+") do table.insert(version, tonumber(n)); end engine.sqlite_version = version; 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 "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"); for _, params in pairs(uris) do print("", "["..params.driver.."] "..params.database..(params.host and " on "..params.host or "")); end print(""); print("Ensure you have working backups of the above databases before continuing! "); if false == hi.show_yesno("Continue with the database upgrade? [yN]") then print("Ok, no upgrade. But you do have backups, don't you? ...don't you?? :-)"); return; end -- 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 print("All done!"); elseif command then print("Unknown command: "..command); else print("Available commands:"); print("","upgrade - Perform database upgrade"); end end prosody-13.0.1/plugins/PaxHeaders/mod_storage_xep0227.lua0000644000000000000000000000011714773555365020256 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.651710774 prosody-13.0.1/plugins/mod_storage_xep0227.lua0000644000175000017500000005403414773555365022463 0ustar00prosodyprosody00000000000000 local ipairs, pairs = ipairs, pairs; local setmetatable = setmetatable; local tostring = tostring; local next, unpack = next, table.unpack; local os_remove = os.remove; local io_open = io.open; 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"; local function default_get_user_xml(self, user, host) --luacheck: ignore 212/self local jid = jid_join(user, host); local path = paths.join(prosody.paths.data, jid..".xml"); local f, err = io_open(path); if not f then module:log("debug", "Unable to load XML file for <%s>: %s", jid, err); return; end module:log("debug", "Loaded %s", path); local s = f:read("*a"); f:close(); return parse_xml_real(s); end local function default_set_user_xml(self, user, host, xml) --luacheck: ignore 212/self local jid = jid_join(user, host); local path = paths.join(prosody.paths.data, jid..".xml"); local f, err = io_open(path, "w"); if not f then return f, err; end if xml then local s = tostring(xml); f:write(s); f:close(); return true; else f:close(); return os_remove(path); end end local function getUserElement(xml) if xml and xml.name == "server-data" then local host = xml.tags[1]; if host and host.name == "host" then local user = host.tags[1]; if user and user.name == "user" then return user; end end end module:log("warn", "Unable to find user element in %s", xml and xml:top_tag() or "nothing"); end local function createOuterXml(user, host) return st.stanza("server-data", {xmlns='urn:xmpp:pie:0'}) :tag("host", {jid=host}) :tag("user", {name = user}); end local function hex_to_base64(s) return base64.encode(hex.decode(s)); end local function base64_to_hex(s) return hex.encode(base64.decode(s)); end 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_enum("password_hash", "SHA-1", "SHA-256"); local scram_properties = set.new({ "server_key", "stored_key", "iteration_count", "salt" }); handlers.accounts = { get = function(self, user) user = getUserElement(self:_get_user_xml(user, self.host)); local scram_credentials = user and user:get_child_with_attr( "scram-credentials", "urn:xmpp:pie:0#scram", "mechanism", "SCRAM-"..scram_hash_name ); if scram_credentials then return { iteration_count = tonumber(scram_credentials:get_child_text("iter-count")); server_key = base64_to_hex(scram_credentials:get_child_text("server-key")); stored_key = base64_to_hex(scram_credentials:get_child_text("stored-key")); salt = base64.decode(scram_credentials:get_child_text("salt")); }; elseif user and user.attr.password then return { password = user.attr.password }; elseif user then local data = {}; for k, v in pairs(user.attr) do if k:sub(1, #extended) == extended then data[k:sub(#extended+1)] = v; end end return data; end end; set = function(self, user, data) if not data then return self:_set_user_xml(user, self.host, nil); end local xml = self:_get_user_xml(user, self.host); if not xml then xml = createOuterXml(user, self.host); end local usere = getUserElement(xml); local account_properties = set.new(it.to_array(it.keys(data))); -- Include SCRAM credentials if known if account_properties:contains_set(scram_properties) then local scram_el = st.stanza("scram-credentials", { xmlns = "urn:xmpp:pie:0#scram", mechanism = "SCRAM-"..scram_hash_name }) :text_tag("server-key", hex_to_base64(data.server_key)) :text_tag("stored-key", hex_to_base64(data.stored_key)) :text_tag("iter-count", ("%d"):format(data.iteration_count)) :text_tag("salt", base64.encode(data.salt)); usere:add_child(scram_el); account_properties:exclude(scram_properties); end -- Include the password if present if account_properties:contains("password") then usere.attr.password = data.password; account_properties:remove("password"); end -- Preserve remaining properties as namespaced attributes for property in account_properties do usere.attr[extended..property] = data[property]; end return self:_set_user_xml(user, self.host, xml); end; }; handlers.vcard = { get = function(self, user) user = getUserElement(self:_get_user_xml(user, self.host)); if user then local vcard = user:get_child("vCard", 'vcard-temp'); if vcard then return st.preserialize(vcard); end end end; set = function(self, user, data) local xml = self:_get_user_xml(user, self.host); local usere = xml and getUserElement(xml); if usere then usere:remove_children("vCard", "vcard-temp"); if not data or not data.attr then -- No data to set, old one deleted, success return true; end local vcard = st.deserialize(data); usere:add_child(vcard); return self:_set_user_xml(user, self.host, xml); end return true; end; }; handlers.private = { get = function(self, user) user = getUserElement(self:_get_user_xml(user, self.host)); if user then local private = user:get_child("query", "jabber:iq:private"); if private then local r = {}; for _, tag in ipairs(private.tags) do r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag); end return r; end end end; set = function(self, user, data) local xml = self:_get_user_xml(user, self.host); local usere = xml and getUserElement(xml); if usere then usere:remove_children("query", "jabber:iq:private"); if data and next(data) ~= nil then local private = st.stanza("query", {xmlns='jabber:iq:private'}); for _,tag in pairs(data) do private:add_child(st.deserialize(tag)); end usere:add_child(private); end return self:_set_user_xml(user, self.host, xml); end return true; end; }; handlers.roster = { get = function(self, user) user = getUserElement(self:_get_user_xml(user, self.host)); if user then local roster = user:get_child("query", "jabber:iq:roster"); if roster then local r = { [false] = { version = roster.attr.version; pending = {}; } }; for item in roster:childtags("item") do r[item.attr.jid] = { jid = item.attr.jid, subscription = item.attr.subscription, ask = item.attr.ask, name = item.attr.name, groups = {}; }; for group in item:childtags("group") do r[item.attr.jid].groups[group:get_text()] = true; end for pending in user:childtags("presence", "jabber:client") do r[false].pending[pending.attr.from] = true; end end return r; end end end; set = function(self, user, data) local xml = self:_get_user_xml(user, self.host); local usere = xml and getUserElement(xml); if usere then local user_jid = jid_join(usere.name, self.host); usere:remove_children("query", "jabber:iq:roster"); usere:maptags(function (tag) if tag.attr.xmlns == "jabber:client" and tag.name == "presence" and tag.attr.type == "subscribe" then return nil; end return tag; end); if data and next(data) ~= nil then local roster = st.stanza("query", {xmlns='jabber:iq:roster'}); usere:add_child(roster); for contact_jid, item in pairs(data) do if contact_jid ~= false then contact_jid = jid_bare(jid_prep(contact_jid)); if contact_jid ~= user_jid then -- Skip self-contacts roster:tag("item", { jid = contact_jid, subscription = item.subscription, ask = item.ask, name = item.name, }); for group in pairs(item.groups) do roster:tag("group"):text(group):up(); end roster:up(); -- move out from item end else roster.attr.version = item.version; for pending_jid in pairs(item.pending) do usere:add_child(st.presence({ from = pending_jid, type = "subscribe" })); end end end end return self:_set_user_xml(user, self.host, xml); end return true; end; }; -- PEP node configuration/etc. (not items) local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; local lib_pubsub = module:require "pubsub"; handlers.pep = { get = function (self, user) local xml = self:_get_user_xml(user, self.host); local user_el = xml and getUserElement(xml); if not user_el then return nil; end local nodes = { --[[ [node_name] = { name = node_name; config = {}; affiliations = {}; subscribers = {}; }; ]] }; local owner_el = user_el:get_child("pubsub", xmlns_pubsub_owner); if not owner_el then local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub); if not pubsub_el then return nil; end for node_el in pubsub_el:childtags("items") do nodes[node_el.attr.node] = true; -- relies on COMPAT behavior in mod_pep end return nodes; end for node_el in owner_el:childtags() do local node_name = node_el.attr.node; local node = nodes[node_name]; if not node then node = { name = node_name; config = {}; affiliations = {}; subscribers = {}; }; nodes[node_name] = node; end if node_el.name == "configure" then local form = node_el:get_child("x", "jabber:x:data"); if form then node.config = lib_pubsub.node_config_form:data(form); end elseif node_el.name == "affiliations" then for affiliation_el in node_el:childtags("affiliation") do local aff_jid = jid_prep(affiliation_el.attr.jid); local aff_value = affiliation_el.attr.affiliation; if aff_jid and aff_value then node.affiliations[aff_jid] = aff_value; end end elseif node_el.name == "subscriptions" then for subscription_el in node_el:childtags("subscription") do local sub_jid = jid_prep(subscription_el.attr.jid); local sub_state = subscription_el.attr.subscription; if sub_jid and sub_state == "subscribed" then local options; local subscription_options_el = subscription_el:get_child("options"); if subscription_options_el then local options_form = subscription_options_el:get_child("x", "jabber:x:data"); if options_form then options = lib_pubsub.subscription_options_form:data(options_form); end end node.subscribers[sub_jid] = options or true; end end else module:log("warn", "Ignoring unknown pubsub element: %s", node_el.name); end end return nodes; end; set = function(self, user, data) local xml = self:_get_user_xml(user, self.host); local user_el = xml and getUserElement(xml); if not user_el then return true; end -- Remove existing data, if any user_el:remove_children("pubsub", xmlns_pubsub_owner); -- Generate new data local owner_el = st.stanza("pubsub", { xmlns = xmlns_pubsub_owner }); for node_name, node_data in pairs(data) do if node_data == true then node_data = { config = {} }; end local configure_el = st.stanza("configure", { node = node_name }) :add_child(lib_pubsub.node_config_form:form(node_data.config, "submit")); owner_el:add_child(configure_el); if node_data.affiliations and next(node_data.affiliations) ~= nil then local affiliations_el = st.stanza("affiliations", { node = node_name }); for aff_jid, aff_value in pairs(node_data.affiliations) do affiliations_el:tag("affiliation", { jid = aff_jid, affiliation = aff_value }):up(); end owner_el:add_child(affiliations_el); end if node_data.subscribers and next(node_data.subscribers) ~= nil then local subscriptions_el = st.stanza("subscriptions", { node = node_name }); for sub_jid, sub_data in pairs(node_data.subscribers) do local sub_el = st.stanza("subscription", { jid = sub_jid, subscribed = "subscribed" }); if sub_data ~= true then local options_form = lib_pubsub.subscription_options_form:form(sub_data, "submit"); sub_el:tag("options"):add_child(options_form):up(); end subscriptions_el:add_child(sub_el); end owner_el:add_child(subscriptions_el); end end user_el:add_child(owner_el); return self:_set_user_xml(user, self.host, xml); end; }; -- PEP items handlers.pep_ = { _stores = function (self, xml) --luacheck: ignore 212/self local store_names = set.new(); local user_el = xml and getUserElement(xml); if not user_el then return store_names; end -- Locate existing pubsub element, if any local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub); if not pubsub_el then return store_names; end -- Find node items element, if any for items_el in pubsub_el:childtags("items") do store_names:add("pep_"..items_el.attr.node); end return store_names; end; find = function (self, user, query) -- query keys: limit, reverse, key (id) local xml = self:_get_user_xml(user, self.host); local user_el = xml and getUserElement(xml); if not user_el then return nil, "no 227 user element found"; end local node_name = self.datastore:match("^pep_(.+)$"); -- Locate existing pubsub element, if any local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub); if not pubsub_el then return nil; end -- Find node items element, if any local node_items_el; for items_el in pubsub_el:childtags("items") do if items_el.attr.node == node_name then node_items_el = items_el; break; end end if not node_items_el then return nil; end local user_jid = user.."@"..self.host; local results = {}; for item_el in node_items_el:childtags("item") do if query and query.key then if item_el.attr.id == query.key then table.insert(results, { item_el.attr.id, item_el.tags[1], 0, user_jid }); break; end else table.insert(results, { item_el.attr.id, item_el.tags[1], 0, user_jid }); end if query and query.limit and #results >= query.limit then break; end end if query and query.reverse then return array.reverse(results); end local i = 0; return function () i = i + 1; local v = results[i]; if v == nil then return nil; end return unpack(v, 1, 4); end; end; append = function (self, user, key, payload, when, with) --luacheck: ignore 212/when 212/with 212/key local xml = self:_get_user_xml(user, self.host); local user_el = xml and getUserElement(xml); if not user_el then return true; end local node_name = self.datastore:match("^pep_(.+)$"); -- Locate existing pubsub element, if any local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub); if not pubsub_el then pubsub_el = st.stanza("pubsub", { xmlns = xmlns_pubsub }); user_el:add_child(pubsub_el); end -- Find node items element, if any local node_items_el; for items_el in pubsub_el:childtags("items") do if items_el.attr.node == node_name then node_items_el = items_el; break; end end if not node_items_el then -- Doesn't exist yet, create one node_items_el = st.stanza("items", { node = node_name }); pubsub_el:add_child(node_items_el); end -- Append item to pubsub_el local item_el = st.stanza("item", { id = key }) :add_child(payload); node_items_el:add_child(item_el); return self:_set_user_xml(user, self.host, xml); end; delete = function (self, user, query) -- query keys: limit, reverse, key (id) local xml = self:_get_user_xml(user, self.host); local user_el = xml and getUserElement(xml); if not user_el then return nil, "no 227 user element found"; end local node_name = self.datastore:match("^pep_(.+)$"); -- Locate existing pubsub element, if any local pubsub_el = user_el:get_child("pubsub", xmlns_pubsub); if not pubsub_el then return nil; end -- Find node items element, if any local node_items_el; for items_el in pubsub_el:childtags("items") do if items_el.attr.node == node_name then node_items_el = items_el; break; end end if not node_items_el then return nil; end local results = array(); for item_el in pubsub_el:childtags("item") do if query and query.key then if item_el.attr.id == query.key then table.insert(results, item_el); break; end else table.insert(results, item_el); end if query and query.limit and #results >= query.limit then break; end end if query and query.truncate then results:sub(-query.truncate); end -- Actually remove the matching items local delete_keys = set.new(results:map(function (item) return item.attr.id; end)); pubsub_el:maptags(function (item_el) if delete_keys:contains(item_el.attr.id) then return nil; end return item_el; end); return self:_set_user_xml(user, self.host, xml); end; }; -- MAM archives local xmlns_pie_mam = "urn:xmpp:pie:0#mam"; handlers.archive = { find = function (self, user, query) assert(query == nil, "XEP-0313 queries are not supported on XEP-0227 files"); local xml = self:_get_user_xml(user, self.host); local user_el = xml and getUserElement(xml); if not user_el then return nil, "no 227 user element found"; end -- Locate existing archive element, if any local archive_el = user_el:get_child("archive", xmlns_pie_mam); if not archive_el then return nil; end local user_jid = user.."@"..self.host; local f, s, result_el = archive_el:childtags("result", "urn:xmpp:mam:2"); return function () result_el = f(s, result_el); if not result_el then return nil; end local id = result_el.attr.id; local item = result_el:find("{urn:xmpp:forward:0}forwarded/{jabber:client}message"); assert(item, "Invalid stanza in XEP-0227 archive"); local when = dt.parse(result_el:find("{urn:xmpp:forward:0}forwarded/{urn:xmpp:delay}delay@stamp")); local to_bare, from_bare = jid_bare(item.attr.to), jid_bare(item.attr.from); local with = to_bare == user_jid and from_bare or to_bare; -- id, item, when, with return id, item, when, with; end; end; append = function (self, user, key, payload, when, with) --luacheck: ignore 212/when 212/with 212/key local xml = self:_get_user_xml(user, self.host); local user_el = xml and getUserElement(xml); if not user_el then return true; end -- Locate existing archive element, if any local archive_el = user_el:get_child("archive", xmlns_pie_mam); if not archive_el then archive_el = st.stanza("archive", { xmlns = xmlns_pie_mam }); user_el:add_child(archive_el); end local item = st.clone(payload); item.attr.xmlns = "jabber:client"; local result_el = st.stanza("result", { xmlns = "urn:xmpp:mam:2", id = key }) :tag("forwarded", { xmlns = "urn:xmpp:forward:0" }) :tag("delay", { xmlns = "urn:xmpp:delay", stamp = dt.datetime(when) }):up() :add_child(item) :up(); -- Append item to archive_el archive_el:add_child(result_el); return self:_set_user_xml(user, self.host, xml); end; }; ----------------------------- local driver = {}; local function users(self) local file_patt = "^.*@"..(self.host:gsub("%p", "%%%1")).."%.xml$"; local f, s, filename = lfs.dir(prosody.paths.data); return function () filename = f(s, filename); while filename and not filename:match(file_patt) do filename = f(s, filename); end if not filename then return nil; end return filename:match("^[^@]+"); end; end function driver:open(datastore, typ) -- luacheck: ignore 212/self if typ and typ ~= "keyval" and typ ~= "archive" then return nil, "unsupported-store"; end local handler = handlers[datastore]; if not handler and datastore:match("^pep_") then handler = handlers.pep_; end if not handler then return nil, "unsupported-datastore"; end local instance = setmetatable({ host = module.host; datastore = datastore; users = users; _get_user_xml = assert(default_get_user_xml); _set_user_xml = default_set_user_xml; }, { __index = handler; } ); if instance.init then instance:init(); end return instance; end -- Custom API that allows some configuration function driver:open_xep0227(datastore, typ, options) local instance, err = self:open(datastore, typ); if not instance then return instance, err; end if options then instance._set_user_xml = assert(options.set_user_xml); instance._get_user_xml = assert(options.get_user_xml); end return instance; end local function get_store_names_from_xml(self, user_xml) local stores = set.new(); for handler_name, handler_funcs in pairs(handlers) do if handler_funcs._stores then stores:include(handler_funcs._stores(self, user_xml)); else stores:add(handler_name); end end return stores; end local function get_store_names(self, path) local stores = set.new(); local f, err = io_open(paths.join(prosody.paths.data, path)); if not f then module:log("warn", "Unable to load XML file for <%s>: %s", "store listing", err); return stores; end module:log("info", "Loaded %s", path); local s = f:read("*a"); f:close(); local user_xml = parse_xml_real(s); return get_store_names_from_xml(self, user_xml); end function driver:stores(username) local store_dir = prosody.paths.data; local mode, err = lfs.attributes(store_dir, "mode"); if not mode then return function() module:log("debug", "Could not iterate over stores in %s: %s", store_dir, err); end end local file_patt = "^.*@"..(module.host:gsub("%p", "%%%1")).."%.xml$"; local all_users = username == true; local store_names = set.new(); for filename in lfs.dir(prosody.paths.data) do if filename:match(file_patt) then if all_users or filename == username.."@"..module.host..".xml" then store_names:include(get_store_names(self, filename)); if not all_users then break; end end end end return store_names:items(); end function driver:xep0227_user_stores(username, host) local user_xml = self:_get_user_xml(username, host); if not user_xml then return nil; end local store_names = get_store_names_from_xml(username, host); return store_names:items(); end module:provides("storage", driver); prosody-13.0.1/plugins/PaxHeaders/mod_time.lua0000644000000000000000000000011714773555365016361 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.651710774 prosody-13.0.1/plugins/mod_time.lua0000644000175000017500000000152414773555365020562 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "prosody.util.stanza"; local datetime = require "prosody.util.datetime".datetime; local now = require "prosody.util.time".now; -- XEP-0202: Entity Time module:add_feature("urn:xmpp:time"); 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(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); prosody-13.0.1/plugins/PaxHeaders/mod_tls.lua0000644000000000000000000000011614773555365016224 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.65571079 prosody-13.0.1/plugins/mod_tls.lua0000644000175000017500000002007614773555365020431 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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_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"); s2s_require_encryption = true; end local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls'; local starttls_attr = { xmlns = xmlns_starttls }; local starttls_initiate= st.stanza("starttls", starttls_attr); local starttls_proceed = st.stanza("proceed", starttls_attr); local starttls_failure = st.stanza("failure", starttls_attr); local c2s_feature = st.stanza("starttls", starttls_attr); local s2s_feature = st.stanza("starttls", starttls_attr); if c2s_require_encryption then c2s_feature:tag("required"):up(); end if s2s_require_encryption then s2s_feature:tag("required"):up(); end local hosts = prosody.hosts; local host = hosts[module.host]; local ssl_ctx_c2s, ssl_ctx_s2sout, ssl_ctx_s2sin; local ssl_cfg_c2s, ssl_cfg_s2sout, ssl_cfg_s2sin; local err_c2s, err_s2sin, err_s2sout; function module.load(reload) local NULL = {}; local modhost = module.host; local parent = modhost:match("%.(.*)$"); local parent_ssl = rawgetopt(parent, "ssl") or NULL; local host_ssl = rawgetopt(modhost, "ssl") or parent_ssl; local global_c2s = rawgetopt("*", "c2s_ssl") or NULL; local parent_c2s = rawgetopt(parent, "c2s_ssl") or NULL; local host_c2s = rawgetopt(modhost, "c2s_ssl") or parent_c2s; local global_s2s = rawgetopt("*", "s2s_ssl") or NULL; local parent_s2s = rawgetopt(parent, "s2s_ssl") or NULL; local host_s2s = rawgetopt(modhost, "s2s_ssl") or parent_s2s; module:log("debug", "Creating context for c2s"); local request_client_certs = { verify = { "peer", "client_once", }; }; local custom_cert_verification = { verifyext = { "lsec_continue", "lsec_ignore_purpose" }; }; local xmpp_alpn = { alpn = "xmpp-server" }; ssl_ctx_c2s, err_c2s, ssl_cfg_c2s = create_context(host.host, "server", host_c2s, host_ssl, global_c2s); -- for incoming client connections if not ssl_ctx_c2s then module:log("error", "Error creating context for c2s: %s", err_c2s); end 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, xmpp_alpn, custom_cert_verification); 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"); -- for incoming server connections ssl_ctx_s2sin, err_s2sin, ssl_cfg_s2sin = create_context(host.host, "server", host_s2s, host_ssl, global_s2s, request_client_certs, custom_cert_verification ); if not ssl_ctx_s2sin then module:log("error", "Error creating contexts for s2sin: %s", err_s2sin); end if reload then module:log("info", "Certificates reloaded"); else module:log("info", "Certificates loaded"); end 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"); end return false; elseif session.ssl_ctx ~= nil then return session.ssl_ctx; end if session.type == "c2s_unauthed" then if not ssl_ctx_c2s and c2s_require_encryption then session.log("error", "No TLS context available for c2s. Earlier error was: %s", err_c2s); end session.ssl_ctx = ssl_ctx_c2s; session.ssl_cfg = ssl_cfg_c2s; elseif session.type == "s2sin_unauthed" and allow_s2s_tls then if not ssl_ctx_s2sin and s2s_require_encryption then session.log("error", "No TLS context available for s2sin. Earlier error was: %s", err_s2sin); end session.ssl_ctx = ssl_ctx_s2sin; session.ssl_cfg = ssl_cfg_s2sin; elseif session.direction == "outgoing" and allow_s2s_tls then if not ssl_ctx_s2sout and s2s_require_encryption then session.log("error", "No TLS context available for s2sout. Earlier error was: %s", err_s2sout); end session.ssl_ctx = ssl_ctx_s2sout; session.ssl_cfg = ssl_cfg_s2sout; else session.log("debug", "Unknown session type, don't know which TLS context to use"); return false; end if not session.ssl_ctx then session.log("debug", "Should be able to do TLS but no context available"); return false; end return session.ssl_ctx; end module:hook("s2sout-created", function (event) -- Initialize TLS context for outgoing connections can_do_tls(event.session); end); -- Hook module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event) local origin = event.origin; origin.starttls = "requested"; if can_do_tls(origin) then if origin.conn.block_reads then -- we need to ensure that no data is read anymore, otherwise we could end up in a situation where -- is sent and the socket receives the TLS handshake (and passes the data to lua) before -- it is asked to initiate TLS -- (not with the classical single-threaded server backends) origin.conn:block_reads() end (origin.sends2s or origin.send)(starttls_proceed); if origin.destroyed then return end origin:reset_stream(); origin.conn:starttls(origin.ssl_ctx); origin.log("debug", "TLS negotiation started for %s...", origin.type); origin.secure = false; else origin.log("warn", "Attempt to start TLS, but TLS is not available on this %s connection", origin.type); (origin.sends2s or origin.send)(starttls_failure); origin:close(); end return true; end); -- Advertise stream feature module:hook("stream-features", function(event) local origin, features = event.origin, event.features; if can_do_tls(origin) then features:add_child(c2s_feature); end end); module:hook("s2s-stream-features", function(event) local origin, features = event.origin, event.features; if can_do_tls(origin) then features:add_child(s2s_feature); end end); -- For s2sout connections, start TLS if we can module:hook_tag("http://etherx.jabber.org/streams", "features", function (session, stanza) module:log("debug", "Received features element"); if can_do_tls(session) then if stanza:get_child("starttls", xmlns_starttls) then module:log("debug", "%s is offering TLS, taking up the offer...", session.to_host); elseif s2s_require_encryption then module:log("debug", "%s is *not* offering TLS, trying anyways!", session.to_host); else module:log("debug", "%s is not offering TLS", session.to_host); return; end session.starttls = "initiated"; session.sends2s(starttls_initiate); return true; end end, 500); module:hook("s2sout-authenticate-legacy", function(event) local session = event.origin; if s2s_require_encryption and can_do_tls(session) then session.sends2s(starttls_initiate); return true; end end, 200); module:hook_tag(xmlns_starttls, "proceed", function (session, stanza) -- luacheck: ignore 212/stanza if session.type == "s2sout_unauthed" and can_do_tls(session) then module:log("debug", "Proceeding with TLS on s2sout..."); session:reset_stream(); session.starttls = "proceeding" session.conn:starttls(session.ssl_ctx, session.to_host); session.secure = false; return true; end end); module:hook_tag(xmlns_starttls, "failure", function (session, stanza) -- luacheck: ignore 212/stanza module:log("warn", "TLS negotiation with %s failed.", session.to_host); session:close(nil, "TLS negotiation failed"); return false; end); prosody-13.0.1/plugins/PaxHeaders/mod_tokenauth.lua0000644000000000000000000000011614773555365017424 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.65571079 prosody-13.0.1/plugins/mod_tokenauth.lua0000644000175000017500000002611514773555365021631 0ustar00prosodyprosody00000000000000local base64 = require "prosody.util.encodings".base64; local hashes = require "prosody.util.hashes"; local id = require "prosody.util.id"; local jid = require "prosody.util.jid"; local random = require "prosody.util.random"; local usermanager = require "prosody.core.usermanager"; local generate_identifier = require "prosody.util.id".short; local token_store = module:open_store("auth_tokens", "keyval+"); local access_time_granularity = module:get_option_period("token_auth_access_time_granularity", 60); local empty_grant_lifetime = module:get_option_period("tokenless_grant_ttl", "2w"); local 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 grant_username, grant_host, grant_resource = jid.split(grant_jid); if grant_host ~= module.host then return nil, "invalid-host"; end local grant_id = id.short(); local now = os.time(); local grant = { id = grant_id; owner = actor_jid; created = now; expires = grant_ttl and (now + grant_ttl) or nil; accessed = now; jid = grant_jid; resource = grant_resource; data = grant_data; -- tokens[..":"..] = token_info tokens = {}; }; local ok, err = token_store:set_key(grant_username, grant_id, grant); if not ok then return nil, err; end module:fire_event("token-grant-created", { id = grant_id; grant = grant; username = grant_username; host = grant_host; }); return grant; end function create_token(grant_jid, grant, token_role, token_ttl, token_purpose, token_data) if (token_data and type(token_data) ~= "table") or (token_purpose and type(token_purpose) ~= "string") then return nil, "bad-request"; end local grant_username, grant_host = jid.split(grant_jid); if grant_host ~= module.host then return nil, "invalid-host"; end if type(grant) == "string" then -- lookup by id grant = token_store:get_key(grant_username, grant); if not grant then return nil; end end if not grant.tokens then return nil, "internal-server-error"; end -- old-style token? local now = os.time(); local expires = grant.expires; -- Default to same expiry as grant if token_ttl then -- explicit lifetime requested if expires then -- Grant has an expiry, so limit to that or shorter expires = math.min(now + token_ttl, expires); else -- Grant never expires, just use whatever expiry is requested for the token expires = now + token_ttl; end end local token_info = { role = token_role; created = now; expires = expires; purpose = token_purpose; data = token_data; }; local token_secret = random.bytes(18); grant.tokens["sha256:"..hashes.sha256(token_secret, true)] = token_info; local ok, err = token_store:set_key(grant_username, grant.id, grant); if not ok then return nil, err; end local token_string = "secret-token:"..base64.encode("2;"..grant.id..";"..token_secret..";"..grant.jid); return token_string, token_info; end local function parse_token(encoded_token) if not encoded_token then return nil; end local encoded_data = encoded_token:match("^secret%-token:(.+)$"); if not encoded_data then return nil; end local token = base64.decode(encoded_data); if not token then return nil; end local token_id, token_secret, token_jid = token:match("^2;([^;]+);(..................);(.+)$"); if not token_id then return nil; end local token_user, token_host = jid.split(token_jid); return token_id, token_user, token_host, token_secret; end local function clear_expired_grant_tokens(grant, now) local updated; now = now or os.time(); for secret, token_info in pairs(grant.tokens) do local expires = token_info.expires; if expires and expires <= now then grant.tokens[secret] = nil; updated = true; end end return updated; end local function _get_validated_grant_info(username, grant) if type(grant) == "string" then grant = token_store:get_key(username, grant); end if not grant or not grant.created or not grant.id then return nil; end -- Invalidate grants from before last password change local account_info = usermanager.get_account_info(username, module.host); local password_updated_at = account_info and account_info.password_updated; local now = os.time(); if password_updated_at and grant.created < password_updated_at then module:log("debug", "Token grant %s of %s issued before last password change, invalidating it now", grant.id, username); token_store:set_key(username, grant.id, nil); return nil, "not-authorized"; elseif grant.expires and grant.expires <= now then module:log("debug", "Token grant %s of %s expired, cleaning up", grant.id, username); token_store:set_key(username, grant.id, nil); return nil, "expired"; end if not grant.tokens then module:log("debug", "Token grant %s of %s without tokens, cleaning up", grant.id, username); token_store:set_key(username, grant.id, nil); return nil, "invalid"; end local found_expired = false for secret_hash, token_info in pairs(grant.tokens) do if token_info.expires and token_info.expires <= now then module:log("debug", "Token %s of grant %s of %s has expired, cleaning it up", secret_hash:sub(-8), grant.id, username); grant.tokens[secret_hash] = nil; found_expired = true; end end if not grant.expires and next(grant.tokens) == nil and grant.accessed + empty_grant_lifetime <= now then module:log("debug", "Token %s of %s grant has no tokens, discarding", grant.id, username); token_store:set_key(username, grant.id, nil); return nil, "expired"; elseif found_expired then token_store:set_key(username, grant.id, grant); end return grant; end local function _get_validated_token_info(token_id, token_user, token_host, token_secret) if token_host ~= module.host then return nil, "invalid-host"; end local 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 -- Verify grant validity (expiry, etc.) grant = _get_validated_grant_info(token_user, grant); if not grant then return nil, "not-authorized"; end -- 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 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 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) prosody-13.0.1/plugins/PaxHeaders/mod_tombstones.lua0000644000000000000000000000011614773555365017617 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.65571079 prosody-13.0.1/plugins/mod_tombstones.lua0000644000175000017500000000754514773555365022032 0ustar00prosodyprosody00000000000000-- TODO warn when trying to create an user before the tombstone expires -- e.g. via telnet or other admin interface 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 "prosody.util.cache".new(module:get_option_integer("tombstone_cache_size", 1024, 1)); local ttl = module:get_option_period("user_tombstone_expiry", nil); -- Keep tombstones forever by default -- -- Rationale: -- There is no way to be completely sure when remote services have -- forgotten and revoked all memberships. -- TODO If the user left a JID they moved to, return a gone+redirect error -- TODO Attempt to deregister from MUCs based on bookmarks -- TODO Unsubscribe from pubsub services if a notification is received module:hook_global("user-deleted", function(event) if event.host == module.host then local ok, err = graveyard:set(nil, event.username, os.time()); if not ok then module:log("error", "Could store tombstone for %s: %s", event.username, err); end end end); -- Public API function has_tombstone(username) local tombstone; -- Check cache local cached_result = graveyard_cache:get(username); if cached_result == false then -- We cached that there is no tombstone for this user return false; elseif cached_result then tombstone = cached_result; else local stored_result, err = graveyard:get(nil, username); if not stored_result and not err then -- Cache that there is no tombstone for this user graveyard_cache:set(username, false); return false; elseif err then -- Failed to check tombstone status return nil, err; end -- We have a tombstone stored, so let's continue with that tombstone = stored_result; end -- Check expiry if ttl and tombstone + ttl < os.time() then module:log("debug", "Tombstone for %s created at %s has expired", username, datetime.datetime(tombstone)); graveyard:set(nil, username, nil); graveyard_cache:set(username, nil); -- clear cache entry (if any) return nil; end -- Cache for the future graveyard_cache:set(username, tombstone); return tombstone; end module:hook("user-registering", function(event) local tombstone, err = has_tombstone(event.username); if err then event.allowed, event.error = errors.coerce(false, err); return true; elseif not tombstone then -- Feel free return; end module:log("debug", "Tombstone for %s created at %s", event.username, datetime.datetime(tombstone)); event.allowed = false; return true; end); module:hook("presence/bare", function(event) local origin, presence = event.origin, event.stanza; local local_username = jid_node(presence.attr.to); if not local_username then return; end -- We want to undo any left-over presence subscriptions and notify the former -- contact that they're gone. -- -- FIXME This leaks that the user once existed. Hard to avoid without keeping -- the contact list in some form, which we don't want to do for privacy -- reasons. Bloom filter perhaps? local pres_type = presence.attr.type; local is_probe = pres_type == "probe"; local is_normal = pres_type == nil or pres_type == "unavailable"; if is_probe and has_tombstone(local_username) then origin.send(st.error_reply(presence, "cancel", "gone", "User deleted")); origin.send(st.presence({ type = "unsubscribed"; to = presence.attr.from; from = presence.attr.to })); return true; elseif is_normal and has_tombstone(local_username) then origin.send(st.error_reply(presence, "cancel", "gone", "User deleted")); origin.send(st.presence({ type = "unsubscribe"; to = presence.attr.from; from = presence.attr.to })); return true; end end, 1); prosody-13.0.1/plugins/PaxHeaders/mod_turn_external.lua0000644000000000000000000000011614773555365020314 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.65571079 prosody-13.0.1/plugins/mod_turn_external.lua0000644000175000017500000000253014773555365022514 0ustar00prosodyprosody00000000000000local 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_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_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"); return; end local services = set.new({ "stun-udp"; "turn-udp" }); if tcp then services:add("stun-tcp"); services:add("turn-tcp"); end if tls_port then services:add("turns-tcp"); end module:depends "external_services"; for _, type in ipairs({ "stun"; "turn"; "turns" }) do for _, transport in ipairs({"udp"; "tcp"}) do if services:contains(type .. "-" .. transport) then module:add_item("external_service", { type = type; transport = transport; host = host; port = type == "turns" and tls_port or port; username = type == "turn" and user or nil; secret = type == "turn" and secret or nil; ttl = type == "turn" and ttl or nil; }) end end end prosody-13.0.1/plugins/PaxHeaders/mod_unknown.lua0000644000000000000000000000011614773555365017121 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.65571079 prosody-13.0.1/plugins/mod_unknown.lua0000644000175000017500000000016514773555365021323 0ustar00prosodyprosody00000000000000-- Unknown platform stub module:set_global(); -- TODO Do things that make sense if we don't know about the platform prosody-13.0.1/plugins/PaxHeaders/mod_uptime.lua0000644000000000000000000000011614773555365016725 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.65571079 prosody-13.0.1/plugins/mod_uptime.lua0000644000175000017500000000274714773555365021137 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "prosody.util.stanza"; local start_time = prosody.start_time; module:hook_global("server-started", function() start_time = prosody.start_time end); -- XEP-0012: Last activity module:add_feature("jabber:iq:last"); module:hook("iq-get/host/jabber:iq:last:query", function(event) local origin, stanza = event.origin, event.stanza; origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:last", seconds = tostring(("%d"):format(os.difftime(os.time(), start_time)))})); return true; end); -- Ad-hoc command module:depends "adhoc"; local adhoc_new = module:require "adhoc".new; function uptime_text() local t = os.time()-prosody.start_time; local seconds = t%60; t = (t - seconds)/60; local minutes = t%60; t = (t - minutes)/60; local hours = t%24; t = (t - hours)/24; local days = t; return string.format("This server has been running for %d day%s, %d hour%s and %d minute%s (since %s)", days, (days ~= 1 and "s") or "", hours, (hours ~= 1 and "s") or "", minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time)); end function uptime_command_handler () return { info = uptime_text(), status = "completed" }; end local descriptor = adhoc_new("Get uptime", "uptime", uptime_command_handler, "any"); module:provides("adhoc", descriptor); prosody-13.0.1/plugins/PaxHeaders/mod_user_account_management.lua0000644000000000000000000000011614773555365022310 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.65571079 prosody-13.0.1/plugins/mod_user_account_management.lua0000644000175000017500000002026014773555365024510 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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; local log = session.log or module._log; local query = stanza.tags[1]; if stanza.attr.type == "get" then local reply = st.reply(stanza); reply:tag("query", {xmlns = "jabber:iq:register"}) :tag("registered"):up() :tag("username"):text(session.username):up() :tag("password"):up(); session.send(reply); else -- stanza.attr.type == "set" 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, ...) self.send(st.reply(stanza)); return old_session_close(self, ...); end local old_session = capture_session_properties(session); if not soft_delete_period then local ok, err = usermanager.delete_user(username, host); if not ok then log("debug", "Removing user account %s@%s failed: %s", username, host, err); session.close = old_session_close; session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); return true; end log("info", "User removed their account: %s@%s (deleted)", username, host); module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = old_session }); else local ok, err = usermanager.disable_user(username, host, { reason = "ibr"; comment = "Deletion requested by user"; when = os.time(); }); if not ok then log("debug", "Removing (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 session.send(st.reply(stanza)); else -- TODO unable to write file, file may be locked, etc, what's the correct error? session.send(st.error_reply(stanza, "wait", "internal-server-error")); end else session.send(st.error_reply(stanza, "modify", "bad-request")); end else session.send(st.error_reply(stanza, "modify", "bad-request")); end end end return true; end module:hook("iq/self/jabber:iq:register:query", handle_registration_stanza); if compat then module:hook("iq/host/jabber:iq:register:query", function (event) local session, stanza = event.origin, event.stanza; if session.type == "c2s" and jid_bare(stanza.attr.to) == session.host then return handle_registration_stanza(event); end 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; }); prosody-13.0.1/plugins/PaxHeaders/mod_vcard.lua0000644000000000000000000000011714773555365016522 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.659710806 prosody-13.0.1/plugins/mod_vcard.lua0000644000175000017500000000455514773555365020732 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local base64 = require "prosody.util.encodings".base64; local jid = require "prosody.util.jid"; local sha1 = require "prosody.util.hashes".sha1; local st = require "prosody.util.stanza" local jid_split = require "prosody.util.jid".split; local store_name = module:get_option_string("vcard_store_name"); local is_component = module:get_host_type() == "component"; if is_component and not store_name and module:get_option_string("component_module") == "muc" then store_name = "vcard_muc"; end local vcards = module:open_store(store_name); module:add_feature("vcard-temp"); local function handle_vcard(event) local session, stanza = event.origin, event.stanza; local to = stanza.attr.to; if stanza.attr.type == "get" then local vCard; if to then local node = jid_split(to); vCard = st.deserialize(vcards:get(node)); -- load vCard for user or server elseif not is_component then vCard = st.deserialize(vcards:get(session.username));-- load user's own vCard end if vCard then session.send(st.reply(stanza):add_child(vCard)); -- send vCard! else session.send(st.error_reply(stanza, "cancel", "item-not-found")); end else -- stanza.attr.type == "set" if not to or (is_component and event.allow_vcard_modification) then local node = is_component and jid.node(stanza.attr.to) or session.username; if vcards:set(node, st.preserialize(stanza.tags[1])) then session.send(st.reply(stanza)); module:fire_event("vcard-updated", event); else -- TODO unable to write file, file may be locked, etc, what's the correct error? session.send(st.error_reply(stanza, "wait", "internal-server-error")); end else session.send(st.error_reply(stanza, "auth", "forbidden")); end end return true; end module:hook("iq/bare/vcard-temp:vCard", handle_vcard); module:hook("iq/host/vcard-temp:vCard", handle_vcard); function get_avatar_hash(username) local vcard = st.deserialize(vcards:get(username)); if not vcard then return end local photo = vcard:get_child("PHOTO"); if not photo then return end local photo_b64 = photo:get_child_text("BINVAL"); local photo_raw = photo_b64 and base64.decode(photo_b64); return (sha1(photo_raw, true)); end prosody-13.0.1/plugins/PaxHeaders/mod_vcard4.lua0000644000000000000000000000011714773555365016606 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.659710806 prosody-13.0.1/plugins/mod_vcard4.lua0000644000175000017500000000315114773555365021005 0ustar00prosodyprosody00000000000000local st = require "prosody.util.stanza" local jid_split = require "prosody.util.jid".split; local mod_pep = module:depends("pep"); module:hook("account-disco-info", function (event) event.reply:tag("feature", { var = "urn:ietf:params:xml:ns:vcard-4.0" }):up(); end); module:hook("iq-get/bare/urn:ietf:params:xml:ns:vcard-4.0:vcard", function (event) local origin, stanza = event.origin, event.stanza; local pep_service = mod_pep.get_pep_service(jid_split(stanza.attr.to) or origin.username); local ok, id, item = pep_service:get_last_item("urn:xmpp:vcard4", stanza.attr.from); if ok and item then origin.send(st.reply(stanza):add_child(item.tags[1])); elseif id == "item-not-found" or not id then origin.send(st.error_reply(stanza, "cancel", "item-not-found")); elseif id == "forbidden" then origin.send(st.error_reply(stanza, "auth", "forbidden")); else origin.send(st.error_reply(stanza, "modify", "undefined-condition")); end return true; end); module:hook("iq-set/self/urn:ietf:params:xml:ns:vcard-4.0:vcard", function (event) local origin, stanza = event.origin, event.stanza; local vcard4 = st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = "current" }) :add_child(stanza.tags[1]); local pep_service = mod_pep.get_pep_service(origin.username); local ok, err = pep_service:publish("urn:xmpp:vcard4", origin.full_jid, "current", vcard4); if ok then origin.send(st.reply(stanza)); elseif err == "forbidden" then origin.send(st.error_reply(stanza, "auth", "forbidden")); else origin.send(st.error_reply(stanza, "modify", "undefined-condition", err)); end return true; end); prosody-13.0.1/plugins/PaxHeaders/mod_vcard_legacy.lua0000644000000000000000000000011714773555365020046 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.659710806 prosody-13.0.1/plugins/mod_vcard_legacy.lua0000644000175000017500000002725114773555365022254 0ustar00prosodyprosody00000000000000local st = require "prosody.util.stanza"; local jid_split = require "prosody.util.jid".split; local mod_pep = module:depends("pep"); local sha1 = require "prosody.util.hashes".sha1; local base64_decode = require "prosody.util.encodings".base64.decode; local vcards = module:open_store("vcard"); module:add_feature("vcard-temp"); module:hook("account-disco-info", function (event) event.reply:tag("feature", { var = "urn:xmpp:pep-vcard-conversion:0" }):up(); end); local function handle_error(origin, stanza, err) if err == "forbidden" then origin.send(st.error_reply(stanza, "auth", "forbidden")); elseif err == "internal-server-error" then origin.send(st.error_reply(stanza, "wait", "internal-server-error")); else origin.send(st.error_reply(stanza, "modify", "undefined-condition", err)); end end -- Simple translations -- hey -> hey local simple_map = { nickname = "text"; title = "text"; role = "text"; categories = "text"; note = "text"; url = "uri"; bday = "date"; } module:hook("iq-get/bare/vcard-temp:vCard", function (event) local origin, stanza = event.origin, event.stanza; local pep_service = mod_pep.get_pep_service(jid_split(stanza.attr.to) or origin.username); local ok, _, vcard4_item = pep_service:get_last_item("urn:xmpp:vcard4", stanza.attr.from); local vcard_temp = st.stanza("vCard", { xmlns = "vcard-temp" }); if ok and vcard4_item then local vcard4 = vcard4_item.tags[1]; local fn = vcard4:get_child("fn"); vcard_temp:text_tag("FN", fn and fn:get_child_text("text")); local v4n = vcard4:get_child("n"); vcard_temp:tag("N") :text_tag("FAMILY", v4n and v4n:get_child_text("surname")) :text_tag("GIVEN", v4n and v4n:get_child_text("given")) :text_tag("MIDDLE", v4n and v4n:get_child_text("additional")) :text_tag("PREFIX", v4n and v4n:get_child_text("prefix")) :text_tag("SUFFIX", v4n and v4n:get_child_text("suffix")) :up(); for tag in vcard4:childtags() do local typ = simple_map[tag.name]; if typ then local text = tag:get_child_text(typ); if text then vcard_temp:text_tag(tag.name:upper(), text); end elseif tag.name == "email" then local text = tag:get_child_text("text"); if text then vcard_temp:tag("EMAIL") :text_tag("USERID", text) :tag("INTERNET"):up(); if tag:find"parameters/type/text#" == "home" then vcard_temp:tag("HOME"):up(); elseif tag:find"parameters/type/text#" == "work" then vcard_temp:tag("WORK"):up(); end vcard_temp:up(); end elseif tag.name == "tel" then local text = tag:get_child_text("uri"); if text then if text:sub(1, 4) == "tel:" then text = text:sub(5) end vcard_temp:tag("TEL"):text_tag("NUMBER", text); if tag:find"parameters/type/text#" == "home" then vcard_temp:tag("HOME"):up(); elseif tag:find"parameters/type/text#" == "work" then vcard_temp:tag("WORK"):up(); end vcard_temp:up(); end elseif tag.name == "adr" then vcard_temp:tag("ADR") :text_tag("POBOX", tag:get_child_text("pobox")) :text_tag("EXTADD", tag:get_child_text("ext")) :text_tag("STREET", tag:get_child_text("street")) :text_tag("LOCALITY", tag:get_child_text("locality")) :text_tag("REGION", tag:get_child_text("region")) :text_tag("PCODE", tag:get_child_text("code")) :text_tag("CTRY", tag:get_child_text("country")); if tag:find"parameters/type/text#" == "home" then vcard_temp:tag("HOME"):up(); elseif tag:find"parameters/type/text#" == "work" then vcard_temp:tag("WORK"):up(); end vcard_temp:up(); elseif tag.name == "impp" then local uri = tag:get_child_text("uri"); if uri and uri:sub(1, 5) == "xmpp:" then vcard_temp:text_tag("JABBERID", uri:sub(6)) end elseif tag.name == "org" then vcard_temp:tag("ORG") :text_tag("ORGNAME", tag:get_child_text("text")) :up(); end end else local ok, _, nick_item = pep_service:get_last_item("http://jabber.org/protocol/nick", stanza.attr.from); if ok and nick_item then local nickname = nick_item:get_child_text("nick", "http://jabber.org/protocol/nick"); if nickname then vcard_temp:text_tag("NICKNAME", nickname); end end end local ok, avatar_hash, meta = pep_service:get_last_item("urn:xmpp:avatar:metadata", stanza.attr.from); if ok and avatar_hash then local info = meta.tags[1]:get_child("info"); if info then vcard_temp:tag("PHOTO"); if info.attr.type then vcard_temp:text_tag("TYPE", info.attr.type); end if info.attr.url then vcard_temp:text_tag("EXTVAL", info.attr.url); elseif info.attr.id then local data_ok, avatar_data = pep_service:get_items("urn:xmpp:avatar:data", stanza.attr.from, { info.attr.id }); if data_ok and avatar_data and avatar_data[info.attr.id] then local data = avatar_data[info.attr.id]; vcard_temp:text_tag("BINVAL", data.tags[1]:get_text()); end end vcard_temp:up(); end end origin.send(st.reply(stanza):add_child(vcard_temp)); return true; end); local node_defaults = { access_model = "open"; _defaults_only = true; }; function vcard_to_pep(vcard_temp) local avatar = {}; local vcard4 = st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = "current" }) :tag("vcard", { xmlns = 'urn:ietf:params:xml:ns:vcard-4.0' }); vcard4:tag("fn"):text_tag("text", vcard_temp:get_child_text("FN")):up(); local N = vcard_temp:get_child("N"); vcard4:tag("n") :text_tag("surname", N and N:get_child_text("FAMILY")) :text_tag("given", N and N:get_child_text("GIVEN")) :text_tag("additional", N and N:get_child_text("MIDDLE")) :text_tag("prefix", N and N:get_child_text("PREFIX")) :text_tag("suffix", N and N:get_child_text("SUFFIX")) :up(); for tag in vcard_temp:childtags() do local typ = simple_map[tag.name:lower()]; if typ then local text = tag:get_text(); if text then vcard4:tag(tag.name:lower()):text_tag(typ, text):up(); end elseif tag.name == "EMAIL" then local text = tag:get_child_text("USERID"); if text then vcard4:tag("email") vcard4:text_tag("text", text) vcard4:tag("parameters"):tag("type"); if tag:get_child("HOME") then vcard4:text_tag("text", "home"); elseif tag:get_child("WORK") then vcard4:text_tag("text", "work"); end vcard4:up():up():up(); end elseif tag.name == "TEL" then local text = tag:get_child_text("NUMBER"); if text then vcard4:tag("tel"):text_tag("uri", "tel:"..text); end vcard4:tag("parameters"):tag("type"); if tag:get_child("HOME") then vcard4:text_tag("text", "home"); elseif tag:get_child("WORK") then vcard4:text_tag("text", "work"); end vcard4:up():up():up(); elseif tag.name == "ORG" then local text = tag:get_child_text("ORGNAME"); if text then vcard4:tag("org"):text_tag("text", text):up(); end elseif tag.name == "DESC" then local text = tag:get_text(); if text then vcard4:tag("note"):text_tag("text", text):up(); end -- gets mapped into in the other direction elseif tag.name == "ADR" then vcard4:tag("adr") :text_tag("pobox", tag:get_child_text("POBOX")) :text_tag("ext", tag:get_child_text("EXTADD")) :text_tag("street", tag:get_child_text("STREET")) :text_tag("locality", tag:get_child_text("LOCALITY")) :text_tag("region", tag:get_child_text("REGION")) :text_tag("code", tag:get_child_text("PCODE")) :text_tag("country", tag:get_child_text("CTRY")); vcard4:tag("parameters"):tag("type"); if tag:get_child("HOME") then vcard4:text_tag("text", "home"); elseif tag:get_child("WORK") then vcard4:text_tag("text", "work"); end vcard4:up():up():up(); elseif tag.name == "JABBERID" then vcard4:tag("impp") :text_tag("uri", "xmpp:" .. tag:get_text()) :up(); elseif tag.name == "PHOTO" then local avatar_type = tag:get_child_text("TYPE"); local avatar_payload = tag:get_child_text("BINVAL"); -- Can EXTVAL be translated? No way to know the sha1 of the data? if avatar_payload then local avatar_raw = base64_decode(avatar_payload); local avatar_hash = sha1(avatar_raw, true); avatar.hash = avatar_hash; avatar.meta = st.stanza("item", { id = avatar_hash, xmlns = "http://jabber.org/protocol/pubsub" }) :tag("metadata", { xmlns="urn:xmpp:avatar:metadata" }) :tag("info", { bytes = tostring(#avatar_raw), id = avatar_hash, type = avatar_type, }); avatar.data = st.stanza("item", { id = avatar_hash, xmlns = "http://jabber.org/protocol/pubsub" }) :tag("data", { xmlns="urn:xmpp:avatar:data" }) :text(avatar_payload); end end end return vcard4, avatar; end function save_to_pep(pep_service, actor, vcard4, avatar) if avatar then if pep_service:purge("urn:xmpp:avatar:metadata", actor) then pep_service:purge("urn:xmpp:avatar:data", actor); end if avatar.data and avatar.meta then local ok, err = assert(pep_service:publish("urn:xmpp:avatar:data", actor, avatar.hash, avatar.data, node_defaults)); if ok then ok, err = assert(pep_service:publish("urn:xmpp:avatar:metadata", actor, avatar.hash, avatar.meta, node_defaults)); end if not ok then return ok, err; end end end if vcard4 then return pep_service:publish("urn:xmpp:vcard4", actor, "current", vcard4, node_defaults); end return true; end module:hook("iq-set/self/vcard-temp:vCard", function (event) local origin, stanza = event.origin, event.stanza; local pep_service = mod_pep.get_pep_service(origin.username); local vcard_temp = stanza.tags[1]; local ok, err = save_to_pep(pep_service, origin.full_jid, vcard_to_pep(vcard_temp)); if ok then origin.send(st.reply(stanza)); else handle_error(origin, stanza, err); end return true; end); local function inject_xep153(event) local origin, stanza = event.origin, event.stanza; local username = origin.username; if not username then return end if stanza.attr.type then return end local pep_service = mod_pep.get_pep_service(username); local x_update = stanza:get_child("x", "vcard-temp:x:update"); if not x_update then x_update = st.stanza("x", { xmlns = "vcard-temp:x:update" }):tag("photo"); stanza:add_direct_child(x_update); elseif x_update:get_child("photo") then return; -- XEP implies that these should be left alone else x_update:tag("photo"); end local ok, avatar_hash = pep_service:get_last_item("urn:xmpp:avatar:metadata", true); if ok and avatar_hash then x_update:text(avatar_hash); end end module:hook("pre-presence/full", inject_xep153, 1); module:hook("pre-presence/bare", inject_xep153, 1); module:hook("pre-presence/host", inject_xep153, 1); if module:get_option_boolean("upgrade_legacy_vcards", true) then module:hook("resource-bind", function (event) local session = event.session; local username = session.username; local vcard_temp = vcards:get(username); if not vcard_temp then session.log("debug", "No legacy vCard to migrate or already migrated"); return; end local pep_service = mod_pep.get_pep_service(username); vcard_temp = st.deserialize(vcard_temp); local vcard4, avatars = vcard_to_pep(vcard_temp); if pep_service:get_last_item("urn:xmpp:vcard4", true) then vcard4 = nil; end if pep_service:get_last_item("urn:xmpp:avatar:metadata", true) or pep_service:get_last_item("urn:xmpp:avatar:data", true) then avatars = nil; end if not (vcard4 or avatars) then session.log("debug", "Already PEP data, not overwriting with migrated data"); vcards:set(username, nil); return; end local ok, err = save_to_pep(pep_service, true, vcard4, avatars); if ok and vcards:set(username, nil) then session.log("info", "Migrated vCard-temp to PEP"); else session.log("info", "Failed to migrate vCard-temp to PEP: %s", err or "problem emptying 'vcard' store"); end end); end prosody-13.0.1/plugins/PaxHeaders/mod_version.lua0000644000000000000000000000011714773555365017110 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.659710806 prosody-13.0.1/plugins/mod_version.lua0000644000175000017500000000261314773555365021311 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "prosody.util.stanza"; module:add_feature("jabber:iq:version"); local query = st.stanza("query", {xmlns = "jabber:iq:version"}) :text_tag("name", "Prosody") :text_tag("version", prosody.version); if not module:get_option_boolean("hide_os_type") then local platform; if os.getenv("WINDIR") then platform = "Windows"; else local os_version_command = module:get_option_string("os_version_command"); local ok, pposix = pcall(require, "prosody.util.pposix"); if not os_version_command and (ok and pposix and pposix.uname) then 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"); if uname then platform = uname:read("*a"); end uname:close(); end end if platform then platform = platform:match("^%s*(.-)%s*$") or platform; query:text_tag("os", platform); end end module:hook("iq-get/host/jabber:iq:version:query", function(event) local origin, stanza = event.origin, event.stanza; origin.send(st.reply(stanza):add_child(query)); return true; end); prosody-13.0.1/plugins/PaxHeaders/mod_watchregistrations.lua0000644000000000000000000000011714773555365021347 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.659710806 prosody-13.0.1/plugins/mod_watchregistrations.lua0000644000175000017500000000236114773555365023550 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local host = module:get_host(); 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_enum("registration_notification_type", "chat", "normal", "headline"); local st = require "prosody.util.stanza"; module:hook("user-registered", function (user) module:log("debug", "Notifying of new registration"); local message = st.message{ type = msg_type, from = registration_from } :tag("body") :text(registration_notification:gsub("%$(%w+)", function (v) return user[v] or user.session and user.session[v] or nil; end)) :up(); for jid in registration_watchers do module:log("debug", "Notifying %s", jid); message.attr.to = jid; module:send(message); end end); prosody-13.0.1/plugins/PaxHeaders/mod_websocket.lua0000644000000000000000000000011714773555365017411 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.659710806 prosody-13.0.1/plugins/mod_websocket.lua0000644000175000017500000003123314773555365021612 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2012-2014 Florian Zeitz -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: ignore 431/log module:set_global(); 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 "prosody.util.dbuffer"; 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; local parse_close = websocket_frames.parse_close; local t_concat = table.concat; 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 module:log("info", "The 'cross_domain_websocket' option has been deprecated"); end local xmlns_framing = "urn:ietf:params:xml:ns:xmpp-framing"; local xmlns_streams = "http://etherx.jabber.org/streams"; local xmlns_client = "jabber:client"; local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; module:depends("c2s") local sessions = module:shared("c2s/sessions"); local c2s_listener = portmanager.get_service("c2s").listener; --- Session methods local function session_open_stream(session, from, to) local attr = { xmlns = xmlns_framing, ["xml:lang"] = "en", version = "1.0", id = session.streamid or "", from = from or session.host, to = to, }; if session.stream_attrs then session:stream_attrs(from, to, attr) end session.send(st.stanza("open", attr)); end local function session_close(session, reason) local log = session.log or log; local close_event_payload = { session = session, reason = reason }; module:context(session.host):fire_event("pre-session-close", close_event_payload); reason = close_event_payload.reason; if session.conn then if session.notopen then session:open_stream(); end if reason then -- nil == no err, initiated by us, false == initiated by client local stream_error = st.stanza("stream:error"); if type(reason) == "string" then -- assume stream error stream_error:tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' }); elseif st.is_stanza(reason) then stream_error = reason; elseif type(reason) == "table" then if reason.condition then stream_error:tag(reason.condition, stream_xmlns_attr):up(); if reason.text then stream_error:tag("text", stream_xmlns_attr):text(reason.text):up(); end if reason.extra then stream_error:add_child(reason.extra); end end end stream_error = tostring(stream_error); log("debug", "Disconnecting client, is: %s", stream_error); session.send(stream_error); end session.send(st.stanza("close", { xmlns = xmlns_framing })); function session.send() return false; end local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason; session.log("debug", "c2s stream for %s closed: %s", session.full_jid or session.ip or "", reason_text or "session closed"); -- Authenticated incoming stream may still be sending us stanzas, so wait for from remote local conn = session.conn; if reason_text == nil and not session.notopen and session.type == "c2s" then -- Grace time to process data from authenticated cleanly-closed stream add_task(stream_close_timeout, function () if not session.destroyed then session.log("warn", "Failed to receive a stream close response, closing connection anyway..."); sm_destroy_session(session, reason_text); if conn then conn:write(build_close(1000, "Stream closed")); conn:close(); end end end); else sm_destroy_session(session, reason_text); if conn then conn:write(build_close(1000, "Stream closed")); conn:close(); end end else local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason; sm_destroy_session(session, reason_text); end end --- Filters local function filter_open_close(data) if not data:find(xmlns_framing, 1, true) then return data; end local oc = parse_xml(data); if not oc then return data; end if oc.attr.xmlns ~= xmlns_framing then return data; end if oc.name == "close" then return ""; end if oc.name == "open" then oc.name = "stream:stream"; oc.attr.xmlns = nil; oc.attr["xmlns:stream"] = xmlns_streams; return oc:top_tag(); end return data; end local default_get_response_text = "It works! Now point your WebSocket client to this URL to connect to Prosody." local websocket_get_response_text = module:get_option_string("websocket_get_response_text", default_get_response_text) local default_get_response_body = [[Websocket

]]..websocket_get_response_text..[[

]] local websocket_get_response_body = module:get_option_string("websocket_get_response_body", default_get_response_body) local function validate_frame(frame, max_length) local opcode, length = frame.opcode, frame.length; if max_length and length > max_length then return false, 1009, "Payload too large"; end -- Error cases if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero return false, 1002, "Reserved bits not zero"; end if opcode == 0x8 and frame.data then -- close frame if length == 1 then return false, 1002, "Close frame with payload, but too short for status code"; elseif length >= 2 then local status_code = parse_close(frame.data) if status_code < 1000 then return false, 1002, "Closed with invalid status code"; elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then return false, 1002, "Closed with reserved status code"; end end end if opcode >= 0x8 then if length > 125 then -- Control frame with too much payload return false, 1002, "Payload too large"; end if not frame.FIN then -- Fragmented control frame return false, 1002, "Fragmented control frame"; end end if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then return false, 1002, "Reserved opcode"; end -- Check opcode if opcode == 0x2 then -- Binary frame return false, 1003, "Only text frames are supported, RFC 7395 3.2"; elseif opcode == 0x8 then -- Close request return false, 1000, "Goodbye"; end -- Other (XMPP-specific) validity checks if not frame.FIN then return false, 1003, "Continuation frames are not supported, RFC 7395 3.3.3"; end if opcode == 0x01 and frame.data and frame.data:byte(1, 1) ~= 60 then return false, 1007, "Invalid payload start character, RFC 7395 3.3.3"; end return true; end function handle_request(event) local request, response = event.request, event.response; local conn = response.conn; conn.starttls = false; -- Prevent mod_tls from believing starttls can be done if not request.headers.sec_websocket_key or request.method ~= "GET" then return module:fire_event("http-message", { response = event.response; --- title = "Prosody WebSocket endpoint"; message = websocket_get_response_text; warning = not (consider_websocket_secure or request.secure) and "This endpoint is not considered secure!" or nil; }) or websocket_get_response_body; end local wants_xmpp = contains_token(request.headers.sec_websocket_protocol or "", "xmpp"); if not wants_xmpp then module:log("debug", "Client didn't want to talk XMPP, list of protocols was %s", request.headers.sec_websocket_protocol or "(empty)"); return 501; end local function websocket_close(code, message) conn:write(build_close(code, message)); conn:close(); end local function websocket_handle_error(session, code, message) if code == 1009 then -- stanza size limit exceeded -- we close the session, rather than the connection, -- otherwise a resuming client will simply resend the -- offending stanza session:close({ condition = "policy-violation", text = "stanza too large" }); else websocket_close(code, message); end end local function handle_frame(frame) module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data); -- Check frame makes sense local frame_ok, err_status, err_text = validate_frame(frame, stanza_size_limit); if not frame_ok then return frame_ok, err_status, err_text; end local opcode = frame.opcode; if opcode == 0x9 then -- Ping frame frame.opcode = 0xA; frame.MASK = false; -- Clients send masked frames, servers don't, see #1484 conn:write(build_frame(frame)); return ""; elseif opcode == 0xA then -- Pong frame, MAY be sent unsolicited, eg as keepalive return ""; elseif opcode ~= 0x1 then -- Not text frame (which is all we support) log("warn", "Received frame with unsupported opcode %i", opcode); return ""; end return frame.data; end conn:setlistener(c2s_listener); c2s_listener.onconnect(conn); local session = sessions[conn]; -- Use upstream IP if a HTTP proxy was used -- See mod_http and #540 session.ip = request.ip; session.secure = consider_websocket_secure or request.secure or session.secure; session.websocket_request = request; session.open_stream = session_open_stream; session.close = session_close; local frameBuffer = dbuffer.new(frame_buffer_limit, frame_fragment_limit); add_filter(session, "bytes/in", function(data) if not frameBuffer:write(data) then session.log("warn", "websocket frame buffer full - terminating session"); session:close({ condition = "resource-constraint", text = "frame buffer exceeded" }); return; end local cache = {}; local frame, length, partial = parse_frame(frameBuffer); while frame do frameBuffer:discard(length); local result, err_status, err_text = handle_frame(frame); if not result then websocket_handle_error(session, err_status, err_text); break; end cache[#cache+1] = filter_open_close(result); frame, length, partial = parse_frame(frameBuffer); end if partial then -- The header of the next frame is already in the buffer, run -- some early validation here local frame_ok, err_status, err_text = validate_frame(partial, stanza_size_limit); if not frame_ok then websocket_handle_error(session, err_status, err_text); end end return t_concat(cache, ""); end); add_filter(session, "stanzas/out", function(stanza) stanza = st.clone(stanza); local attr = stanza.attr; attr.xmlns = attr.xmlns or xmlns_client; if stanza.name:find("^stream:") then attr["xmlns:stream"] = attr["xmlns:stream"] or xmlns_streams; end return stanza; end, -1000); add_filter(session, "bytes/out", function(data) return build_frame({ FIN = true, opcode = 0x01, data = tostring(data)}); end); response.status_code = 101; response.headers.upgrade = "websocket"; response.headers.connection = "Upgrade"; response.headers.sec_webSocket_accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")); response.headers.sec_webSocket_protocol = "xmpp"; module:fire_event("websocket-session", { session = session, request = request }); session.log("debug", "Sending WebSocket handshake"); return ""; end local function keepalive(event) local session = event.session; if session.open_stream == session_open_stream then return session.conn:write(build_frame({ opcode = 0x9, FIN = true })); end end function module.add_host(module) module:hook("c2s-read-timeout", keepalive, -0.9); module:depends("http"); module:provides("http", { name = "websocket"; default_path = "xmpp-websocket"; cors = { enabled = true; }; route = { ["GET"] = handle_request; ["GET /"] = handle_request; }; }); if module.host ~= "*" then module:depends("http_altconnect", true); end module:hook("c2s-read-timeout", keepalive, -0.9); end if require"prosody.core.modulemanager".get_modules_for_host("*"):contains(module.name) then module:add_host(); end prosody-13.0.1/plugins/PaxHeaders/mod_welcome.lua0000644000000000000000000000011714773555365017056 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.659710806 prosody-13.0.1/plugins/mod_welcome.lua0000644000175000017500000000127214773555365021257 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 "prosody.util.stanza"; module:hook("user-registered", function (user) local welcome_stanza = st.message({ to = user.username.."@"..user.host, from = host }, welcome_text:gsub("$(%w+)", user)); module:send(welcome_stanza); module:log("debug", "Welcomed user %s@%s", user.username, user.host); end); prosody-13.0.1/plugins/PaxHeaders/mod_windows.lua0000644000000000000000000000011714773555365017115 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.663710821 prosody-13.0.1/plugins/mod_windows.lua0000644000175000017500000000013014773555365021306 0ustar00prosodyprosody00000000000000-- Windows platform stub module:set_global(); -- TODO Add Windows-specific things here prosody-13.0.1/plugins/PaxHeaders/muc0000644000000000000000000000013114773555365014564 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.663710821 30 ctime=1743706869.679710885 prosody-13.0.1/plugins/muc/0000755000175000017500000000000014773555365017044 5ustar00prosodyprosody00000000000000prosody-13.0.1/plugins/muc/PaxHeaders/config_form_sections.lib.lua0000644000000000000000000000011714773555365022314 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.663710821 prosody-13.0.1/plugins/muc/config_form_sections.lib.lua0000644000175000017500000000106214773555365024512 0ustar00prosodyprosody00000000000000module:hook("muc-config-form", function(event) table.insert(event.form, { type = "fixed"; value = "Room information"; }); end, 100); module:hook("muc-config-form", function(event) table.insert(event.form, { type = "fixed"; value = "Access to the room"; }); end, 90); module:hook("muc-config-form", function(event) table.insert(event.form, { type = "fixed"; value = "Permissions in the room"; }); end, 80); module:hook("muc-config-form", function(event) table.insert(event.form, { type = "fixed"; value = "Other options"; }); end, 70); prosody-13.0.1/plugins/muc/PaxHeaders/description.lib.lua0000644000000000000000000000011714773555365020440 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.663710821 prosody-13.0.1/plugins/muc/description.lib.lua0000644000175000017500000000251614773555365022643 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local function get_description(room) return room._data.description; end local function set_description(room, description) if description == "" then description = nil; end if get_description(room) == description then return false; end room._data.description = description; return true; end local function add_disco_form(event) table.insert(event.form, { name = "muc#roominfo_description"; label = "Description"; value = ""; }); event.formdata["muc#roominfo_description"] = get_description(event.room); end local function add_form_option(event) table.insert(event.form, { name = "muc#roomconfig_roomdesc"; type = "text-single"; label = "Description"; desc = "A brief description of the room"; value = get_description(event.room) or ""; }); end module:hook("muc-disco#info", add_disco_form); module:hook("muc-config-form", add_form_option, 100-2); module:hook("muc-config-submitted/muc#roomconfig_roomdesc", function(event) if set_description(event.room, event.value) then event.status_codes["104"] = true; end end); return { get = get_description; set = set_description; }; prosody-13.0.1/plugins/muc/PaxHeaders/hats.lib.lua0000644000000000000000000000011714773555365017054 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.663710821 prosody-13.0.1/plugins/muc/hats.lib.lua0000644000175000017500000000245714773555365021263 0ustar00prosodyprosody00000000000000local st = require "prosody.util.stanza"; local muc_util = module:require "muc/util"; local hats_compat = module:get_option_boolean("muc_hats_compat", false); -- COMPAT for pre-XEP namespace 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); module:hook("muc-build-occupant-presence", function (event) local bare_jid = event.occupant and event.occupant.bare_jid or event.bare_jid; local aff_data = event.room:get_affiliation_data(bare_jid); 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); prosody-13.0.1/plugins/muc/PaxHeaders/hidden.lib.lua0000644000000000000000000000011714773555365017350 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.663710821 prosody-13.0.1/plugins/muc/hidden.lib.lua0000644000175000017500000000310414773555365021545 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local restrict_public = not module:get_option_boolean("muc_room_allow_public", true); module:default_permission(restrict_public and "prosody:admin" or "prosody:registered", ":create-public-room"); local function get_hidden(room) return room._data.hidden; end local function set_hidden(room, hidden) hidden = hidden and true or nil; if get_hidden(room) == hidden then return false; end room._data.hidden = hidden; return true; end module:hook("muc-config-form", function(event) 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, { name = "muc#roomconfig_publicroom"; type = "boolean"; label = "Include room information in public lists"; desc = "Enable this to allow people to find the room"; value = not get_hidden(event.room); }); end, 100-9); module:hook("muc-config-submitted/muc#roomconfig_publicroom", function(event) if not module:may(":create-public-room", event.actor) then return; -- Not allowed end if set_hidden(event.room, not event.value) then event.status_codes["104"] = true; end end); module:hook("muc-disco#info", function(event) event.reply:tag("feature", {var = get_hidden(event.room) and "muc_hidden" or "muc_public"}):up(); end); return { get = get_hidden; set = set_hidden; }; prosody-13.0.1/plugins/muc/PaxHeaders/history.lib.lua0000644000000000000000000000011714773555365017616 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.663710821 prosody-13.0.1/plugins/muc/history.lib.lua0000644000175000017500000001557214773555365022027 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local gettime = os.time; local datetime = require "prosody.util.datetime"; local st = require "prosody.util.stanza"; local default_history_length = 20; 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; end local function get_historylength(room) return math.min(room._data.history_length or default_history_length, max_history_length); end local function set_historylength(room, length) if length then length = assert(tonumber(length), "Length not a valid number"); end if length == default_history_length then length = nil; end room._data.history_length = length; return true; end -- Fix for clients who don't support XEP-0045 correctly -- Default number of history messages the room returns local function get_defaulthistorymessages(room) return room._data.default_history_messages or default_history_length; end local function set_defaulthistorymessages(room, number) number = math.min(tonumber(number) or default_history_length, room._data.history_length or default_history_length); if number == default_history_length then number = nil; end room._data.default_history_messages = number; end module:hook("muc-config-form", function(event) table.insert(event.form, { name = "muc#roomconfig_historylength"; type = "text-single"; datatype = "xs:integer"; label = "Maximum number of history messages returned by room"; desc = "Specify the maximum number of previous messages that should be sent to users when they join the room"; value = get_historylength(event.room); }); table.insert(event.form, { name = 'muc#roomconfig_defaulthistorymessages', type = 'text-single', datatype = "xs:integer"; label = 'Default number of history messages returned by room', desc = "Specify the number of previous messages sent to new users when they join the room"; value = get_defaulthistorymessages(event.room); }); end, 70-5); module:hook("muc-config-submitted/muc#roomconfig_historylength", function(event) if set_historylength(event.room, event.value) then event.status_codes["104"] = true; end end); module:hook("muc-config-submitted/muc#roomconfig_defaulthistorymessages", function(event) if set_defaulthistorymessages(event.room, event.value) then event.status_codes["104"] = true; end end); local function parse_history(stanza) local x_tag = stanza:get_child("x", "http://jabber.org/protocol/muc"); local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc"); if not history_tag then return nil, nil, nil; end local maxchars = tonumber(history_tag.attr.maxchars); local maxstanzas = tonumber(history_tag.attr.maxstanzas); -- messages received since the UTC datetime specified local since = history_tag.attr.since; if since then since = datetime.parse(since); end -- messages received in the last "X" seconds. local seconds = tonumber(history_tag.attr.seconds); if seconds then seconds = gettime() - seconds; if since then since = math.max(since, seconds); else since = seconds; end end return maxchars, maxstanzas, since; end module:hook("muc-get-history", function(event) local room = event.room; local history = room._history; -- send discussion history if not history then return nil end local history_len = #history; local to = event.to; local maxchars = event.maxchars; local maxstanzas = event.maxstanzas or history_len; local since = event.since; local n = 0; local charcount = 0; for i=history_len,1,-1 do local entry = history[i]; if maxchars then if not entry.chars then entry.stanza.attr.to = ""; entry.chars = #tostring(entry.stanza); end charcount = charcount + entry.chars + #to; if charcount > maxchars then break; end end if since and since > entry.timestamp then break; end if n + 1 > maxstanzas then break; end n = n + 1; end local i = history_len-n+1 function event.next_stanza() if i > history_len then return nil end local entry = history[i]; local msg = entry.stanza; msg.attr.to = to; i = i + 1; return msg; end return true; end, -1); local function send_history(room, stanza) local maxchars, maxstanzas, since = parse_history(stanza); if not(maxchars or maxstanzas or since) then maxstanzas = get_defaulthistorymessages(room); end local event = { room = room; stanza = stanza; to = stanza.attr.from; -- `to` is required to calculate the character count for `maxchars` maxchars = maxchars, maxstanzas = maxstanzas, since = since; next_stanza = function() end; -- events should define this iterator }; module:fire_event("muc-get-history", event); for msg in event.next_stanza, event do room:route_stanza(msg); end end -- Send history on join module:hook("muc-occupant-session-new", function(event) send_history(event.room, event.stanza); end, 50); -- Before subject(20) -- add to history module:hook("muc-add-history", function(event) local room = event.room if get_historylength(room) == 0 then room._history = nil; return; end local history = room._history; if not history then history = {}; room._history = history; end local stanza = st.clone(event.stanza); stanza.attr.to = ""; local ts = gettime(); local stamp = datetime.datetime(ts); stanza:tag("delay", { -- XEP-0203 xmlns = "urn:xmpp:delay", from = room.jid, stamp = stamp }):up(); local entry = { stanza = stanza, timestamp = ts }; table.insert(history, entry); while #history > get_historylength(room) do table.remove(history, 1) end return true; end, -1); -- Have a single muc-add-history event, so that plugins can mark it -- as handled without stopping other muc-broadcast-message handlers module:hook("muc-broadcast-message", function(event) if module:fire_event("muc-message-is-historic", event) then module:fire_event("muc-add-history", event); end end); module:hook("muc-message-is-historic", function (event) local stanza = event.stanza; if stanza:get_child("no-store", "urn:xmpp:hints") or stanza:get_child("no-permanent-store", "urn:xmpp:hints") then return false, "hint"; end if stanza:get_child("store", "urn:xmpp:hints") then return true, "hint"; end if stanza:get_child("body") then return true; end if stanza:get_child("encryption", "urn:xmpp:eme:0") then -- Since we can't know what an encrypted message contains, we assume it's important -- XXX Experimental XEP return true, "encrypted"; end if stanza:get_child(nil, "urn:xmpp:chat-markers:0") then return true, "marker"; end end, -1); return { set_max_length = set_max_history_length; parse_history = parse_history; send = send_history; get_length = get_historylength; set_length = set_historylength; }; prosody-13.0.1/plugins/muc/PaxHeaders/language.lib.lua0000644000000000000000000000011714773555365017700 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.667710837 prosody-13.0.1/plugins/muc/language.lib.lua0000644000175000017500000000250714773555365022103 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local function get_language(room) return room._data.language; end local function set_language(room, language) if language == "" then language = nil; end if get_language(room) == language then return false; end room._data.language = language; return true; end local function add_disco_form(event) table.insert(event.form, { name = "muc#roominfo_lang"; value = ""; }); event.formdata["muc#roominfo_lang"] = get_language(event.room); end local function add_form_option(event) table.insert(event.form, { name = "muc#roomconfig_lang"; label = "Language tag for room (e.g. 'en', 'de', 'fr' etc.)"; type = "text-single"; desc = "Indicate the primary language spoken in this room"; datatype = "xs:language"; value = get_language(event.room) or ""; }); end module:hook("muc-disco#info", add_disco_form); module:hook("muc-config-form", add_form_option, 100-3); module:hook("muc-config-submitted/muc#roomconfig_lang", function(event) if set_language(event.room, event.value) then event.status_codes["104"] = true; end end); return { get = get_language; set = set_language; }; prosody-13.0.1/plugins/muc/PaxHeaders/lock.lib.lua0000644000000000000000000000011714773555365017045 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.667710837 prosody-13.0.1/plugins/muc/lock.lib.lua0000644000175000017500000000331314773555365021244 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "prosody.util.stanza"; local lock_rooms = module:get_option_boolean("muc_room_locking", true); 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;}); room._data.locked = os.time() + lock_room_timeout; end local function unlock(room) module:fire_event("muc-room-unlocked", {room = room;}); room._data.locked = nil; end local function is_locked(room) local ts = room._data.locked; if ts then if os.time() < ts then return true; end unlock(room); end return false; end if lock_rooms then module:hook("muc-room-pre-create", function(event) -- Older groupchat protocol doesn't lock if not event.stanza:get_child("x", "http://jabber.org/protocol/muc") then return end -- Lock room at creation local room = event.room; lock(room); end, 10); end -- Don't let users into room while it is locked module:hook("muc-occupant-pre-join", function(event) if not event.is_new_room and is_locked(event.room) then -- Deny entry module:log("debug", "Room is locked, denying entry"); event.origin.send(st.error_reply(event.stanza, "cancel", "item-not-found", nil, module.host)); return true; end end, -30); -- When config is submitted; unlock the room module:hook("muc-config-submitted", function(event) if is_locked(event.room) then unlock(event.room); end end, -1); return { lock = lock; unlock = unlock; is_locked = is_locked; }; prosody-13.0.1/plugins/muc/PaxHeaders/members_only.lib.lua0000644000000000000000000000011714773555365020610 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.667710837 prosody-13.0.1/plugins/muc/members_only.lib.lua0000644000175000017500000001410514773555365023010 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "prosody.util.stanza"; local muc_util = module:require "muc/util"; local valid_affiliations = muc_util.valid_affiliations; local function get_members_only(room) return room._data.members_only; end local function set_members_only(room, members_only) members_only = members_only and true or nil; if room._data.members_only == members_only then return false; end room._data.members_only = members_only; if members_only then --[[ If as a result of a change in the room configuration the room type is changed to members-only but there are non-members in the room, the service MUST remove any non-members from the room and include a status code of 322 in the presence unavailable stanzas sent to those users as well as any remaining occupants. ]] local occupants_changed = {}; for _, occupant in room:each_occupant() do local affiliation = room:get_affiliation(occupant.bare_jid); if valid_affiliations[affiliation or "none"] <= valid_affiliations.none then occupant.role = nil; room:save_occupant(occupant); occupants_changed[occupant] = true; end end local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) :tag("status", {code="322"}):up(); for occupant in pairs(occupants_changed) do room:publicise_occupant_status(occupant, x); module:fire_event("muc-occupant-left", {room = room; nick = occupant.nick; occupant = occupant;}); end end return true; end local function get_allow_member_invites(room) return room._data.allow_member_invites; end -- Allows members to invite new members into a members-only room, -- effectively creating an invite-only room local function set_allow_member_invites(room, allow_member_invites) allow_member_invites = allow_member_invites and true or nil; if room._data.allow_member_invites == allow_member_invites then return false; end room._data.allow_member_invites = allow_member_invites; return true; end module:hook("muc-disco#info", function(event) local members_only_room = not not get_members_only(event.room); local members_can_invite = not not get_allow_member_invites(event.room); event.reply:tag("feature", {var = members_only_room and "muc_membersonly" or "muc_open"}):up(); table.insert(event.form, { name = "{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites"; label = "Allow members to invite new members"; type = "boolean"; value = members_can_invite; }); table.insert(event.form, { name = "muc#roomconfig_allowinvites"; label = "Allow users to invite other users"; type = "boolean"; value = not members_only_room or members_can_invite; }); end); module:hook("muc-config-form", function(event) table.insert(event.form, { name = "muc#roomconfig_membersonly"; type = "boolean"; label = "Only allow members to join"; desc = "Enable this to only allow access for room owners, admins and members"; value = get_members_only(event.room); }); table.insert(event.form, { name = "{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites"; type = "boolean"; label = "Allow members to invite new members"; value = get_allow_member_invites(event.room); }); end, 90-3); module:hook("muc-config-submitted/muc#roomconfig_membersonly", function(event) if set_members_only(event.room, event.value) then event.status_codes["104"] = true; end end); module:hook("muc-config-submitted/{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", function(event) if set_allow_member_invites(event.room, event.value) then event.status_codes["104"] = true; end end); -- No affiliation => role of "none" module:hook("muc-get-default-role", function(event) if not event.affiliation and get_members_only(event.room) then return false; end end, 2); -- registration required for entering members-only room module:hook("muc-occupant-pre-join", function(event) local room = event.room; if get_members_only(room) then local stanza = event.stanza; local affiliation = room:get_affiliation(stanza.attr.from); if valid_affiliations[affiliation or "none"] <= valid_affiliations.none then local reply = st.error_reply(stanza, "auth", "registration-required", nil, room.jid):up(); event.origin.send(reply); return true; end end end, -5); -- Invitation privileges in members-only rooms SHOULD be restricted to room admins; -- if a member without privileges to edit the member list attempts to invite another user -- the service SHOULD return a error to the occupant module:hook("muc-pre-invite", function(event) local room = event.room; if get_members_only(room) then local stanza = event.stanza; local inviter_affiliation = room:get_affiliation(stanza.attr.from) or "none"; local required_affiliation = room._data.allow_member_invites and "member" or "admin"; if valid_affiliations[inviter_affiliation] < valid_affiliations[required_affiliation] then event.origin.send(st.error_reply(stanza, "auth", "forbidden", nil, room.jid)); return true; end end end); -- When an invite is sent; add an affiliation for the invitee module:hook("muc-invite", function(event) local room = event.room; if get_members_only(room) then local stanza = event.stanza; local invitee = stanza.attr.to; local affiliation = room:get_affiliation(invitee); local invited_unaffiliated = valid_affiliations[affiliation or "none"] <= valid_affiliations.none; if invited_unaffiliated then local from = stanza:get_child("x", "http://jabber.org/protocol/muc#user") :get_child("invite").attr.from; module:log("debug", "%s invited %s into members only room %s, granting membership", from, invitee, room.jid); -- This might fail; ignore for now room:set_affiliation(true, invitee, "member", "Invited by " .. from); room:save(); end end end); return { get = get_members_only; set = set_members_only; get_allow_member_invites = get_allow_member_invites; set_allow_member_invites = set_allow_member_invites; }; prosody-13.0.1/plugins/muc/PaxHeaders/mod_muc.lua0000644000000000000000000000011714773555365016773 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.667710837 prosody-13.0.1/plugins/muc/mod_muc.lua0000644000175000017500000005201214773555365021172 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- Exposed functions: -- -- create_room(jid) -> room -- track_room(room) -- delete_room(room) -- forget_room(room) -- get_room_from_jid(jid) -> room -- each_room(live_only) -> () -> room [DEPRECATED] -- all_rooms() -> room -- live_rooms() -> room -- shutdown_component() if module:get_host_type() ~= "component" then error("MUC should be loaded as a component, please see https://prosody.im/doc/components", 0); end local muclib = module:require "muc"; room_mt = muclib.room_mt; -- Yes, global. new_room = muclib.new_room; local name = module:require "muc/name"; room_mt.get_name = name.get; room_mt.set_name = name.set; local description = module:require "muc/description"; room_mt.get_description = description.get; room_mt.set_description = description.set; local language = module:require "muc/language"; room_mt.get_language = language.get; room_mt.set_language = language.set; local hidden = module:require "muc/hidden"; room_mt.get_hidden = hidden.get; room_mt.set_hidden = hidden.set; function room_mt:get_public() return not self:get_hidden(); end function room_mt:set_public(public) return self:set_hidden(not public); end local password = module:require "muc/password"; room_mt.get_password = password.get; room_mt.set_password = password.set; local members_only = module:require "muc/members_only"; room_mt.get_members_only = members_only.get; room_mt.set_members_only = members_only.set; room_mt.get_allow_member_invites = members_only.get_allow_member_invites; room_mt.set_allow_member_invites = members_only.set_allow_member_invites; local moderated = module:require "muc/moderated"; room_mt.get_moderated = moderated.get; room_mt.set_moderated = moderated.set; local request = module:require "muc/request"; room_mt.handle_role_request = request.handle_request; local persistent = module:require "muc/persistent"; room_mt.get_persistent = persistent.get; room_mt.set_persistent = persistent.set; local subject = module:require "muc/subject"; room_mt.get_changesubject = subject.get_changesubject; room_mt.set_changesubject = subject.set_changesubject; room_mt.get_subject = subject.get; room_mt.set_subject = subject.set; room_mt.send_subject = subject.send; local history = module:require "muc/history"; room_mt.send_history = history.send; room_mt.get_historylength = history.get_length; room_mt.set_historylength = history.set_length; local register = module:require "muc/register"; 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; -- 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 "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"; module:depends("disco"); module:add_identity("conference", "text", module:get_option_string("name", "Prosody Chatrooms")); module:add_feature("http://jabber.org/protocol/muc"); module:depends "muc_unique" module:require "muc/hats"; module:require "muc/lock"; if module:get_option_boolean("muc_vcard", true) ~= false then module:require "muc/vcard"; 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", false) then -- Monkey patch to make server admins room owners local _get_affiliation = room_mt.get_affiliation; function room_mt:get_affiliation(jid) 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 module:could(":automatic-ownership", jid) then return nil, "modify", "not-acceptable"; end return _set_affiliation(self, actor, jid, affiliation, reason, data); end end local persistent_rooms_storage = module:open_store("persistent"); local persistent_rooms = module:open_store("persistent", "map"); local room_configs = module:open_store("config"); local room_state = module:open_store("state"); local room_items_cache = {}; local function room_save(room, forced, savestate) local node = jid_split(room.jid); local is_persistent = persistent.get(room); if room:get_public() then room_items_cache[room.jid] = room:get_name() or ""; else room_items_cache[room.jid] = nil; end if is_persistent or savestate then persistent_rooms:set(nil, room.jid, true); local data, state = room:freeze(savestate); room_state:set(node, state); return room_configs:set(node, data); elseif forced then persistent_rooms:set(nil, room.jid, nil); room_state:set(node, nil); return room_configs:set(node, nil); end end 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") local room_eviction = module:measure("room_eviction", "rate"); local rooms = cache.new(max_rooms or max_live_rooms, function (jid, room) if max_rooms then module:log("info", "Room limit of %d reached, no new rooms allowed", max_rooms); return false; end module:log("debug", "Evicting room %s", jid); room_eviction(); if room:get_public() then room_items_cache[room.jid] = room:get_name() or ""; else room_items_cache[room.jid] = nil; end local ok, err = room_save(room, nil, true); -- Force to disk if not ok then module:log("error", "Failed to swap inactive room %s to disk: %s", jid, err); return false; end end); local measure_rooms_size = module:measure("live_room", "amount"); module:hook_global("stats-update", function () measure_rooms_size(rooms:count()); end); -- Automatically destroy empty non-persistent rooms module:hook("muc-occupant-left",function(event) local room = event.room if room.destroying then return end if not room:has_occupant() and not persistent.get(room) then -- empty, non-persistent room module:log("debug", "%q empty, destroying", room.jid); module:fire_event("muc-room-destroyed", { room = room }); end end, -1); function track_room(room) if rooms:set(room.jid, room) then -- When room is created, over-ride 'save' method room.save = room_save; return room; end -- Resource limit reached return false; end local function handle_broken_room(room, origin, stanza) module:log("debug", "Returning error from broken room %s", room.jid); origin.send(st.error_reply(stanza, "wait", "internal-server-error", nil, room.jid)); return true; end local function restore_room(jid) local node = jid_split(jid); local data, err = room_configs:get(node); if data then module:log("debug", "Restoring room %s from storage", jid); if module:fire_event("muc-room-pre-restore", { jid = jid, data = data }) == false then return false; end local state, s_err = room_state:get(node); if not state and s_err then module:log("debug", "Could not restore state of room %s: %s", jid, s_err); end local room = muclib.restore_room(data, state); if track_room(room) then room_state:set(node, nil); module:fire_event("muc-room-restored", { jid = jid, room = room }); return room; else return false; end elseif err then module:log("error", "Error restoring room %s from storage: %s", jid, err); local room = muclib.new_room(jid, { locked = math.huge }); room.handle_normal_presence = handle_broken_room; room.handle_first_presence = handle_broken_room; return room; end end -- Removes a room from memory, without saving it (save first if required) function forget_room(room) module:log("debug", "Forgetting %s", room.jid); rooms.save = nil; rooms:set(room.jid, nil); end -- Removes a room from the database (may remain in memory) function delete_room(room) module:log("debug", "Deleting %s", room.jid); room_configs:set(jid_split(room.jid), nil); room_state:set(jid_split(room.jid), nil); persistent_rooms:set(nil, room.jid, nil); room_items_cache[room.jid] = nil; end function module.unload() for room in live_rooms() do room:save(nil, true); forget_room(room); end end function get_room_from_jid(room_jid) local room = rooms:get(room_jid); if room then room_hit(); rooms:set(room_jid, room); -- bump to top; return room; end room_miss(); return restore_room(room_jid); end 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_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_changesubject(module:get_option_boolean("muc_room_default_change_subject", room:get_changesubject())); 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_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) if jid_bare(room_jid) ~= room_jid or not jid_prep(room_jid, true) then return nil, "invalid-jid"; end local exists = get_room_from_jid(room_jid); if exists then return nil, "room-exists"; end local room = muclib.new_room(room_jid, config); if not config then set_room_defaults(room); end module:fire_event("muc-room-created", { room = room; }); return track_room(room); end function all_rooms() return coroutine.wrap(function () local seen = {}; -- Don't iterate over persistent rooms twice for room in live_rooms() do coroutine.yield(room); seen[room.jid] = true; end local all_persistent_rooms, err = persistent_rooms_storage:get(nil); if not all_persistent_rooms then if err then module:log("error", "Error loading list of persistent rooms, only rooms live in memory were iterated over"); module:log("debug", "%s", debug.traceback(err)); end return nil; end for room_jid in pairs(all_persistent_rooms) do if not seen[room_jid] then local room = restore_room(room_jid); if room then coroutine.yield(room); else module:log("error", "Missing data for room '%s', omitting from iteration", room_jid); end end end end); end function live_rooms() return rooms:values(); end function each_room(live_only) if live_only then return live_rooms(); end return all_rooms(); end module:hook("host-disco-items", function(event) module:log("debug", "host-disco-items called"); if not module:could(":list-rooms", event) then module:log("debug", "Returning empty room list to unauthorized request"); return; end local reply = event.reply; 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 reply:tag("item", { jid = jid, name = room_name }):up(); end else for room in all_rooms() do if not room:get_hidden() then local jid, room_name = room.jid, room:get_name(); room_items_cache[jid] = room_name or ""; reply:tag("item", { jid = jid, name = room_name }):up(); end end end end); module:hook("muc-room-pre-create", function (event) set_room_defaults(event.room, event.stanza.attr["xml:lang"]); end, 1); module:hook("muc-room-pre-create", function(event) local origin, stanza = event.origin, event.stanza; if not track_room(event.room) then origin.send(st.error_reply(stanza, "wait", "resource-constraint", nil, module.host)); return true; end end, -1000); module:hook("muc-room-destroyed",function(event) local room = event.room; forget_room(room); delete_room(room); end); if module:get_option_boolean("muc_tombstones", true) then local ttl = module:get_option_period("muc_tombstone_expiry", "31 days"); module:hook("muc-room-destroyed",function(event) local room = event.room; if not room:get_persistent() then return end if room._data.destroyed then return -- Allow destruction of tombstone end local tombstone = new_room(room.jid, { locked = os.time() + ttl; destroyed = true; reason = event.reason; newjid = event.newjid; -- password? }); tombstone.save = room_save; tombstone:set_persistent(true); tombstone:set_hidden(true); tombstone:save(true); return true; end, -10); 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); for event_name, method in pairs { -- Normal room interactions ["iq-get/bare/http://jabber.org/protocol/disco#info:query"] = "handle_disco_info_get_query" ; ["iq-get/bare/http://jabber.org/protocol/disco#items:query"] = "handle_disco_items_get_query" ; ["iq-set/bare/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ; ["iq-get/bare/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_get_command" ; ["iq-set/bare/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ; ["iq-get/bare/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_get_to_room" ; ["message/bare"] = "handle_message_to_room" ; ["presence/bare"] = "handle_presence_to_room" ; ["iq/bare/jabber:iq:register:query"] = "handle_register_iq"; -- Host room ["iq-get/host/http://jabber.org/protocol/disco#info:query"] = "handle_disco_info_get_query" ; ["iq-get/host/http://jabber.org/protocol/disco#items:query"] = "handle_disco_items_get_query" ; ["iq-set/host/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ; ["iq-get/host/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_get_command" ; ["iq-set/host/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ; ["iq-get/host/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_get_to_room" ; ["message/host"] = "handle_message_to_room" ; ["presence/host"] = "handle_presence_to_room" ; -- Direct to occupant (normal rooms and host room) ["presence/full"] = "handle_presence_to_occupant" ; ["iq/full"] = "handle_iq_to_occupant" ; ["message/full"] = "handle_message_to_occupant" ; } do module:hook(event_name, function (event) local origin, stanza = event.origin, event.stanza; local room_jid = jid_bare(stanza.attr.to); local room = get_room_from_jid(room_jid); if room and room._data.destroyed then if room._data.locked < os.time() 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; else if stanza.attr.type ~= "error" then local reply = st.error_reply(stanza, "cancel", "gone", room._data.reason, module.host) if room._data.newjid then local uri = "xmpp:"..room._data.newjid.."?join"; reply:get_child("error"):child_with_name("gone"):text(uri); end event.origin.send(reply); end return true; end end if room == nil then -- Watch presence to create rooms if not jid_prep(room_jid, true) then origin.send(st.error_reply(stanza, "modify", "jid-malformed", nil, module.host)); return true; end if stanza.attr.type == nil and stanza.name == "presence" and stanza:get_child("x", "http://jabber.org/protocol/muc") then room = muclib.new_room(room_jid); return room:handle_first_presence(origin, stanza); elseif stanza.attr.type ~= "error" then origin.send(st.error_reply(stanza, "cancel", "item-not-found", nil, module.host)); return true; else return; end elseif room == false then -- Error loading room origin.send(st.error_reply(stanza, "wait", "resource-constraint", nil, module.host)); return true; end return room[method](room, origin, stanza); end, -2) end function shutdown_component() for room in live_rooms() do room:save(nil, true); end end module:hook_global("server-stopping", shutdown_component, -300); do -- Ad-hoc commands module:depends "adhoc"; local t_concat = table.concat; local adhoc_new = module:require "adhoc".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"; instructions = "Select the rooms to destroy"; { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#destroy" }; { name = "rooms", type = "list-multi", required = true, label = "Rooms to destroy:"}; }; local destroy_rooms_handler = adhoc_initial(destroy_rooms_layout, function() return { rooms = array.collect(all_rooms()):pluck("jid"):sort(); }; end, function(fields, errors) if errors then local errmsg = {}; for field, err in pairs(errors) do errmsg[#errmsg + 1] = field .. ": " .. err; end return { status = "completed", error = { message = t_concat(errmsg, "\n") } }; end local destroyed = array(); for _, room_jid in ipairs(fields.rooms) do local room = get_room_from_jid(room_jid); if room and room:destroy() then destroyed:push(room.jid); end end return { status = "completed", info = "The following rooms were destroyed:\n"..t_concat(destroyed, "\n") }; end); local destroy_rooms_desc = adhoc_new("Destroy Rooms", "http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin"); module:provides("adhoc", destroy_rooms_desc); local set_affiliation_layout = dataforms_new { -- FIXME wordsmith title, instructions, labels etc title = "Set affiliation"; { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#set-affiliation" }; { name = "room", type = "jid-single", required = true, label = "Room"}; { name = "jid", type = "jid-single", required = true, label = "JID"}; { name = "affiliation", type = "list-single", required = true, label = "Affiliation", options = { "owner"; "admin"; "member"; "none"; "outcast"; }, }; { name = "reason", type = "text-single", "Reason", } }; local set_affiliation_handler = adhoc_simple(set_affiliation_layout, function (fields, errors) if errors then local errmsg = {}; for field, err in pairs(errors) do errmsg[#errmsg + 1] = field .. ": " .. err; end return { status = "completed", error = { message = t_concat(errmsg, "\n") } }; end local room = get_room_from_jid(fields.room); if not room then return { status = "canceled", error = { message = "No such room"; }; }; end local ok, err, condition = room:set_affiliation(true, fields.jid, fields.affiliation, fields.reason); if not ok then return { status = "canceled", error = { message = "Affiliation change failed: "..err..":"..condition; }; }; end return { status = "completed", info = "Affiliation updated", }; end); local set_affiliation_desc = adhoc_new("Set affiliation in room", "http://prosody.im/protocol/muc#set-affiliation", set_affiliation_handler, "admin"); module:provides("adhoc", set_affiliation_desc); end prosody-13.0.1/plugins/muc/PaxHeaders/moderated.lib.lua0000644000000000000000000000011714773555365020061 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.667710837 prosody-13.0.1/plugins/muc/moderated.lib.lua0000644000175000017500000000275514773555365022271 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local function get_moderated(room) return room._data.moderated; end local function set_moderated(room, moderated) moderated = moderated and true or nil; if get_moderated(room) == moderated then return false; end room._data.moderated = moderated; return true; end module:hook("muc-disco#info", function(event) event.reply:tag("feature", {var = get_moderated(event.room) and "muc_moderated" or "muc_unmoderated"}):up(); end); module:hook("muc-config-form", function(event) table.insert(event.form, { name = "muc#roomconfig_moderatedroom"; type = "boolean"; label = "Moderated (require permission to speak)"; desc = "In moderated rooms occupants must be given permission to speak by a room moderator"; value = get_moderated(event.room); }); end, 80-3); module:hook("muc-config-submitted/muc#roomconfig_moderatedroom", function(event) if set_moderated(event.room, event.value) then event.status_codes["104"] = true; end end); module:hook("muc-get-default-role", function(event) if event.affiliation == nil then if get_moderated(event.room) then -- XEP-0045: -- An implementation MAY grant voice by default to visitors in unmoderated rooms. return "visitor" end end end, 1); return { get = get_moderated; set = set_moderated; }; prosody-13.0.1/plugins/muc/PaxHeaders/muc.lib.lua0000644000000000000000000000011714773555365016701 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.671710854 prosody-13.0.1/plugins/muc/muc.lib.lua0000644000175000017500000017005014773555365021103 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local select = select; local pairs = pairs; local next = next; local setmetatable = setmetatable; 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; local occupant_lib = module:require "muc/occupant" local muc_util = module:require "muc/util"; local is_kickable_error = muc_util.is_kickable_error; local valid_roles, valid_affiliations = muc_util.valid_roles, muc_util.valid_affiliations; local room_mt = {}; room_mt.__index = room_mt; function room_mt:__tostring() return "MUC room ("..self.jid..")"; end function room_mt.save() -- overridden by mod_muc.lua end function room_mt:get_occupant_jid(real_jid) return self._jid_nick[real_jid] end function room_mt:get_default_role(affiliation) local role = module:fire_event("muc-get-default-role", { room = self; affiliation = affiliation; affiliation_rank = valid_affiliations[affiliation or "none"]; }); role = role ~= "none" and role or nil; -- coerces `role == false` to `nil` return role, valid_roles[role or "none"]; end module:hook("muc-get-default-role", function(event) if event.affiliation_rank >= valid_affiliations.admin then return "moderator"; elseif event.affiliation_rank >= valid_affiliations.none then return "participant"; end end, -1); --- Occupant functions function room_mt:new_occupant(bare_real_jid, nick) local occupant = occupant_lib.new(bare_real_jid, nick); local affiliation = self:get_affiliation(bare_real_jid); occupant.role = self:get_default_role(affiliation); return occupant; end -- nick is in the form of an in-room JID function room_mt:get_occupant_by_nick(nick) local occupant = self._occupants[nick]; if occupant == nil then return nil end return occupant_lib.copy(occupant); end do local function next_copied_occupant(occupants, occupant_jid) local next_occupant_jid, raw_occupant = next(occupants, occupant_jid); if next_occupant_jid == nil then return nil end return next_occupant_jid, occupant_lib.copy(raw_occupant); end -- FIXME Explain what 'read_only' is supposed to be function room_mt:each_occupant(read_only) -- luacheck: ignore 212 return next_copied_occupant, self._occupants, nil; end end function room_mt:has_occupant() return next(self._occupants, nil) ~= nil end function room_mt:get_occupant_by_real_jid(real_jid) local occupant_jid = self:get_occupant_jid(real_jid); if occupant_jid == nil then return nil end return self:get_occupant_by_nick(occupant_jid); end function room_mt:save_occupant(occupant) occupant = occupant_lib.copy(occupant); -- So that occupant can be modified more local id = occupant.nick -- Need to maintain _jid_nick secondary index local old_occupant = self._occupants[id]; if old_occupant then for real_jid in old_occupant:each_session() do self._jid_nick[real_jid] = nil; end end local has_live_session = false if occupant.role ~= nil then for real_jid, presence in occupant:each_session() do if presence.attr.type == nil then has_live_session = true self._jid_nick[real_jid] = occupant.nick; end end if not has_live_session then -- Has no live sessions left; they have left the room. occupant.role = nil end end if not has_live_session then occupant = nil end self._occupants[id] = occupant return occupant end function room_mt:route_to_occupant(occupant, stanza) local to = stanza.attr.to; for jid in occupant:each_session() do stanza.attr.to = jid; self:route_stanza(stanza); end stanza.attr.to = to; end -- actor is the attribute table local function add_item(x, affiliation, role, jid, nick, actor_nick, actor_jid, reason) x:tag("item", {affiliation = affiliation or "none"; role = role; jid = jid; nick = nick;}) if actor_nick or actor_jid then x:tag("actor", {nick = actor_nick; jid = actor_jid;}):up() end if reason then x:tag("reason"):text(reason):up() end x:up(); return x end -- actor is (real) jid function room_mt:build_item_list(occupant, x, is_anonymous, nick, actor_nick, actor_jid, reason) local affiliation = self:get_affiliation(occupant.bare_jid) or "none"; local role = occupant.role or "none"; if is_anonymous then add_item(x, affiliation, role, nil, nick, actor_nick, actor_jid, reason); else for real_jid in occupant:each_session() do add_item(x, affiliation, role, real_jid, nick, actor_nick, actor_jid, reason); end end return x end function room_mt:broadcast_message(stanza) if module:fire_event("muc-broadcast-message", {room = self, stanza = stanza}) then return true; end self:broadcast(stanza); return true; end -- Strip delay tags claiming to be from us module:hook("muc-occupant-groupchat", function (event) local stanza = event.stanza; local room = event.room; local room_jid = room.jid; stanza:maptags(function (child) if child.name == "delay" and child.attr["xmlns"] == "urn:xmpp:delay" then if child.attr["from"] == room_jid then return nil; end end if child.name == "x" and child.attr["xmlns"] == "jabber:x:delay" then if child.attr["from"] == room_jid then return nil; end end return child; end) end); -- Broadcast a stanza to all occupants in the room. -- optionally checks conditional called with (nick, occupant) function room_mt:broadcast(stanza, cond_func) for nick, occupant in self:each_occupant() do if cond_func == nil or cond_func(nick, occupant) then self:route_to_occupant(occupant, stanza) end end end local function can_see_real_jids(whois, occupant) if whois == "anyone" then return true; elseif whois == "moderators" then return valid_roles[occupant.role or "none"] >= valid_roles.moderator; end end -- Broadcasts an occupant's presence to the whole room -- Takes the x element that goes into the stanzas function room_mt:publicise_occupant_status(occupant, x, nick, actor, reason, prev_role, force_unavailable, recipient) local base_x = x.base or x; -- Build real jid and (optionally) occupant jid template presences local base_presence do -- Try to use main jid's presence local pr = occupant:get_presence(); if pr and (occupant.role ~= nil or pr.attr.type == "unavailable") and not force_unavailable then base_presence = st.clone(pr); else -- user is leaving but didn't send a leave presence. make one for them base_presence = st.presence {from = occupant.nick; type = "unavailable";}; end end -- Fire event (before full_p and anon_p are created) local event = { room = self; stanza = base_presence; x = base_x; occupant = occupant; nick = nick; actor = actor; reason = reason; } module:fire_event("muc-build-occupant-presence", event); if not recipient then module:fire_event("muc-broadcast-presence", event); end -- Allow muc-broadcast-presence listeners to change things nick = event.nick; actor = event.actor; reason = event.reason; local whois = self:get_whois(); local actor_nick; if actor then actor_nick = jid_resource(self:get_occupant_jid(actor)); end local full_p, full_x; local function get_full_p() if full_p == nil then full_x = st.clone(x.full or base_x); self:build_item_list(occupant, full_x, false, nick, actor_nick, actor, reason); full_p = st.clone(base_presence):add_child(full_x); end return full_p, full_x; end local anon_p, anon_x; local function get_anon_p() if anon_p == nil then anon_x = st.clone(x.anon or base_x); self:build_item_list(occupant, anon_x, true, nick, actor_nick, nil, reason); anon_p = st.clone(base_presence):add_child(anon_x); end return anon_p, anon_x; end local self_p, self_x; do -- Can always see your own full jids -- But not allowed to see actor's self_x = st.clone(x.self or base_x); self:build_item_list(occupant, self_x, false, nick, actor_nick, nil, reason); self_p = st.clone(base_presence):add_child(self_x); end local function get_p(rec_occupant) local pr; if can_see_real_jids(whois, rec_occupant) then pr = get_full_p(); elseif occupant.bare_jid == rec_occupant.bare_jid then pr = self_p; else pr = get_anon_p(); end return pr end if recipient then return self:route_to_occupant(recipient, get_p(recipient)); end local broadcast_roles = self:get_presence_broadcast(); -- General populace for occupant_nick, n_occupant in self:each_occupant() do if occupant_nick ~= occupant.nick then if broadcast_roles[occupant.role or "none"] or force_unavailable then self:route_to_occupant(n_occupant, get_p(n_occupant)); elseif prev_role and broadcast_roles[prev_role] then local pr = get_p(n_occupant); pr.attr.type = 'unavailable'; self:route_to_occupant(n_occupant, pr); end end end -- Presences for occupant itself self_x:tag("status", {code = "110";}):up(); if occupant.role == nil then -- They get an unavailable self:route_to_occupant(occupant, self_p); else -- use their own presences as templates for full_jid, pr in occupant:each_session() do pr = st.clone(pr); module:fire_event("muc-build-occupant-presence", { room = self, occupant = occupant, stanza = pr }); pr.attr.to = full_jid; pr:add_child(self_x); self:route_stanza(pr); end end end function room_mt:send_occupant_list(to, filter) local to_bare = jid_bare(to); local broadcast_roles = self:get_presence_broadcast(); local is_anonymous = self:is_anonymous_for(to); local broadcast_bare_jids = {}; -- Track which bare JIDs we have sent presence for for occupant_jid, occupant in self:each_occupant() do broadcast_bare_jids[occupant.bare_jid] = true; if (filter == nil or filter(occupant_jid, occupant)) and (to_bare == occupant.bare_jid or broadcast_roles[occupant.role or "none"]) then local x = st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'}); self:build_item_list(occupant, x, is_anonymous and to_bare ~= occupant.bare_jid); -- can always see your own jids local pres = st.clone(occupant:get_presence()); pres.attr.to = to; pres:add_child(x); module:fire_event("muc-build-occupant-presence", { room = self, occupant = occupant, stanza = pres }); self:route_stanza(pres); end end if broadcast_roles.none then -- Broadcast stanzas for affiliated users not currently in the MUC for affiliated_jid, affiliation, affiliation_data in self:each_affiliation() do local nick = affiliation_data and affiliation_data.reserved_nickname; if (nick or not is_anonymous) and not broadcast_bare_jids[affiliated_jid] and (filter == nil or filter(affiliated_jid, nil)) then local from = nick and (self.jid.."/"..nick) or self.jid; local pres = st.presence({ to = to, from = from, type = "unavailable" }) :tag("x", { xmlns = 'http://jabber.org/protocol/muc#user' }) :tag("item", { affiliation = affiliation; role = "none"; nick = nick; jid = not is_anonymous and affiliated_jid or nil }):up() :up(); self:route_stanza(pres); end end end end function room_mt:get_disco_info(stanza) local node = stanza.tags[1].attr.node; local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node }); local event_name = "muc-disco#info"; local event_data = { room = self, reply = reply, stanza = stanza }; if node and node ~= "" then event_name = event_name.."/"..node; else event_data.form = dataform.new { { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }; }; event_data.formdata = {}; end module:fire_event(event_name, event_data); if event_data.form then reply:add_child(event_data.form:form(event_data.formdata, "result")); end return reply; end module:hook("muc-disco#info", function(event) event.reply:tag("feature", {var = "http://jabber.org/protocol/muc"}):up(); event.reply:tag("feature", {var = "http://jabber.org/protocol/muc#stable_id"}):up(); event.reply:tag("feature", {var = "http://jabber.org/protocol/muc#self-ping-optimization"}):up(); end); module:hook("muc-disco#info", function(event) table.insert(event.form, { name = "muc#roominfo_occupants", label = "Number of occupants" }); event.formdata["muc#roominfo_occupants"] = tostring(iterators.count(event.room:each_occupant())); end); function room_mt:get_disco_items(stanza) -- luacheck: ignore 212 return st.reply(stanza):query("http://jabber.org/protocol/disco#items"); end function room_mt:handle_kickable(origin, stanza) -- luacheck: ignore 212 local real_jid = stanza.attr.from; local occupant = self:get_occupant_by_real_jid(real_jid); if occupant == nil then return nil; end local _, condition, text = stanza:get_error(); local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error"); if text and self:get_whois() == "anyone" then error_message = error_message..": "..text; end occupant:set_session(real_jid, st.presence({type="unavailable"}) :tag('status'):text(error_message)); local orig_role = occupant.role; local is_last_session = occupant.jid == real_jid; if is_last_session then occupant.role = nil; end local new_occupant = self:save_occupant(occupant); local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); if is_last_session then x:tag("status", {code = "333"}); end self:publicise_occupant_status(new_occupant or occupant, x, nil, nil, nil, orig_role); if is_last_session then module:fire_event("muc-occupant-left", { room = self; nick = occupant.nick; occupant = occupant; }); end return true; end -- Give the room creator owner affiliation module:hook("muc-room-pre-create", function(event) event.room:set_affiliation(true, jid_bare(event.stanza.attr.from), "owner"); end, -1); -- check if user is banned module:hook("muc-occupant-pre-join", function(event) local room, stanza = event.room, event.stanza; local affiliation = room:get_affiliation(stanza.attr.from); if affiliation == "outcast" then local reply = st.error_reply(stanza, "auth", "forbidden", nil, room.jid):up(); event.origin.send(reply); return true; end end, -10); module:hook("muc-occupant-pre-join", function(event) local room = event.room; local nick = jid_resource(event.occupant.nick); if not nick:find("%S") then event.origin.send(st.error_reply(event.stanza, "modify", "not-allowed", "Invisible Nicknames are forbidden", room.jid)); return true; end end, 1); module:hook("muc-occupant-pre-change", function(event) local room = event.room; if not jid_resource(event.dest_occupant.nick):find("%S") then event.origin.send(st.error_reply(event.stanza, "modify", "not-allowed", "Invisible Nicknames are forbidden", room.jid)); return true; end end, 1); module:hook("muc-occupant-pre-join", function(event) local room = event.room; local nick = jid_resource(event.occupant.nick); if not resourceprep(nick, true) then -- strict event.origin.send(st.error_reply(event.stanza, "modify", "jid-malformed", "Nickname must pass strict validation", room.jid)); return true; end end, 2); module:hook("muc-occupant-pre-change", function(event) local room = event.room; local nick = jid_resource(event.dest_occupant.nick); if not resourceprep(nick, true) then -- strict event.origin.send(st.error_reply(event.stanza, "modify", "jid-malformed", "Nickname must pass strict validation", room.jid)); return true; end end, 2); function room_mt:handle_first_presence(origin, stanza) local real_jid = stanza.attr.from; local dest_jid = stanza.attr.to; local bare_jid = jid_bare(real_jid); if module:fire_event("muc-room-pre-create", { room = self; origin = origin; stanza = stanza; }) then return true; end local is_first_dest_session = true; local dest_occupant = self:new_occupant(bare_jid, dest_jid); local orig_nick = dest_occupant.nick; if module:fire_event("muc-occupant-pre-join", { room = self; origin = origin; stanza = stanza; is_first_session = is_first_dest_session; is_new_room = true; occupant = dest_occupant; }) then return true; end local nick_changed = orig_nick ~= dest_occupant.nick; dest_occupant:set_session(real_jid, stanza); local dest_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); dest_x:tag("status", {code = "201"}):up(); if self:get_whois() == "anyone" then dest_x:tag("status", {code = "100"}):up(); end if nick_changed then dest_x:tag("status", {code = "210"}):up(); end self:save_occupant(dest_occupant); self:publicise_occupant_status(dest_occupant, dest_x); module:fire_event("muc-occupant-joined", { room = self; nick = dest_occupant.nick; occupant = dest_occupant; stanza = stanza; origin = origin; }); module:fire_event("muc-occupant-session-new", { room = self; nick = dest_occupant.nick; occupant = dest_occupant; stanza = stanza; origin = origin; jid = real_jid; }); module:fire_event("muc-room-created", { room = self; creator = dest_occupant; stanza = stanza; origin = origin; }); return true; end function room_mt:is_anonymous_for(jid) local is_anonymous = false; local whois = self:get_whois(); if whois ~= "anyone" then local affiliation = self:get_affiliation(jid); if affiliation ~= "admin" and affiliation ~= "owner" then local occupant = self:get_occupant_by_real_jid(jid); if not (occupant and can_see_real_jids(whois, occupant)) then is_anonymous = true; end end end return is_anonymous; end function room_mt:build_unavailable_presence(from_muc_jid, to_jid) local nick = jid_resource(from_muc_jid); local from_jid = self:get_registered_jid(nick); if (not from_jid) then module:log("debug", "Received presence probe for unavailable nickname that's not registered"); return; end local is_anonymous = self:is_anonymous_for(to_jid); local affiliation = self:get_affiliation(from_jid) or "none"; local pr = st.presence({ to = to_jid, from = from_muc_jid, type = "unavailable" }) :tag("x", { xmlns = 'http://jabber.org/protocol/muc#user' }) :tag("item", { affiliation = affiliation; role = "none"; nick = nick; jid = not is_anonymous and from_jid or nil }):up() :up(); local x = pr:get_child("x", "http://jabber.org/protocol/muc"); local event = { room = self; stanza = pr; x = x; bare_jid = from_jid; nick = nick; } module:fire_event("muc-build-occupant-presence", event); return event.stanza; end function room_mt:respond_to_probe(origin, stanza, probing_occupant) if probing_occupant == nil then origin.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not currently connected to this chat", self.jid)); return; end local from_muc_jid = stanza.attr.to; local probed_occupant = self:get_occupant_by_nick(from_muc_jid); if probed_occupant == nil then local to_jid = stanza.attr.from; local pr = self:build_unavailable_presence(from_muc_jid, to_jid); if pr then self:route_stanza(pr); end return; end local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}); self:publicise_occupant_status(probed_occupant, x, nil, nil, nil, nil, false, probing_occupant); end function room_mt:handle_normal_presence(origin, stanza) local type = stanza.attr.type; local real_jid = stanza.attr.from; local bare_jid = jid_bare(real_jid); local orig_occupant = self:get_occupant_by_real_jid(real_jid); local muc_x = stanza:get_child("x", "http://jabber.org/protocol/muc"); if orig_occupant == nil and not muc_x and stanza.attr.type == nil then module:log("debug", "Attempted join without , possibly desynced"); origin.send(st.error_reply(stanza, "cancel", "item-not-found", "You are not currently connected to this chat", self.jid)); return true; end local is_first_dest_session; local dest_occupant; if type == "unavailable" then if orig_occupant == nil then return true; end -- Unavailable from someone not in the room -- dest_occupant = nil elseif type == "probe" then self:respond_to_probe(origin, stanza, orig_occupant) return true; elseif orig_occupant and orig_occupant.nick == stanza.attr.to then -- Just a presence update log("debug", "presence update for %s from session %s", orig_occupant.nick, real_jid); dest_occupant = orig_occupant; else local dest_jid = stanza.attr.to; dest_occupant = self:get_occupant_by_nick(dest_jid); if dest_occupant == nil then log("debug", "no occupant found for %s; creating new occupant object for %s", dest_jid, real_jid); is_first_dest_session = true; dest_occupant = self:new_occupant(bare_jid, dest_jid); if orig_occupant then dest_occupant.role = orig_occupant.role; end else is_first_dest_session = false; end end local is_last_orig_session; if orig_occupant ~= nil then -- Is there are least 2 sessions? local iter, ob, last = orig_occupant:each_session(); is_last_orig_session = iter(ob, iter(ob, last)) == nil; end local orig_nick = dest_occupant and dest_occupant.nick; local event, event_name = { room = self; origin = origin; stanza = stanza; is_first_session = is_first_dest_session; is_last_session = is_last_orig_session; }; if orig_occupant == nil then event_name = "muc-occupant-pre-join"; event.occupant = dest_occupant; elseif dest_occupant == nil then event_name = "muc-occupant-pre-leave"; event.occupant = orig_occupant; else event_name = "muc-occupant-pre-change"; event.orig_occupant = orig_occupant; event.dest_occupant = dest_occupant; end if module:fire_event(event_name, event) then return true; end local nick_changed = dest_occupant and orig_nick ~= dest_occupant.nick; -- Check for nick conflicts if dest_occupant ~= nil and not is_first_dest_session and bare_jid ~= jid_bare(dest_occupant.bare_jid) then -- new nick or has different bare real jid log("debug", "%s couldn't join due to nick conflict: %s", real_jid, dest_occupant.nick); local reply = st.error_reply(stanza, "cancel", "conflict", nil, self.jid):up(); origin.send(reply); return true; end -- Send presence stanza about original occupant if orig_occupant ~= nil and orig_occupant ~= dest_occupant then local orig_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); local orig_role = orig_occupant.role; local dest_nick; if dest_occupant == nil then -- Session is leaving log("debug", "session %s is leaving occupant %s", real_jid, orig_occupant.nick); if is_last_orig_session then orig_occupant.role = nil; end orig_occupant:set_session(real_jid, stanza); else log("debug", "session %s is changing from occupant %s to %s", real_jid, orig_occupant.nick, dest_occupant.nick); local generated_unavail = st.presence {from = orig_occupant.nick, to = real_jid, type = "unavailable"}; orig_occupant:set_session(real_jid, generated_unavail); dest_nick = jid_resource(dest_occupant.nick); if not is_first_dest_session then -- User is swapping into another pre-existing session log("debug", "session %s is swapping into multisession %s, showing it leave.", real_jid, dest_occupant.nick); -- Show the other session leaving local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); add_item(x, self:get_affiliation(bare_jid), "none"); local pr = st.presence{from = dest_occupant.nick, to = real_jid, type = "unavailable"} :tag("status"):text("you are joining pre-existing session " .. dest_nick):up() :add_child(x); self:route_stanza(pr); end if is_first_dest_session and is_last_orig_session then -- Normal nick change log("debug", "no sessions in %s left; publicly marking as nick change", orig_occupant.nick); orig_x:tag("status", {code = "303";}):up(); else -- The session itself always needs to see a nick change -- don't want to get our old nick's available presence, -- so remove our session from there, and manually generate an unavailable orig_occupant:remove_session(real_jid); log("debug", "generating nick change for %s", real_jid); local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); -- COMPAT: clients get confused if they see other items besides their own -- self:build_item_list(orig_occupant, x, false, dest_nick); add_item(x, self:get_affiliation(bare_jid), orig_occupant.role, real_jid, dest_nick); x:tag("status", {code = "303";}):up(); x:tag("status", {code = "110";}):up(); self:route_stanza(generated_unavail:add_child(x)); dest_nick = nil; -- set dest_nick to nil; so general populace doesn't see it for whole orig_occupant end end self:save_occupant(orig_occupant); self:publicise_occupant_status(orig_occupant, orig_x, dest_nick, nil, nil, orig_role); if is_last_orig_session then module:fire_event("muc-occupant-left", { room = self; nick = orig_occupant.nick; occupant = orig_occupant; origin = origin; stanza = stanza; }); end end if dest_occupant ~= nil then dest_occupant:set_session(real_jid, stanza); self:save_occupant(dest_occupant); if orig_occupant == nil or muc_x then -- Send occupant list to newly joined or desynced user self:send_occupant_list(real_jid, function(nick, occupant) -- luacheck: ignore 212 -- Don't include self return (not occupant) or occupant:get_presence(real_jid) == nil; end) end local dest_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); local self_x = st.clone(dest_x); if orig_occupant == nil and self:get_whois() == "anyone" then self_x:tag("status", {code = "100"}):up(); end if nick_changed then self_x:tag("status", {code="210"}):up(); end self:publicise_occupant_status(dest_occupant, {base=dest_x,self=self_x}, nil, nil, nil, orig_occupant and orig_occupant.role or nil); if orig_occupant ~= nil and orig_occupant ~= dest_occupant and not is_last_orig_session then -- If user is swapping and wasn't last original session log("debug", "session %s split nicks; showing %s rejoining", real_jid, orig_occupant.nick); -- Show the original nick joining again local pr = st.clone(orig_occupant:get_presence()); pr.attr.to = real_jid; local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); self:build_item_list(orig_occupant, x, false); -- TODO: new status code to inform client this was the multi-session it left? pr:add_child(x); self:route_stanza(pr); end if orig_occupant == nil or muc_x then if is_first_dest_session then module:fire_event("muc-occupant-joined", { room = self; nick = dest_occupant.nick; occupant = dest_occupant; stanza = stanza; origin = origin; }); end module:fire_event("muc-occupant-session-new", { room = self; nick = dest_occupant.nick; occupant = dest_occupant; stanza = stanza; origin = origin; jid = real_jid; }); end end return true; end function room_mt:handle_presence_to_occupant(origin, stanza) local type = stanza.attr.type; if type == "error" then -- error, kick em out! return self:handle_kickable(origin, stanza) elseif type == nil or type == "unavailable" or type == "probe" then return self:handle_normal_presence(origin, stanza); elseif type ~= 'result' then -- bad type if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences origin.send(st.error_reply(stanza, "modify", "bad-request", nil, self.jid)); -- FIXME correct error? end end return true; end function room_mt:handle_iq_to_occupant(origin, stanza) local from, to = stanza.attr.from, stanza.attr.to; local type = stanza.attr.type; local id = stanza.attr.id; local occupant = self:get_occupant_by_nick(to); if (type == "error" or type == "result") then do -- deconstruct_stanza_id if not occupant then return nil; end local from_jid, orig_id, to_jid_hash = (base64.decode(id) or ""):match("^(%Z+)%z(%Z*)%z(.+)$"); if not(from == from_jid or from == jid_bare(from_jid)) then return nil; end local from_occupant_jid = self:get_occupant_jid(from_jid); if from_occupant_jid == nil then return nil; end local session_jid local salt = self:get_salt(); for to_jid in occupant:each_session() do if hmac_sha256(salt, to_jid):sub(1,8) == to_jid_hash then session_jid = to_jid; break; end end if session_jid == nil then return nil; end stanza.attr.from, stanza.attr.to, stanza.attr.id = from_occupant_jid, session_jid, orig_id; end log("debug", "%s sent private iq stanza to %s (%s)", from, to, stanza.attr.to); self:route_stanza(stanza); stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; return true; else -- Type is "get" or "set" local current_nick = self:get_occupant_jid(from); if not current_nick then origin.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not currently connected to this chat", self.jid)); return true; end if not occupant then -- recipient not in room origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room", self.jid)); return true; end -- XEP-0410 MUC Self-Ping #1220 if to == current_nick and stanza.attr.type == "get" and stanza:get_child("ping", "urn:xmpp:ping") then self:route_stanza(st.reply(stanza)); return true; end do -- construct_stanza_id local salt = self:get_salt(); stanza.attr.id = base64.encode(occupant.jid.."\0"..stanza.attr.id.."\0"..hmac_sha256(salt, from):sub(1,8)); end stanza.attr.from, stanza.attr.to = current_nick, occupant.jid; log("debug", "%s sent private iq stanza to %s (%s)", from, to, occupant.jid); local iq_ns = stanza.tags[1].attr.xmlns; if iq_ns == 'vcard-temp' or iq_ns == "http://jabber.org/protocol/pubsub" or iq_ns == "urn:ietf:params:xml:ns:vcard-4.0" then stanza.attr.to = jid_bare(stanza.attr.to); end self:route_stanza(stanza); stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; return true; end end function room_mt:handle_message_to_occupant(origin, stanza) local from, to = stanza.attr.from, stanza.attr.to; local current_nick = self:get_occupant_jid(from); local type = stanza.attr.type; if not current_nick then -- not in room if type ~= "error" then origin.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not currently connected to this chat", self.jid)); end return true; end if type == "groupchat" then -- groupchat messages not allowed in PM origin.send(st.error_reply(stanza, "modify", "bad-request", nil, self.jid)); return true; elseif type == "error" and is_kickable_error(stanza) then log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid); return self:handle_kickable(origin, stanza); -- send unavailable end local o_data = self:get_occupant_by_nick(to); if not o_data then origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room", self.jid)); return true; end log("debug", "%s sent private message stanza to %s (%s)", from, to, o_data.jid); stanza = muc_util.filter_muc_x(st.clone(stanza)); stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up(); stanza.attr.from = current_nick; if module:fire_event("muc-private-message", { room = self, origin = origin, stanza = stanza }) ~= false then self:route_to_occupant(o_data, stanza) end -- TODO: Remove x tag? stanza.attr.from = from; return true; end function room_mt:send_form(origin, stanza) origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") :add_child(self:get_form_layout(stanza.attr.from):form()) ); end function room_mt:get_form_layout(actor) local form = dataform.new({ title = "Configuration for "..self.jid, instructions = "Complete and submit this form to configure the room.", { name = 'FORM_TYPE', type = 'hidden', value = 'http://jabber.org/protocol/muc#roomconfig' } }); return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form; end function room_mt:process_form(origin, stanza) local form = stanza.tags[1]:get_child("x", "jabber:x:data"); if form.attr.type == "cancel" then origin.send(st.reply(stanza)); elseif form.attr.type == "submit" then -- luacheck: ignore 231/errors local fields, errors, present; if form.tags[1] == nil then -- Instant room fields, present = {}, {}; else -- FIXME handle form errors fields, errors, present = self:get_form_layout(stanza.attr.from):data(form); if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); return true; end end local event = { room = self; origin = origin; stanza = stanza; fields = fields; status_codes = {}; actor = stanza.attr.from; }; function event.update_option(name, field, allowed) local new = fields[field]; if new == nil then return; end if allowed and not allowed[new] then return; end if new == self["get_"..name](self) then return; end event.status_codes["104"] = true; self["set_"..name](self, new); return true; end module:fire_event("muc-config-submitted", event); for submitted_field in pairs(present) do event.field, event.value = submitted_field, fields[submitted_field]; module:fire_event("muc-config-submitted/"..submitted_field, event); end event.field, event.value = nil, nil; self:save(true); origin.send(st.reply(stanza)); if next(event.status_codes) then local msg = st.message({type='groupchat', from=self.jid}) :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) for code in pairs(event.status_codes) do msg:tag("status", {code = code;}):up(); end msg:up(); self:broadcast_message(msg); end else origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); end return true; end -- Removes everyone from the room function room_mt:clear(x) x = x or st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'}); local occupants_updated = {}; for nick, occupant in self:each_occupant() do -- luacheck: ignore 213 local prev_role = occupant.role; occupant.role = nil; self:save_occupant(occupant); occupants_updated[occupant] = prev_role; end for occupant, prev_role in pairs(occupants_updated) do self:publicise_occupant_status(occupant, x, nil, nil, nil, prev_role); module:fire_event("muc-occupant-left", { room = self; nick = occupant.nick; occupant = occupant; }); end end function room_mt:destroy(newjid, reason, password) local x = st.stanza("x", { xmlns = "http://jabber.org/protocol/muc#user" }); local event = { room = self; newjid = newjid; reason = reason; password = password; x = x, allowed = true }; module:fire_event("muc-pre-room-destroy", event); if not event.allowed then return false, event.error; end newjid, reason, password = event.newjid, event.reason, event.password; x:tag("destroy", { jid = newjid }); if reason then x:tag("reason"):text(reason):up(); end if password then x:tag("password"):text(password):up(); end x:up(); self.destroying = reason or true; self:clear(x); module:fire_event("muc-room-destroyed", { room = self, reason = reason, newjid = newjid, password = password }); return true; end function room_mt:handle_disco_info_get_query(origin, stanza) origin.send(self:get_disco_info(stanza)); return true; end function room_mt:handle_disco_items_get_query(origin, stanza) origin.send(self:get_disco_items(stanza)); return true; end function room_mt:handle_admin_query_set_command(origin, stanza) local item = stanza.tags[1].tags[1]; if not item then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return true; end if item.attr.jid then -- Validate provided JID item.attr.jid = jid_prep(item.attr.jid); if not item.attr.jid then origin.send(st.error_reply(stanza, "modify", "jid-malformed")); return true; elseif jid_resource(item.attr.jid) then origin.send(st.error_reply(stanza, "modify", "jid-malformed", "Bare JID expected, got full JID")); return true; end end if item.attr.nick then -- Validate provided nick item.attr.nick = resourceprep(item.attr.nick); if not item.attr.nick then origin.send(st.error_reply(stanza, "modify", "jid-malformed", "invalid nickname")); return true; end end if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation local occupant = self:get_occupant_by_nick(self.jid.."/"..item.attr.nick); if occupant then item.attr.jid = occupant.bare_jid; end elseif item.attr.role and not item.attr.nick and item.attr.jid then -- Role changes should use nick, but we have a JID so pull the nick from that local nick = self:get_occupant_jid(item.attr.jid); if nick then item.attr.nick = jid_resource(nick); end end local actor = stanza.attr.from; 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 = 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); if existing_occupant and existing_occupant.bare_jid ~= item.attr.jid then module:log("debug", "Existing occupant for %s: %s does not match %s", room_nick, existing_occupant.bare_jid, item.attr.jid); 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; 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 success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, reason); else success, errtype, err = nil, "cancel", "bad-request"; end self:save(true); if not success then origin.send(st.error_reply(stanza, errtype, err)); else origin.send(st.reply(stanza)); end return true; end function room_mt:handle_admin_query_get_command(origin, stanza) local actor = stanza.attr.from; local affiliation = self:get_affiliation(actor); local item = stanza.tags[1].tags[1]; local _aff = item.attr.affiliation; local _aff_rank = valid_affiliations[_aff or "none"]; local _rol = item.attr.role; if _aff and _aff_rank and not _rol then -- You need to be at least an admin, and be requesting info about your affiliation or lower -- e.g. an admin can't ask for a list of owners local affiliation_rank = valid_affiliations[affiliation or "none"]; if (affiliation_rank >= valid_affiliations.admin and affiliation_rank >= _aff_rank) 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, _, 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 }); if data and data.reason then reply:text_tag("reason", data.reason); end reply:up(); end origin.send(reply:up()); return true; else origin.send(st.error_reply(stanza, "auth", "forbidden")); return true; end elseif _rol and valid_roles[_rol or "none"] and not _aff then local role = self:get_role(self:get_occupant_jid(actor)) or self:get_default_role(affiliation); if valid_roles[role or "none"] >= valid_roles.moderator then if _rol == "none" then _rol = nil; end local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); -- TODO: whois check here? (though fully anonymous rooms are not supported) for occupant_jid, occupant in self:each_occupant() do if occupant.role == _rol then local nick = jid_resource(occupant_jid); self:build_item_list(occupant, reply, false, nick); end end origin.send(reply:up()); return true; else origin.send(st.error_reply(stanza, "auth", "forbidden")); return true; end else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return true; end end function room_mt:handle_owner_query_get_to_room(origin, stanza) if self:get_affiliation(stanza.attr.from) ~= "owner" then origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms")); return true; end self:send_form(origin, stanza); return true; end function room_mt:handle_owner_query_set_to_room(origin, stanza) if self:get_affiliation(stanza.attr.from) ~= "owner" then origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms")); return true; end local child = stanza.tags[1].tags[1]; if not child then origin.send(st.error_reply(stanza, "modify", "bad-request")); return true; elseif child.name == "destroy" then local newjid = child.attr.jid; local reason = child:get_child_text("reason"); local password = child:get_child_text("password"); local destroyed, err = self:destroy(newjid, reason, password); if destroyed then origin.send(st.reply(stanza)); else origin.send(st.error_reply(stanza, err or "cancel", "not-allowed")); end return true; elseif child.name == "x" and child.attr.xmlns == "jabber:x:data" then return self:process_form(origin, stanza); else origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return true; end end function room_mt:handle_groupchat_to_room(origin, stanza) local from = stanza.attr.from; local occupant = self:get_occupant_by_real_jid(from); if not stanza.attr.id then stanza.attr.id = new_id() end local event_data = {room = self; origin = origin; stanza = stanza; from = from; occupant = occupant}; if module:fire_event("muc-occupant-groupchat", event_data) then return true; end if event_data.occupant then stanza.attr.from = event_data.occupant.nick; else stanza.attr.from = self.jid; end self:broadcast_message(stanza); stanza.attr.from = from; return true; end -- Role check module:hook("muc-occupant-groupchat", function(event) local role_rank = valid_roles[event.occupant and event.occupant.role or "none"]; if role_rank <= valid_roles.none then event.origin.send(st.error_reply(event.stanza, "cancel", "not-acceptable", "You are not currently connected to this chat")); return true; elseif role_rank <= valid_roles.visitor then event.origin.send(st.error_reply(event.stanza, "auth", "forbidden", "You do not currently have permission to speak in this chat")); return true; end end, 50); -- hack - some buggy clients send presence updates to the room rather than their nick function room_mt:handle_presence_to_room(origin, stanza) local current_nick = self:get_occupant_jid(stanza.attr.from); local handled if current_nick then local to = stanza.attr.to; stanza.attr.to = current_nick; handled = self:handle_presence_to_occupant(origin, stanza); stanza.attr.to = to; end return handled; end -- Need visitor role or higher to invite module:hook("muc-pre-invite", function(event) local room, stanza = event.room, event.stanza; local _from = stanza.attr.from; local inviter = room:get_occupant_by_real_jid(_from); local role = inviter and inviter.role or room:get_default_role(room:get_affiliation(_from)); if valid_roles[role or "none"] <= valid_roles.visitor then event.origin.send(st.error_reply(stanza, "auth", "forbidden")); return true; end end); function room_mt:handle_mediated_invite(origin, stanza) local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite"); local invitee = jid_prep(payload.attr.to); if not invitee then origin.send(st.error_reply(stanza, "cancel", "jid-malformed")); return true; elseif module:fire_event("muc-pre-invite", {room = self, origin = origin, stanza = stanza}) then return true; end local invite = muc_util.filter_muc_x(st.clone(stanza)); invite.attr.from = self.jid; invite.attr.to = invitee; invite:tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) :tag('invite', {from = stanza.attr.from;}) :tag('reason'):text(payload:get_child_text("reason")):up() :up() :up(); if not module:fire_event("muc-invite", {room = self, stanza = invite, origin = origin, incoming = stanza}) then self:route_stanza(invite); end return true; end -- COMPAT: Some older clients expect this module:hook("muc-invite", function(event) local room, stanza = event.room, event.stanza; local invite = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite"); local reason = invite:get_child_text("reason"); stanza:tag('x', {xmlns = "jabber:x:conference"; jid = room.jid;}) :text(reason or "") :up(); end); -- Add a plain message for clients which don't support invites module:hook("muc-invite", function(event) local room, stanza = event.room, event.stanza; if not stanza:get_child("body") then local invite = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite"); local reason = invite:get_child_text("reason") or ""; stanza:tag("body") :text(invite.attr.from.." invited you to the room "..room.jid..(reason ~= "" and (" ("..reason..")") or "")) :up(); end end); function room_mt:handle_mediated_decline(origin, stanza) local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("decline"); local declinee = jid_prep(payload.attr.to); if not declinee then origin.send(st.error_reply(stanza, "cancel", "jid-malformed")); return true; elseif module:fire_event("muc-pre-decline", {room = self, origin = origin, stanza = stanza}) then return true; end local decline = muc_util.filter_muc_x(st.clone(stanza)); decline.attr.from = self.jid; decline.attr.to = declinee; decline:tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) :tag("decline", {from = stanza.attr.from}) :tag("reason"):text(payload:get_child_text("reason")):up() :up() :up(); if not module:fire_event("muc-decline", {room = self, stanza = decline, origin = origin, incoming = stanza}) then declinee = decline.attr.to; -- re-fetch, in case event modified it local occupant if jid_bare(declinee) == self.jid then -- declinee jid is already an in-room jid occupant = self:get_occupant_by_nick(declinee); end if occupant then self:route_to_occupant(occupant, decline); else self:route_stanza(decline); end end return true; end -- Add a plain message for clients which don't support declines module:hook("muc-decline", function(event) local room, stanza = event.room, event.stanza; if not stanza:get_child("body") then local decline = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("decline"); local reason = decline:get_child_text("reason") or ""; stanza:body(decline.attr.from.." declined your invite to the room " ..room.jid..(reason ~= "" and (" ("..reason..")") or "")); end end); function room_mt:handle_message_to_room(origin, stanza) local type = stanza.attr.type; if type == "groupchat" then return self:handle_groupchat_to_room(origin, stanza) elseif type == "error" and is_kickable_error(stanza) then return self:handle_kickable(origin, stanza) elseif type == nil or type == "normal" then local x = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); if x then local payload = x.tags[1]; if payload == nil then --luacheck: ignore 542 -- fallthrough elseif payload.name == "invite" and payload.attr.to then return self:handle_mediated_invite(origin, stanza) elseif payload.name == "decline" and payload.attr.to then return self:handle_mediated_decline(origin, stanza) end origin.send(st.error_reply(stanza, "cancel", "bad-request")); return true; end local form = stanza:get_child("x", "jabber:x:data"); local form_type = dataform.get_type(form); if form_type == "http://jabber.org/protocol/muc#request" then self:handle_role_request(origin, stanza, form); return true; end end end function room_mt:route_stanza(stanza) -- luacheck: ignore 212 module:send(stanza); end function room_mt:get_affiliation(jid) local node, host = jid_split(jid); -- Affiliations are granted, revoked, and maintained based on the user's bare JID. local bare = node and node.."@"..host or host; local result = self._affiliations[bare]; if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned return result; end -- Iterates over jid, affiliation pairs function room_mt:each_affiliation(with_affiliation) local _affiliations, _affiliation_data = self._affiliations, self._affiliation_data; return function(_, jid) local affiliation; repeat -- Iterate until we get a match jid, affiliation = next(_affiliations, jid); until with_affiliation == nil or jid == nil or affiliation == with_affiliation return jid, affiliation, _affiliation_data[jid]; end, nil, nil; end function room_mt:set_affiliation(actor, jid, affiliation, reason, data) if not actor then return nil, "modify", "not-acceptable"; end; local node, host = jid_split(jid); if not host then return nil, "modify", "not-acceptable"; end jid = jid_join(node, host); -- Bare local is_host_only = node == nil; if valid_affiliations[affiliation or "none"] == nil then return nil, "modify", "not-acceptable"; end affiliation = affiliation ~= "none" and affiliation or nil; -- coerces `affiliation == false` to `nil` local target_affiliation = self._affiliations[jid]; -- Raw; don't want to check against host local is_downgrade = valid_affiliations[target_affiliation or "none"] > valid_affiliations[affiliation or "none"]; if actor == true then actor = nil -- So we can pass it safely to 'publicise_occupant_status' below else local actor_affiliation = self:get_affiliation(actor); if actor_affiliation == "owner" then if jid_bare(actor) == jid and is_downgrade then -- self change -- need at least one owner local is_last = true; for j in self:each_affiliation("owner") do if j ~= jid then is_last = false; break; end end if is_last then return nil, "cancel", "conflict"; end end -- owners can do anything else elseif affiliation == "owner" or affiliation == "admin" or actor_affiliation ~= "admin" or target_affiliation == "owner" or target_affiliation == "admin" then -- Can't demote owners or other admins return nil, "cancel", "not-allowed"; end end local event_data = { room = self; actor = actor; jid = jid; affiliation = affiliation or "none"; reason = reason; previous_affiliation = target_affiliation or "none"; data = data and data or nil; -- coerce false to nil previous_data = self._affiliation_data[jid] or nil; }; module:fire_event("muc-pre-set-affiliation", event_data); if event_data.allowed == false then local err = event_data.error or { type = "cancel", condition = "not-allowed" }; return nil, err.type, err.condition; end if affiliation and not data and event_data.data then -- Allow handlers to add data when none was going to be set data = event_data.data; end -- Set in 'database' self._affiliations[jid] = affiliation; if not affiliation or data == false or (data ~= nil and next(data) == nil) then module:log("debug", "Clearing affiliation data for %s", jid); self._affiliation_data[jid] = nil; elseif data then module:log("debug", "Updating affiliation data for %s", jid); self._affiliation_data[jid] = data; end -- Update roles local role = self:get_default_role(affiliation); local role_rank = valid_roles[role or "none"]; local occupants_updated = {}; -- Filled with old roles for nick, occupant in self:each_occupant() do -- luacheck: ignore 213 if occupant.bare_jid == jid or ( -- Outcast can be by host. is_host_only and affiliation == "outcast" and select(2, jid_split(occupant.bare_jid)) == host ) then -- need to publicize in all cases; as affiliation in has changed. occupants_updated[occupant] = occupant.role; if occupant.role ~= role and ( is_downgrade or valid_roles[occupant.role or "none"] < role_rank -- upgrade ) then occupant.role = role; self:save_occupant(occupant); end end end -- Tell the room of the new occupant affiliations+roles local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}); if not role then -- getting kicked if affiliation == "outcast" then x:tag("status", {code="301"}):up(); -- banned else x:tag("status", {code="321"}):up(); -- affiliation change end end local is_semi_anonymous = self:get_whois() == "moderators"; if next(occupants_updated) ~= nil then for occupant, old_role in pairs(occupants_updated) do self:publicise_occupant_status(occupant, x, nil, actor, reason, old_role); if occupant.role == nil then module:fire_event("muc-occupant-left", { room = self; nick = occupant.nick; occupant = occupant; }); elseif is_semi_anonymous and ((old_role == "moderator" and occupant.role ~= "moderator") or (old_role ~= "moderator" and occupant.role == "moderator")) then -- Has gained or lost moderator status -- Send everyone else's presences (as jid visibility has changed) for real_jid in occupant:each_session() do self:send_occupant_list(real_jid, function(occupant_jid, occupant) --luacheck: ignore 212 433 return (not occupant) or occupant.bare_jid ~= jid; end); end end end else -- Announce affiliation change for a user that is not currently in the room, -- XEP-0045 (v1.31.2) example 195 -- add_item(x, affiliation, role, jid, nick, actor_nick, actor_jid, reason) local announce_msg = st.message({ from = self.jid }) :add_child(add_item(st.clone(x), affiliation, nil, jid, nil, nil, nil, reason)); local min_role = is_semi_anonymous and "moderator" or "none"; self:broadcast(announce_msg, muc_util.only_with_min_role(min_role)); end self:save(true); event_data.in_room = next(occupants_updated) ~= nil; module:fire_event("muc-set-affiliation", event_data); return true; end function room_mt:get_affiliation_data(jid, key) local data = self._affiliation_data[jid]; if not data then return nil; end if key then return data[key]; end return data; end function room_mt:set_affiliation_data(jid, key, value) if key == nil then return nil, "invalid key"; end local data = self._affiliation_data[jid]; if not data then if value == nil then return true; end data = {}; self._affiliation_data[jid] = data; end local old_value = data[key]; data[key] = value; if old_value ~= value then module:fire_event("muc-set-affiliation-data/"..key, { room = self; jid = jid; key = key; value = value; old_value = old_value; }); end self:save(true); return true; end function room_mt:get_role(nick) local occupant = self:get_occupant_by_nick(nick); return occupant and occupant.role or nil; end function room_mt:may_set_role(actor, occupant, role) local event = { room = self, actor = actor, occupant = occupant, role = role, }; module:fire_event("muc-pre-set-role", event); if event.allowed ~= nil then return event.allowed, event.error, event.condition; end local actor_affiliation = self:get_affiliation(actor) or "none"; local occupant_affiliation = self:get_affiliation(occupant.bare_jid) or "none"; -- Can't do anything to someone with higher affiliation if valid_affiliations[actor_affiliation] < valid_affiliations[occupant_affiliation] then return nil, "cancel", "not-allowed"; end -- If you are trying to give or take moderator role you need to be an owner or admin if occupant.role == "moderator" or role == "moderator" then if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then return nil, "cancel", "not-allowed"; end end -- Need to be in the room and a moderator local actor_occupant = self:get_occupant_by_real_jid(actor); if not actor_occupant or actor_occupant.role ~= "moderator" then return nil, "cancel", "not-allowed"; end return true; end function room_mt:set_role(actor, occupant_jid, role, reason) if not actor then return nil, "modify", "not-acceptable"; end local occupant = self:get_occupant_by_nick(occupant_jid); if not occupant then return nil, "modify", "item-not-found"; end if valid_roles[role or "none"] == nil then return nil, "modify", "not-acceptable"; end role = role ~= "none" and role or nil; -- coerces `role == false` to `nil` if actor == true then actor = nil -- So we can pass it safely to 'publicise_occupant_status' below else local allowed, err, condition = self:may_set_role(actor, occupant, role) if not allowed then return allowed, err, condition; end end local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}); if not role then x:tag("status", {code = "307"}):up(); end local prev_role = occupant.role; occupant.role = role; self:save_occupant(occupant); self:publicise_occupant_status(occupant, x, nil, actor, reason, prev_role); if role == nil then module:fire_event("muc-occupant-left", { room = self; nick = occupant.nick; occupant = occupant; }); end return true; end local whois = module:require "muc/whois"; room_mt.get_whois = whois.get; room_mt.set_whois = whois.set; local _M = {}; -- module "muc" function _M.new_room(jid, config) return setmetatable({ jid = jid; _jid_nick = {}; _occupants = {}; _data = config or {}; _affiliations = {}; _affiliation_data = {}; }, room_mt); end local new_format = module:get_option_boolean("new_muc_storage_format", true); function room_mt:freeze(live) local frozen, state; if new_format then frozen = { _jid = self.jid; _data = self._data; _affiliation_data = self._affiliation_data; }; for user, affiliation in pairs(self._affiliations) do frozen[user] = affiliation; end else frozen = { jid = self.jid; _data = self._data; _affiliations = self._affiliations; _affiliation_data = self._affiliation_data; }; end if live then state = {}; for nick, occupant in self:each_occupant() do state[nick] = { bare_jid = occupant.bare_jid; role = occupant.role; jid = occupant.jid; } for jid, presence in occupant:each_session() do state[jid] = st.preserialize(presence); end end local history = self._history; if history and history[1] ~= nil then state._last_message = st.preserialize(history[#history].stanza); state._last_message_at = history[#history].timestamp; end end return frozen, state; end function _M.restore_room(frozen, state) local room_jid = frozen._jid or frozen.jid; local room = _M.new_room(room_jid, frozen._data); if state and state._last_message and state._last_message_at then room._history = { { stanza = st.deserialize(state._last_message), timestamp = state._last_message_at, }, }; end local occupants = {}; local room_name, room_host = jid_split(room_jid); room._affiliation_data = frozen._affiliation_data or {}; if frozen.jid and frozen._affiliations then -- Old storage format room._affiliations = frozen._affiliations; else -- New storage format for jid, data in pairs(frozen) do local _, host, resource = jid_split(jid); if host:sub(1,1) ~= "_" and not resource and type(data) == "string" then -- bare jid: affiliation room._affiliations[jid] = data; end end end for jid, data in pairs(state or frozen) do local node, host, resource = jid_split(jid); if node or host:sub(1,1) ~= "_" then if host == room_host and node == room_name and resource and type(data) == "table" then -- full room jid: bare real jid and role local nick = jid; local occupant = occupants[nick] or occupant_lib.new(data.bare_jid, nick); occupant.bare_jid = data.bare_jid; occupant.role = data.role; occupant.jid = data.jid; -- Primary session JID occupants[nick] = occupant; elseif type(data) == "table" and data.name == "presence" then -- full user jid: presence local nick = data.attr.from; local occupant = occupants[nick] or occupant_lib.new(nil, nick); local presence = st.deserialize(data); occupant:set_session(jid, presence); occupants[nick] = occupant; end end end for _, occupant in pairs(occupants) do room:save_occupant(occupant); end return room; end _M.room_mt = room_mt; return _M; prosody-13.0.1/plugins/muc/PaxHeaders/name.lib.lua0000644000000000000000000000011714773555365017035 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.675710869 prosody-13.0.1/plugins/muc/name.lib.lua0000644000175000017500000000215114773555365021233 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local function get_name(room) return room._data.name; end local function set_name(room, name) if name == "" then name = nil; end if room._data.name == name then return false; end room._data.name = name; return true; end local function insert_name_into_form(event) table.insert(event.form, { name = "muc#roomconfig_roomname"; type = "text-single"; label = "Title"; value = event.room._data.name; }); end module:hook("muc-disco#info", function(event) event.reply:tag("identity", {category="conference", type="text", name=get_name(event.room)}):up(); insert_name_into_form(event); end); module:hook("muc-config-form", insert_name_into_form, 100-1); module:hook("muc-config-submitted/muc#roomconfig_roomname", function(event) if set_name(event.room, event.value) then event.status_codes["104"] = true; end end); return { get = get_name; set = set_name; }; prosody-13.0.1/plugins/muc/PaxHeaders/occupant.lib.lua0000644000000000000000000000011714773555365017731 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.675710869 prosody-13.0.1/plugins/muc/occupant.lib.lua0000644000175000017500000000434114773555365022132 0ustar00prosodyprosody00000000000000local pairs = pairs; local setmetatable = setmetatable; local st = require "prosody.util.stanza"; local util = module:require "muc/util"; local function get_filtered_presence(stanza) return util.filter_muc_x(st.clone(stanza)); end local occupant_mt = {}; occupant_mt.__index = occupant_mt; local function new_occupant(bare_real_jid, nick) return setmetatable({ bare_jid = bare_real_jid; nick = nick; -- in-room jid sessions = {}; -- hash from real_jid to presence stanzas. stanzas should not be modified role = nil; jid = nil; -- Primary session }, occupant_mt); end -- Deep copy an occupant local function copy_occupant(occupant) local sessions = {}; for full_jid, presence_stanza in pairs(occupant.sessions) do -- Don't keep unavailable presences, as they'll accumulate; unless they're the primary session if presence_stanza.attr.type ~= "unavailable" or full_jid == occupant.jid then sessions[full_jid] = presence_stanza; end end return setmetatable({ bare_jid = occupant.bare_jid; nick = occupant.nick; sessions = sessions; role = occupant.role; jid = occupant.jid; }, occupant_mt); end -- finds another session to be the primary (there might not be one) function occupant_mt:choose_new_primary() for jid, pr in self:each_session() do if pr.attr.type == nil then return jid; end end return nil; end function occupant_mt:set_session(real_jid, presence_stanza, replace_primary) local pr = get_filtered_presence(presence_stanza); pr.attr.from = self.nick; pr.attr.to = real_jid; self.sessions[real_jid] = pr; if replace_primary then self.jid = real_jid; elseif self.jid == nil or (pr.attr.type == "unavailable" and self.jid == real_jid) then -- Only leave an unavailable presence as primary when there are no other options self.jid = self:choose_new_primary() or real_jid; end end function occupant_mt:remove_session(real_jid) -- Delete original session self.sessions[real_jid] = nil; if self.jid == real_jid then self.jid = self:choose_new_primary(); end end function occupant_mt:each_session() return pairs(self.sessions) end function occupant_mt:get_presence(real_jid) return self.sessions[real_jid or self.jid] end return { new = new_occupant; copy = copy_occupant; mt = occupant_mt; } prosody-13.0.1/plugins/muc/PaxHeaders/occupant_id.lib.lua0000644000000000000000000000011714773555365020405 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.675710869 prosody-13.0.1/plugins/muc/occupant_id.lib.lua0000644000175000017500000000426514773555365022613 0ustar00prosodyprosody00000000000000-- Implementation of https://xmpp.org/extensions/xep-0421.html -- XEP-0421: Anonymous unique occupant identifiers for MUCs -- (C) 2020 Maxime “pep” Buquet -- (C) 2020 Matthew Wild 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"; local function get_room_salt(room) local salt = room._data.occupant_id_salt; if not salt then salt = uuid.generate(); room._data.occupant_id_salt = salt; end return salt; end local function get_occupant_id(room, occupant) if occupant.stable_id then return occupant.stable_id; end local salt = get_room_salt(room) occupant.stable_id = b64encode(hmac_sha256(occupant.bare_jid, salt)); return occupant.stable_id; end local function update_occupant(event) local stanza, room, occupant, dest_occupant = event.stanza, event.room, event.occupant, event.dest_occupant; -- "muc-occupant-pre-change" provides "dest_occupant" but not "occupant". if dest_occupant ~= nil then occupant = dest_occupant; end -- strip any existing tags to avoid forgery stanza:remove_children("occupant-id", xmlns_occupant_id); local unique_id = get_occupant_id(room, occupant); stanza:tag("occupant-id", { xmlns = xmlns_occupant_id, id = unique_id }):up(); end local function muc_private(event) local stanza, room = event.stanza, event.room; local occupant = room._occupants[stanza.attr.from]; update_occupant({ stanza = stanza, room = room, occupant = occupant, }); end if module:get_option_boolean("muc_occupant_id", true) then module:add_feature(xmlns_occupant_id); module:hook("muc-disco#info", function (event) event.reply:tag("feature", { var = xmlns_occupant_id }):up(); end); module:hook("muc-broadcast-presence", update_occupant); module:hook("muc-occupant-pre-join", update_occupant); module:hook("muc-occupant-pre-change", update_occupant); module:hook("muc-occupant-groupchat", update_occupant); module:hook("muc-private-message", muc_private); end return { get_room_salt = get_room_salt; get_occupant_id = get_occupant_id; }; prosody-13.0.1/plugins/muc/PaxHeaders/password.lib.lua0000644000000000000000000000011714773555365017757 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.675710869 prosody-13.0.1/plugins/muc/password.lib.lua0000644000175000017500000000476714773555365022174 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "prosody.util.stanza"; local function get_password(room) return room._data.password; end local function set_password(room, password) if password == "" then password = nil; end if room._data.password == password then return false; end room._data.password = password; return true; end module:hook("muc-disco#info", function(event) event.reply:tag("feature", {var = get_password(event.room) and "muc_passwordprotected" or "muc_unsecured"}):up(); end); module:hook("muc-config-form", function(event) table.insert(event.form, { name = "muc#roomconfig_roomsecret"; type = "text-private"; label = "Password"; value = get_password(event.room) or ""; }); end, 90-2); module:hook("muc-config-submitted/muc#roomconfig_roomsecret", function(event) if set_password(event.room, event.value) then event.status_codes["104"] = true; end end); -- Don't allow anyone to join room unless they provide the password module:hook("muc-occupant-pre-join", function(event) local room, stanza = event.room, event.stanza; if not get_password(room) then return end local muc_x = stanza:get_child("x", "http://jabber.org/protocol/muc"); if not muc_x then return end local password = muc_x:get_child_text("password", "http://jabber.org/protocol/muc"); if not password or password == "" then password = nil; end if get_password(room) ~= password then local from, to = stanza.attr.from, stanza.attr.to; module:log("debug", "%s couldn't join due to invalid password: %s", from, to); local reply = st.error_reply(stanza, "auth", "not-authorized", nil, room.jid):up(); event.origin.send(reply); return true; end end, -20); -- Add password to outgoing invite module:hook("muc-invite", function(event) local password = get_password(event.room); if password then local x = event.stanza:get_child("x", "http://jabber.org/protocol/muc#user"); x:tag("password"):text(password):up(); end end); module:hook("muc-room-pre-create", function (event) local stanza, room = event.stanza, event.room; local muc_x = stanza:get_child("x", "http://jabber.org/protocol/muc"); if not muc_x then return end local password = muc_x:get_child_text("password", "http://jabber.org/protocol/muc"); set_password(room, password); end); return { get = get_password; set = set_password; }; prosody-13.0.1/plugins/muc/PaxHeaders/persistent.lib.lua0000644000000000000000000000011714773555365020315 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.675710869 prosody-13.0.1/plugins/muc/persistent.lib.lua0000644000175000017500000000347514773555365022525 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local restrict_persistent = not module:get_option_boolean("muc_room_allow_persistent", true); module:default_permission( restrict_persistent and "prosody:admin" or "prosody:registered", ":create-persistent-room" ); local function get_persistent(room) return room._data.persistent; end local function set_persistent(room, persistent) persistent = persistent and true or nil; if get_persistent(room) == persistent then return false; end room._data.persistent = persistent; return true; end module:hook("muc-config-form", function(event) 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, { name = "muc#roomconfig_persistentroom"; type = "boolean"; label = "Persistent (room should remain even when it is empty)"; desc = "Rooms are automatically deleted when they are empty, unless this option is enabled"; value = get_persistent(event.room); }); end, 100-5); module:hook("muc-config-submitted/muc#roomconfig_persistentroom", function(event) if not module:may(":create-persistent-room", event.actor) then return; -- Not allowed end if set_persistent(event.room, event.value) then event.status_codes["104"] = true; end end); module:hook("muc-disco#info", function(event) event.reply:tag("feature", {var = get_persistent(event.room) and "muc_persistent" or "muc_temporary"}):up(); end); module:hook("muc-room-destroyed", function(event) set_persistent(event.room, false); end, -100); return { get = get_persistent; set = set_persistent; }; prosody-13.0.1/plugins/muc/PaxHeaders/presence_broadcast.lib.lua0000644000000000000000000000011714773555365021743 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.675710869 prosody-13.0.1/plugins/muc/presence_broadcast.lib.lua0000644000175000017500000000445614773555365024153 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "prosody.util.stanza"; local valid_roles = { "none", "visitor", "participant", "moderator" }; local default_broadcast = { visitor = true; participant = true; moderator = true; }; local function get_presence_broadcast(room) return room._data.presence_broadcast or default_broadcast; end local function set_presence_broadcast(room, broadcast_roles) broadcast_roles = broadcast_roles or default_broadcast; local changed = false; local old_broadcast_roles = get_presence_broadcast(room); for _, role in ipairs(valid_roles) do if old_broadcast_roles[role] ~= broadcast_roles[role] then changed = true; end end if not changed then return false; end room._data.presence_broadcast = broadcast_roles; for _, occupant in room:each_occupant() do local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}); local role = occupant.role or "none"; if broadcast_roles[role] and not old_broadcast_roles[role] then -- Presence broadcast is now enabled, so announce existing user room:publicise_occupant_status(occupant, x); elseif old_broadcast_roles[role] and not broadcast_roles[role] then -- Presence broadcast is now disabled, so mark existing user as unavailable room:publicise_occupant_status(occupant, x, nil, nil, nil, nil, true); end end return true; end module:hook("muc-config-form", function(event) local values = {}; for role, value in pairs(get_presence_broadcast(event.room)) do if value then values[#values + 1] = role; end end table.insert(event.form, { name = "muc#roomconfig_presencebroadcast"; type = "list-multi"; label = "Only show participants with roles:"; value = values; options = valid_roles; }); end, 70-7); module:hook("muc-config-submitted/muc#roomconfig_presencebroadcast", function(event) local broadcast_roles = {}; for _, role in ipairs(event.value) do broadcast_roles[role] = true; end if set_presence_broadcast(event.room, broadcast_roles) then event.status_codes["104"] = true; end end); return { get = get_presence_broadcast; set = set_presence_broadcast; }; prosody-13.0.1/plugins/muc/PaxHeaders/register.lib.lua0000644000000000000000000000011714773555365017741 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.679710885 prosody-13.0.1/plugins/muc/register.lib.lua0000644000175000017500000002127514773555365022147 0ustar00prosodyprosody00000000000000local 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); local enforce_nick = module:get_option_boolean("enforce_registered_nickname", false); -- Whether to include the current registration data as a dataform. Disabled -- by default currently as it hasn't been widely tested with clients. local include_reg_form = module:get_option_boolean("muc_registration_include_form", false); -- reserved_nicks[nick] = jid local function get_reserved_nicks(room) if room._reserved_nicks then return room._reserved_nicks; end module:log("debug", "Refreshing reserved nicks..."); local reserved_nicks = {}; for jid, _, data in room:each_affiliation() do local nick = data and data.reserved_nickname; module:log("debug", "Refreshed for %s: %s", jid, nick); if nick then reserved_nicks[nick] = jid; end end room._reserved_nicks = reserved_nicks; return reserved_nicks; end -- Returns the registered nick, if any, for a JID -- Note: this is just the *nick* part, i.e. the resource of the in-room JID local function get_registered_nick(room, jid) local registered_data = room._affiliation_data[jid]; if not registered_data then return; end return registered_data.reserved_nickname; end -- Returns the JID, if any, that registered a nick (not in-room JID) local function get_registered_jid(room, nick) local reserved_nicks = get_reserved_nicks(room); return reserved_nicks[nick]; end module:hook("muc-set-affiliation", function (event) -- Clear reserved nick cache event.room._reserved_nicks = nil; end); module:hook("muc-disco#info", function (event) event.reply:tag("feature", { var = "jabber:iq:register" }):up(); end); local registration_form = dataforms.new { { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#register" }, { name = "muc#register_roomnick", type = "text-single", required = true, label = "Nickname"}, }; module:handle_items("muc-registration-field", function (event) module:log("debug", "Adding MUC registration form field: %s", event.item.name); table.insert(registration_form, event.item); end, function (event) module:log("debug", "Removing MUC registration form field: %s", event.item.name); local removed_field_name = event.item.name; for i, field in ipairs(registration_form) do if field.name == removed_field_name then table.remove(registration_form, i); break; end end end); local function enforce_nick_policy(event) local origin, stanza = event.origin, event.stanza; local room = assert(event.room); -- FIXME if not room then return; end -- Check if the chosen nickname is reserved local requested_nick = jid_resource(stanza.attr.to); local reserved_by = get_registered_jid(room, requested_nick); if reserved_by and reserved_by ~= jid_bare(stanza.attr.from) then module:log("debug", "%s attempted to use nick %s reserved by %s", stanza.attr.from, requested_nick, reserved_by); local reply = st.error_reply(stanza, "cancel", "conflict", nil, room.jid):up(); origin.send(reply); return true; end -- Check if the occupant has a reservation they must use if enforce_nick then 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); return true; end end end end module:hook("muc-occupant-pre-join", enforce_nick_policy); module:hook("muc-occupant-pre-change", enforce_nick_policy); -- Discovering Reserved Room Nickname -- http://xmpp.org/extensions/xep-0045.html#reservednick module:hook("muc-disco#info/x-roomuser-item", function (event) local nick = get_registered_nick(event.room, jid_bare(event.stanza.attr.from)); if nick then event.reply:tag("identity", { category = "conference", type = "text", name = nick }) end end); local function handle_register_iq(room, origin, stanza) local user_jid = jid_bare(stanza.attr.from) local affiliation = room:get_affiliation(user_jid); if affiliation == "outcast" then origin.send(st.error_reply(stanza, "auth", "forbidden")); return true; elseif not (affiliation or allow_unaffiliated) then origin.send(st.error_reply(stanza, "auth", "registration-required")); return true; end local reply = st.reply(stanza); local registered_nick = get_registered_nick(room, user_jid); if stanza.attr.type == "get" then reply:query("jabber:iq:register"); if registered_nick then reply:tag("registered"):up(); reply:tag("username"):text(registered_nick):up(); if include_reg_form then local aff_data = room:get_affiliation_data(user_jid); if aff_data then reply:add_child(registration_form:form(aff_data, "result")); end end origin.send(reply); return true; end reply:add_child(registration_form:form()); else -- type == set -- handle registration form local query = stanza.tags[1]; if query:get_child("remove") then -- Remove "member" affiliation, but preserve if any other local new_affiliation = affiliation ~= "member" and affiliation; local ok, err_type, err_condition = room:set_affiliation(true, user_jid, new_affiliation, nil, false); if not ok then origin.send(st.error_reply(stanza, err_type, err_condition)); return true; end origin.send(reply); return true; end local form_tag = query:get_child("x", "jabber:x:data"); if not form_tag then origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform")); return true; end local form_type, err = dataforms.get_type(form_tag); if not form_type then origin.send(st.error_reply(stanza, "modify", "bad-request", "Error with form: "..err)); return true; elseif form_type ~= "http://jabber.org/protocol/muc#register" then origin.send(st.error_reply(stanza, "modify", "bad-request", "Error in form")); return true; end local reg_data = registration_form:data(form_tag); if not reg_data then origin.send(st.error_reply(stanza, "modify", "bad-request", "Error in form")); return true; end -- Is the nickname valid? local desired_nick = resourceprep(reg_data["muc#register_roomnick"], true); if not desired_nick then origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid Nickname")); return true; end -- Is the nickname currently in use by another user? local current_occupant = room:get_occupant_by_nick(room.jid.."/"..desired_nick); if current_occupant and current_occupant.bare_jid ~= user_jid then origin.send(st.error_reply(stanza, "cancel", "conflict")); return true; end -- Is the nickname currently reserved by another user? local reserved_by = get_registered_jid(room, desired_nick); if reserved_by and reserved_by ~= user_jid then origin.send(st.error_reply(stanza, "cancel", "conflict")); return true; end if enforce_nick then -- Kick any sessions that are not using this nick before we register it local required_room_nick = room.jid.."/"..desired_nick; for room_nick, occupant in room:each_occupant() do if occupant.bare_jid == user_jid and room_nick ~= required_room_nick then room:set_role(true, room_nick, nil); -- Kick (TODO: would be nice to use 333 code) end end end -- Checks passed, save the registration if registered_nick ~= desired_nick then local registration_data = { reserved_nickname = desired_nick }; module:fire_event("muc-registration-submitted", { room = room; origin = origin; stanza = stanza; submitted_data = reg_data; affiliation_data = registration_data; }); local ok, err_type, err_condition = room:set_affiliation(true, user_jid, affiliation or "member", nil, registration_data); if not ok then origin.send(st.error_reply(stanza, err_type, err_condition)); return true; end module:log("debug", "Saved nick registration for %s: %s", user_jid, desired_nick); origin.send(reply); return true; end end origin.send(reply); return true; end return { get_registered_nick = get_registered_nick; get_registered_jid = get_registered_jid; handle_register_iq = handle_register_iq; } prosody-13.0.1/plugins/muc/PaxHeaders/request.lib.lua0000644000000000000000000000011714773555365017605 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.679710885 prosody-13.0.1/plugins/muc/request.lib.lua0000644000175000017500000000721414773555365022010 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 "prosody.util.dataforms".new({ title = "Voice Request"; { name = "FORM_TYPE"; type = "hidden"; value = "http://jabber.org/protocol/muc#request"; }, { name = "muc#jid"; type = "jid-single"; label = "User ID"; desc = "The user's JID (address)"; }, { name = "muc#roomnick"; type = "text-single"; label = "Room nickname"; desc = "The user's nickname within the room"; }, { name = "muc#role"; type = "list-single"; label = "Requested role"; value = "participant"; options = { "none", "visitor", "participant", "moderator", }; }, { name = "muc#request_allow"; type = "boolean"; label = "Grant voice to this person?"; desc = "Specify whether this person is able to speak in a moderated room"; value = false; } }); local function handle_request(room, origin, stanza, form) local occupant = room:get_occupant_by_real_jid(stanza.attr.from); local fields = voice_request_form:data(form); local event = { room = room; origin = origin; stanza = stanza; fields = fields; occupant = occupant; }; if occupant.role == "moderator" then module:log("debug", "%s responded to a voice request in %s", jid_resource(occupant.nick), room.jid); module:fire_event("muc-voice-response", event); else module:log("debug", "%s requested voice in %s", jid_resource(occupant.nick), room.jid); module:fire_event("muc-voice-request", event); end end module:hook("muc-voice-request", function(event) if event.occupant.role == "visitor" then local nick = jid_resource(event.occupant.nick); local formdata = { ["muc#jid"] = event.stanza.attr.from; ["muc#roomnick"] = nick; }; local message = st.message({ type = "normal"; from = event.room.jid }) :add_child(voice_request_form:form(formdata)); event.room:broadcast(message, function (_, occupant) return occupant.role == "moderator"; end); end end); module:hook("muc-voice-response", function(event) local actor = event.stanza.attr.from; local affected_occupant = event.room:get_occupant_by_real_jid(event.fields["muc#jid"]); local occupant = event.occupant; if occupant.role ~= "moderator" then module:log("debug", "%s tried to grant voice but wasn't a moderator", jid_resource(occupant.nick)); return; end if not event.fields["muc#request_allow"] then module:log("debug", "%s did not grant voice", jid_resource(occupant.nick)); return; end if not affected_occupant then module:log("debug", "%s tried to grant voice to unknown occupant %s", jid_resource(occupant.nick), event.fields["muc#jid"]); return; end if affected_occupant.role ~= "visitor" then module:log("debug", "%s tried to grant voice to %s but they already have it", jid_resource(occupant.nick), jid_resource(occupant.jid)); return; end module:log("debug", "%s granted voice to %s", jid_resource(event.occupant.nick), jid_resource(occupant.jid)); local ok, errtype, err = event.room:set_role(actor, affected_occupant.nick, "participant", "Voice granted"); if not ok then module:log("debug", "Error granting voice: %s", err or errtype); event.origin.send(st.error_reply(event.stanza, errtype, err)); end end); return { handle_request = handle_request; }; prosody-13.0.1/plugins/muc/PaxHeaders/restrict_pm.lib.lua0000644000000000000000000000011714773555365020450 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.679710885 prosody-13.0.1/plugins/muc/restrict_pm.lib.lua0000644000175000017500000000714014773555365022651 0ustar00prosodyprosody00000000000000-- Based on code from mod_muc_restrict_pm in prosody-modules@d82c0383106a -- by Nicholas George 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; }; prosody-13.0.1/plugins/muc/PaxHeaders/subject.lib.lua0000644000000000000000000000011714773555365017554 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.679710885 prosody-13.0.1/plugins/muc/subject.lib.lua0000644000175000017500000000762514773555365021765 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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; local function create_subject_message(from, subject) return st.message({from = from; type = "groupchat"}) :tag("subject"):text(subject or ""):up(); end local function get_changesubject(room) return room._data.changesubject; end local function set_changesubject(room, changesubject) changesubject = changesubject and true or nil; if get_changesubject(room) == changesubject then return false; end room._data.changesubject = changesubject; return true; end module:hook("muc-disco#info", function (event) table.insert(event.form, { name = "muc#roomconfig_changesubject"; type = "boolean"; }); event.formdata["muc#roomconfig_changesubject"] = get_changesubject(event.room); end); module:hook("muc-config-form", function(event) table.insert(event.form, { name = "muc#roomconfig_changesubject"; type = "boolean"; label = "Allow anyone to set the room's subject"; desc = "Choose whether anyone, or only moderators, may set the room's subject"; value = get_changesubject(event.room); }); end, 80-1); module:hook("muc-config-submitted/muc#roomconfig_changesubject", function(event) if set_changesubject(event.room, event.value) then event.status_codes["104"] = true; end end); local function get_subject(room) -- a stanza from the room JID (or from the occupant JID of the entity that set the subject) return room._data.subject_from or room.jid, room._data.subject; end local function send_subject(room, to, time) local msg = create_subject_message(get_subject(room)); msg.attr.to = to; if time then msg:tag("delay", { xmlns = "urn:xmpp:delay", from = room.jid, stamp = dt.datetime(time); }):up(); end room:route_stanza(msg); end local function set_subject(room, from, subject) if subject == "" then subject = nil; end local old_from, old_subject = get_subject(room); if old_subject == subject and old_from == from then return false; end room._data.subject_from = from; room._data.subject = subject; room._data.subject_time = os.time(); local msg = create_subject_message(from, subject); room:broadcast_message(msg); return true; end -- Send subject to joining user module:hook("muc-occupant-session-new", function(event) send_subject(event.room, event.stanza.attr.from, event.room._data.subject_time); end, 20); -- Prosody has made the decision that messages with are exclusively subject changes -- e.g. body will be ignored; even if the subject change was not allowed module:hook("muc-occupant-groupchat", function(event) local stanza = event.stanza; local subject = stanza:get_child("subject"); if subject then if stanza:get_child("body") or stanza:get_child("thread") then -- Note: A message with a and a or a and -- a is a legitimate message, but it SHALL NOT be interpreted -- as a subject change. return; end local room = event.room; local occupant = event.occupant; -- Role check for subject changes local role_rank = valid_roles[occupant and occupant.role or "none"]; if role_rank >= valid_roles.moderator or ( role_rank >= valid_roles.participant and get_changesubject(room) ) then -- and participant set_subject(room, occupant.nick, subject:get_text()); room:save(); return true; else event.origin.send(st.error_reply(stanza, "auth", "forbidden", "You are not allowed to change the subject")); return true; end end end, 20); return { get_changesubject = get_changesubject; set_changesubject = set_changesubject; get = get_subject; set = set_subject; send = send_subject; }; prosody-13.0.1/plugins/muc/PaxHeaders/util.lib.lua0000644000000000000000000000011714773555365017072 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.679710885 prosody-13.0.1/plugins/muc/util.lib.lua0000644000175000017500000000316714773555365021300 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local _M = {}; _M.valid_affiliations = { outcast = -1; none = 0; member = 1; admin = 2; owner = 3; }; _M.valid_roles = { none = 0; visitor = 1; participant = 2; moderator = 3; }; local kickable_error_conditions = { ["gone"] = true; ["internal-server-error"] = true; ["item-not-found"] = true; ["jid-malformed"] = true; ["recipient-unavailable"] = true; ["redirect"] = true; ["remote-server-not-found"] = true; ["remote-server-timeout"] = true; ["service-unavailable"] = true; ["malformed error"] = true; }; function _M.is_kickable_error(stanza) local cond = select(2, stanza:get_error()) or "malformed error"; return kickable_error_conditions[cond]; end local filtered_namespaces = module:shared("filtered-namespaces"); filtered_namespaces["http://jabber.org/protocol/muc"] = true; filtered_namespaces["http://jabber.org/protocol/muc#user"] = true; local function muc_ns_filter(tag) if filtered_namespaces[tag.attr.xmlns] then return nil; end return tag; end function _M.filter_muc_x(stanza) return stanza:maptags(muc_ns_filter); end function _M.add_filtered_namespace(xmlns) filtered_namespaces[xmlns] = true; end function _M.only_with_min_role(role) local min_role_value = _M.valid_roles[role]; return function (nick, occupant) --luacheck: ignore 212/nick if _M.valid_roles[occupant.role or "none"] >= min_role_value then return true; end end; end return _M; prosody-13.0.1/plugins/muc/PaxHeaders/vcard.lib.lua0000644000000000000000000000011714773555365017214 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.679710885 prosody-13.0.1/plugins/muc/vcard.lib.lua0000644000175000017500000000433714773555365021422 0ustar00prosodyprosody00000000000000local mod_vcard = module:depends("vcard"); local jid = require "prosody.util.jid"; local st = require "prosody.util.stanza"; -- This must be the same event that mod_vcard hooks local vcard_event = "iq/bare/vcard-temp:vCard"; local advertise_hashes = module:get_option("muc_avatar_advertise_hashes"); --luacheck: ignore 113/get_room_from_jid local function get_avatar_hash(room) if room.avatar_hash then return room.avatar_hash; end local room_node = jid.split(room.jid); local hash = mod_vcard.get_avatar_hash(room_node); room.avatar_hash = hash; return hash; end local function send_avatar_hash(room, to) local hash = get_avatar_hash(room); if not hash and to then return; end -- Don't announce when no avatar local presence_vcard = st.presence({to = to, from = room.jid}) :tag("x", { xmlns = "vcard-temp:x:update" }) :tag("photo"):text(hash):up(); if to == nil then if not advertise_hashes or advertise_hashes == "presence" then room:broadcast_message(presence_vcard); end if not advertise_hashes or advertise_hashes == "message" then room:broadcast_message(st.message({ from = room.jid, type = "groupchat" }) :tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }) :tag("status", { code = "104" })); end else module:send(presence_vcard); end end module:hook(vcard_event, function (event) local stanza = event.stanza; local to = stanza.attr.to; if stanza.attr.type ~= "set" then return; end local room = get_room_from_jid(to); if not room then return; end local sender_affiliation = room:get_affiliation(stanza.attr.from); if sender_affiliation == "owner" then event.allow_vcard_modification = true; end end, 10); if advertise_hashes ~= "none" then module:hook("muc-occupant-joined", function (event) send_avatar_hash(event.room, event.stanza.attr.from); end); module:hook("vcard-updated", function (event) local room = get_room_from_jid(event.stanza.attr.to); send_avatar_hash(room, nil); end); end module:hook("muc-disco#info", function (event) event.reply:tag("feature", { var = "vcard-temp" }):up(); table.insert(event.form, { name = "muc#roominfo_avatarhash", type = "text-multi", }); event.formdata["muc#roominfo_avatarhash"] = get_avatar_hash(event.room); end); prosody-13.0.1/plugins/muc/PaxHeaders/whois.lib.lua0000644000000000000000000000011714773555365017246 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.679710885 prosody-13.0.1/plugins/muc/whois.lib.lua0000644000175000017500000000403414773555365021446 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2014 Daurnimator -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local valid_whois = { moderators = true; anyone = true; }; local function get_whois(room) return room._data.whois or "moderators"; end local function set_whois(room, whois) assert(valid_whois[whois], "Invalid whois value") if get_whois(room) == whois then return false; end room._data.whois = whois; return true; end module:hook("muc-disco#info", function(event) local whois = get_whois(event.room) ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"; event.reply:tag("feature", { var = whois }):up(); end); module:hook("muc-config-form", function(event) local whois = get_whois(event.room); table.insert(event.form, { name = 'muc#roomconfig_whois', type = 'list-single', label = 'Addresses (JIDs) of room occupants may be viewed by:', options = { { value = 'moderators', label = 'Moderators only', default = whois == 'moderators' }, { value = 'anyone', label = 'Anyone', default = whois == 'anyone' } } }); end, 80-4); module:hook("muc-config-submitted/muc#roomconfig_whois", function(event) if set_whois(event.room, event.value) then local code = (event.value == 'moderators') and "173" or "172"; event.status_codes[code] = true; end end); -- Mask 'from' jid as occupant jid if room is anonymous module:hook("muc-invite", function(event) local room, stanza = event.room, event.stanza; if get_whois(room) == "moderators" and room:get_default_role(room:get_affiliation(stanza.attr.to)) ~= "moderator" then local invite = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite"); local occupant_jid = room:get_occupant_jid(invite.attr.from); if occupant_jid ~= nil then -- FIXME: This will expose real jid if inviter is not in room invite.attr.from = occupant_jid; end end end, 50); return { get = get_whois; set = set_whois; }; prosody-13.0.1/PaxHeaders/prosody0000644000000000000000000000011714773555365014022 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.683710901 prosody-13.0.1/prosody0000755000175000017500000000536014773555365016230 0ustar00prosodyprosody00000000000000#!/usr/bin/env lua -- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- prosody - main executable for Prosody XMPP server -- Will be modified by configure script if run -- CFG_SOURCEDIR=CFG_SOURCEDIR or os.getenv("PROSODY_SRCDIR"); CFG_CONFIGDIR=CFG_CONFIGDIR or os.getenv("PROSODY_CFGDIR"); CFG_PLUGINDIR=CFG_PLUGINDIR or os.getenv("PROSODY_PLUGINDIR"); CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR"); -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- local function is_relative(path) local path_sep = package.config:sub(1,1); return ((path_sep == "/" and path:sub(1,1) ~= "/") or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\"))) end -- Tell Lua where to find our libraries if CFG_SOURCEDIR then local function filter_relative_paths(path) if is_relative(path) then return ""; end end local function sanitise_paths(paths) return (paths:gsub("[^;]+;?", filter_relative_paths):gsub(";;+", ";")); end package.path = sanitise_paths(CFG_SOURCEDIR.."/?.lua;"..package.path); package.cpath = sanitise_paths(CFG_SOURCEDIR.."/?.so;"..package.cpath); end -- Substitute ~ with path to home directory in data path if CFG_DATADIR then if os.getenv("HOME") then CFG_DATADIR = CFG_DATADIR:gsub("^~", os.getenv("HOME")); end 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 "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 ( :/ ). local thread = async.runner(); thread:run(startup.prosody); prosody.main_thread = thread; local function loop() -- Error handler for errors that make it this far local function catch_uncaught_error(err) if type(err) == "string" and err:match("interrupted!$") then return "quitting"; end prosody.log("error", "Top-level error, please report:\n%s", tostring(err)); local traceback = debug.traceback("", 2); if traceback then prosody.log("error", "%s", traceback); end prosody.events.fire_event("very-bad-error", {error = err, traceback = traceback}); end local sleep = require"socket".sleep; local server = require "prosody.net.server"; while select(2, xpcall(server.loop, catch_uncaught_error)) ~= "quitting" do sleep(0.2); end end loop(); startup.exit(); prosody-13.0.1/PaxHeaders/prosody.cfg.lua.dist0000644000000000000000000000011714773555365016302 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.683710901 prosody-13.0.1/prosody.cfg.lua.dist0000644000175000017500000002442514773555365020510 0ustar00prosodyprosody00000000000000-- Prosody Example Configuration File -- -- Information on configuring Prosody can be found on our -- website at https://prosody.im/doc/configure -- -- Tip: You can check that the syntax of this file is correct -- when you have finished by running this command: -- prosodyctl check config -- If there are any errors, it will let you know what and where -- they are, otherwise it will keep quiet. -- -- Upgrading from a previous release? Check https://prosody.im/doc/upgrading -- -- The only thing left to do is rename this file to remove the .dist ending, and fill in the -- blanks. Good luck, and happy Jabbering! ---------- Server-wide settings ---------- -- Settings in this section apply to the whole server and are the default settings -- for any virtual hosts -- This is a (by default, empty) list of accounts that are admins -- for the server. Note that you must create the accounts separately -- (see https://prosody.im/doc/creating_accounts for info) -- Example: admins = { "user1@example.com", "user2@example.net" } admins = { } -- This option allows you to specify additional locations where Prosody -- will search first for modules. For additional modules you can install, see -- the community module repository at https://modules.prosody.im/ --plugin_paths = {} -- This is the list of modules Prosody will load on startup. -- Documentation for bundled modules can be found at: https://prosody.im/doc/modules modules_enabled = { -- Generally required "disco"; -- Service discovery "roster"; -- Allow users to have a roster. Recommended ;) "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in. "tls"; -- Add support for secure TLS on c2s/s2s connections -- Not essential, but recommended "blocklist"; -- Allow users to block communications with other users "bookmarks"; -- Synchronise the list of open rooms between clients "carbons"; -- Keep multiple online clients in sync "dialback"; -- Support for verifying remote servers using DNS "limits"; -- Enable bandwidth limiting for XMPP connections "pep"; -- Allow users to store public and private data in their account "private"; -- Legacy account storage mechanism (XEP-0049) "smacks"; -- Stream management and resumption (XEP-0198) "vcard4"; -- User profiles (stored in PEP) "vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard -- Nice to have "account_activity"; -- Record time when an account was last used "cloud_notify"; -- Push notifications for mobile devices "csi_simple"; -- Simple but effective traffic optimizations for mobile devices "invites"; -- Create and manage invites "invites_adhoc"; -- Allow admins/users to create invitations via their client "invites_register"; -- Allows invited users to create accounts "ping"; -- Replies to XMPP pings with pongs "register"; -- Allow users to register on this server using a client and change passwords "time"; -- Let others know the time here on this server "uptime"; -- Report how long server has been running "version"; -- Replies to server version requests --"mam"; -- Store recent messages to allow multi-device synchronization --"turn_external"; -- Provide external STUN/TURN service for e.g. audio/video calls -- Admin interfaces "admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands "admin_shell"; -- Allow secure administration via 'prosodyctl shell' -- HTTP modules --"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP" --"http_openmetrics"; -- for exposing metrics to stats collectors --"websocket"; -- XMPP over WebSockets -- Other specific functionality --"announce"; -- Send announcement to all online users --"groups"; -- Shared roster support --"mimicking"; -- Prevent address spoofing --"motd"; -- Send a message to users when they log in --"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use --"s2s_bidi"; -- Bi-directional server-to-server (XEP-0288) --"server_contact_info"; -- Publish contact information for this service --"tombstones"; -- Prevent registration of deleted accounts --"watchregistrations"; -- Alert admins of registrations --"welcome"; -- Welcome users who register accounts } -- These modules are auto-loaded, but should you want -- to disable them then uncomment them here: modules_disabled = { -- "offline"; -- Store offline messages -- "c2s"; -- Handle client connections -- "s2s"; -- Handle server-to-server connections } -- Server-to-server authentication -- Require valid certificates for server-to-server connections? -- If false, other methods such as dialback (DNS) may be used instead. s2s_secure_auth = true -- Some servers have invalid or self-signed certificates. You can list -- remote domains here that will not be required to authenticate using -- certificates. They will be authenticated using other methods instead, -- even when s2s_secure_auth is enabled. --s2s_insecure_domains = { "insecure.example" } -- Even if you disable s2s_secure_auth, you can still require valid -- certificates for some domains by specifying a list here. --s2s_secure_domains = { "jabber.org" } -- Rate limits -- Enable rate limits for incoming client and server connections. These help -- protect from excessive resource consumption and denial-of-service attacks. limits = { c2s = { rate = "10kb/s"; }; s2sin = { rate = "30kb/s"; }; } -- Authentication -- Select the authentication backend to use. The 'internal' providers -- use Prosody's configured data storage to store the authentication data. -- For more information see https://prosody.im/doc/authentication authentication = "internal_hashed" -- Many authentication providers, including the default one, allow you to -- create user accounts via Prosody's admin interfaces. For details, see the -- documentation at https://prosody.im/doc/creating_accounts -- Storage -- Select the storage backend to use. By default Prosody uses flat files -- in its configured data directory, but it also supports more backends -- through modules. An "sql" backend is included by default, but requires -- additional dependencies. See https://prosody.im/doc/storage for more info. --storage = "sql" -- Default is "internal" -- For the "sql" backend, you can uncomment *one* of the below to configure: --sql = { driver = "SQLite3", database = "prosody.sqlite" } -- Default. 'database' is the filename. --sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } --sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } -- Archiving configuration -- If mod_mam is enabled, Prosody will store a copy of every message. This -- is used to synchronize conversations between multiple clients, even if -- they are offline. This setting controls how long Prosody will keep -- messages in the archive before removing them. archive_expires_after = "1w" -- Remove archived messages after 1 week -- You can also configure messages to be stored in-memory only. For more -- archiving options, see https://prosody.im/doc/modules/mod_mam -- Audio/video call relay (STUN/TURN) -- To ensure clients connected to the server can establish connections for -- low-latency media streaming (such as audio and video calls), it is -- recommended to run a STUN/TURN server for clients to use. If you do this, -- specify the details here so clients can discover it. -- Find more information at https://prosody.im/doc/turn -- Specify the address of the TURN service (you may use the same domain as XMPP) --turn_external_host = "turn.example.com" -- This secret must be set to the same value in both Prosody and the TURN server --turn_external_secret = "your-secret-turn-access-token" -- Logging configuration -- For advanced logging see https://prosody.im/doc/logging log = { info = "prosody.log"; -- Change 'info' to 'debug' for verbose logging error = "prosody.err"; -- "*syslog"; -- Uncomment this for logging to syslog -- "*console"; -- Log to the console, useful for debugging when running in the foreground } -- Uncomment to enable statistics -- For more info see https://prosody.im/doc/statistics -- statistics = "internal" -- Certificates -- Every virtual host and component needs a certificate so that clients and -- servers can securely verify its identity. Prosody will automatically load -- certificates/keys from the directory specified here. -- For more information, including how to use 'prosodyctl' to auto-import certificates -- (from e.g. Let's Encrypt) see https://prosody.im/doc/certificates -- Location of directory to find certificates in (relative to main config file): certificates = "certs" ----------- Virtual hosts ----------- -- You need to add a VirtualHost entry for each domain you wish Prosody to serve. -- Settings under each VirtualHost entry apply *only* to that host. VirtualHost "localhost" -- Prosody requires at least one enabled VirtualHost to function. You can -- safely remove or disable 'localhost' once you have added another. --VirtualHost "example.com" ------ Components ------ -- You can specify components to add hosts that provide special services, -- like multi-user conferences, and transports. -- For more information on components, see https://prosody.im/doc/components ---Set up a MUC (multi-user chat) room server on conference.example.com: --Component "conference.example.com" "muc" --- Store MUC messages in an archive and allow users to access it --modules_enabled = { "muc_mam" } ---Set up a file sharing component --Component "share.example.com" "http_file_share" ---Set up an external component (default component port is 5347) -- -- External components allow adding various services, such as gateways/ -- bridges to non-XMPP networks and services. For more info -- see: https://prosody.im/doc/components#adding_an_external_component -- --Component "gateway.example.com" -- component_secret = "password" ---------- End of the Prosody Configuration file ---------- -- You usually **DO NOT** want to add settings here at the end, as they would -- only apply to the last defined VirtualHost or Component. -- -- Settings for the global section should go higher up, before the first -- VirtualHost or Component line, while settings intended for specific hosts -- should go under the corresponding VirtualHost or Component line. -- -- For more information see https://prosody.im/doc/configure prosody-13.0.1/PaxHeaders/prosody.release0000644000000000000000000000012714773555365015442 xustar0029 mtime=1743706869.95171197 29 atime=1743706869.95171197 29 ctime=1743706869.95171197 prosody-13.0.1/prosody.release0000644000175000017500000000000714773555365017635 0ustar00prosodyprosody0000000000000013.0.1 prosody-13.0.1/PaxHeaders/prosodyctl0000644000000000000000000000011714773555365014525 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.683710901 prosody-13.0.1/prosodyctl0000755000175000017500000005251114773555365016733 0ustar00prosodyprosody00000000000000#!/usr/bin/env lua -- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- prosodyctl - command-line controller for Prosody XMPP server -- Will be modified by configure script if run -- CFG_SOURCEDIR=CFG_SOURCEDIR or os.getenv("PROSODY_SRCDIR"); CFG_CONFIGDIR=CFG_CONFIGDIR or os.getenv("PROSODY_CFGDIR"); CFG_PLUGINDIR=CFG_PLUGINDIR or os.getenv("PROSODY_PLUGINDIR"); CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR"); -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- local function is_relative(path) local path_sep = package.config:sub(1,1); return ((path_sep == "/" and path:sub(1,1) ~= "/") or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\"))) end -- Tell Lua where to find our libraries if CFG_SOURCEDIR then local function filter_relative_paths(path) if is_relative(path) then return ""; end end local function sanitise_paths(paths) return (paths:gsub("[^;]+;?", filter_relative_paths):gsub(";;+", ";")); end package.path = sanitise_paths(CFG_SOURCEDIR.."/?.lua;"..package.path); package.cpath = sanitise_paths(CFG_SOURCEDIR.."/?.so;"..package.cpath); end -- Substitute ~ with path to home directory in data path if CFG_DATADIR then if os.getenv("HOME") then CFG_DATADIR = CFG_DATADIR:gsub("^~", os.getenv("HOME")); end 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 "prosody.util.startup"; startup.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 "prosody.util.dependencies"; local lfs = dependencies.softreq "lfs"; ----------------------- 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; local read_password = human_io.read_password; local call_luarocks = prosodyctl.call_luarocks; local error_messages = prosodyctl.error_messages; local prosodyctl_timeout = (configmanager.get("*", "prosodyctl_timeout") or 5) * 2; ----------------------- local commands = {}; local command = table.remove(arg, 1); local only_help = { short_params = { h = "help"; ["?"] = "help" } } function commands.install(arg) local opts = parse_args(arg, only_help); if opts.help or not arg[1] then show_usage([[install]], [[Installs a prosody/luarocks plugin]]); return opts.help and 0 or 1; end -- TODO finalize config option name local server = opts.server or configmanager.get("*", "plugin_server"); if not (arg[1]:match("^https://") or lfs.attributes(arg[1]) or server) then show_warning("There is no 'plugin_server' option in the configuration file"); -- see https://prosody.im/doc/TODO documentation -- #1602 return 1; end show_message("Installing %s in %s", arg[1], prosody.paths.installer); local ret = call_luarocks("install", arg[1], server); if ret == 0 and arg[1]:match("^mod_") then prosodyctl.show_module_configuration_help(arg[1]); end return ret; end function commands.remove(arg) local opts = parse_args(arg, only_help); if opts.help or not arg[1] then show_usage([[remove]], [[Removes a module installed in the working directory's plugins folder]]); return opts.help and 0 or 1; end show_message("Removing %s from %s", arg[1], prosody.paths.installer); local ret = call_luarocks("remove", arg[1]); return ret; end function commands.list(arg) local opts = parse_args(arg, only_help); if opts.help then show_usage([[list]], [[Shows installed rocks]]); return 0; end local server = opts.server or configmanager.get("*", "plugin_server"); if opts.outdated then -- put this back for luarocks arg[1] = "--outdated"; if not server then show_warning("There is no 'plugin_server' option in the configuration file, but this is needed for 'list --outdated' to work."); return 1; end end local ret = call_luarocks("list", arg[1], server); return ret; end function commands.adduser(arg) local opts = parse_args(arg, only_help); if opts.help or not arg[1] then show_usage([[adduser JID]], [[Create the specified user account in Prosody]]); return opts.help and 0 or 1; end local shell = require "prosody.util.prosodyctl.shell"; return shell.shell({ ("user:create(%q, nil, %q)"):format(arg[1], "prosody:member") }); end function commands.passwd(arg) local opts = parse_args(arg, only_help); if opts.help or not arg[1] then show_usage([[passwd JID]], [[Set the password for the specified user account in Prosody]]); return opts.help and 0 or 1; end local shell = require "prosody.util.prosodyctl.shell"; return shell.shell({ ("user:password(%q, nil)"):format(arg[1]) }); end function commands.deluser(arg) local opts = parse_args(arg, only_help); if opts.help or not arg[1] then show_usage([[deluser JID]], [[Permanently remove the specified user account from Prosody]]); return opts.help and 0 or 1; end local shell = require "prosody.util.prosodyctl.shell"; return shell.shell({ ("user:delete(%q)"):format(arg[1]) }); end local function has_init_system() --> which lfs = lfs or require"lfs"; if lfs.attributes("/etc/systemd") then return "systemd"; elseif lfs.attributes("/etc/init.d/prosody") then return "rc.d"; end end local function service_command_warning(service_command) if prosody.installed and configmanager.get("*", "prosodyctl_service_warnings") ~= false then show_warning("ERROR: Use of 'prosodyctl %s' is disabled in this installation because", service_command); local init = has_init_system() if init then show_warning(" we detected that this system uses %s for managing services.", init); show_warning(""); show_warning(" To avoid problems, use that directly instead. For example:"); show_warning(""); if init == "systemd" then show_warning(" systemctl %s prosody", service_command); elseif init == "rc.d" then show_warning(" /etc/init.d/prosody %s", service_command); end show_warning(""); else show_warning(" it may conflict with your system's service manager."); show_warning(""); end show_warning(" Proceeding to use prosodyctl may cause process management issues."); show_warning(" You can pass --force to override this warning, or set"); show_warning(" prosodyctl_service_warnings = false in your global config."); os.exit(1); end end function commands.start(arg) local opts = parse_args(arg, only_help); if opts.help then show_usage([[start]], [[Start Prosody]]); return 0; end if not opts.force then service_command_warning("start"); end local ok, ret = prosodyctl.isrunning(); if not ok then show_message(error_messages[ret]); return 1; end if ret then --luacheck: ignore 421/ret local ok, ret = prosodyctl.getpid(); if not ok then show_message("Couldn't get running Prosody's PID"); show_message(error_messages[ret]); return 1; end show_message("Prosody is already running with PID %s", ret or "(unknown)"); return 1; end --luacheck: ignore 411/ret local lua; do local i = 0; repeat i = i - 1; until arg[i-1] == nil lua = arg[i]; end local ok, ret = prosodyctl.start(prosody.paths.source, lua); if ok then local daemonize = configmanager.get("*", "daemonize"); if daemonize == nil then daemonize = prosody.installed; end if daemonize then local i=1; while true do local ok, running = prosodyctl.isrunning(); if ok and running then break; elseif i == 5 then show_message("Still waiting..."); elseif i >= prosodyctl_timeout then show_message("Prosody is still not running. Please give it some time or check your log files for errors."); return 2; end socket.sleep(0.5); i = i + 1; end show_message("Started"); end return 0; end show_message("Failed to start Prosody"); show_message(error_messages[ret]) return 1; end function commands.status(arg) local opts = parse_args(arg, only_help); if opts.help then show_usage([[status]], [[Reports the running status of Prosody]]); return 0; end local ok, ret = prosodyctl.isrunning(); if not ok then show_message(error_messages[ret]); return 1; end if ret then --luacheck: ignore 421/ret local ok, ret = prosodyctl.getpid(); if not ok then show_message("Couldn't get running Prosody's PID"); show_message(error_messages[ret]); return 1; end show_message("Prosody is running with PID %s", ret or "(unknown)"); return 0; else show_message("Prosody is not running"); if not prosody.switched_user and prosody.current_uid ~= 0 then print("\nNote:") print(" You will also see this if prosodyctl is not running under"); print(" the same user account as Prosody. Try running as root (e.g. "); print(" with 'sudo' in front) to gain access to Prosody's real status."); end return 2 end end function commands.stop(arg) local opts = parse_args(arg, only_help); if opts.help then show_usage([[stop]], [[Stop a running Prosody server]]); return 0; end if not opts.force then service_command_warning("stop"); end local ok, running = prosodyctl.isrunning(); if not ok then show_message(error_messages[running]); return 1; elseif not running then show_message("Prosody is not running"); return 1; end local ok, ret = prosodyctl.stop(); if ok then local i=1; while true do local ok, running = prosodyctl.isrunning(); --luacheck: ignore 421 if ok and not running then break; elseif i == 5 then show_message("Still waiting..."); elseif i >= prosodyctl_timeout then show_message("Prosody is still running. Please give it some time or check your log files for errors."); return 2; end socket.sleep(0.5); i = i + 1; end show_message("Stopped"); return 0; end show_message(error_messages[ret]); return 1; end function commands.restart(arg) local opts = parse_args(arg, only_help); if opts.help then show_usage([[restart]], [[Restart a running Prosody server]]); return 1; end if not opts.force then service_command_warning("restart"); end commands.stop(arg); return commands.start(arg); end function commands.about(arg) local opts = parse_args(arg, only_help); if opts.help then show_usage([[about]], [[Show information about this Prosody installation]]); return 0; end local pwd = "."; 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)")); print(""); print("# Prosody directories"); print("Data directory: "..relpath(pwd, prosody.paths.data)); print("Config directory: "..relpath(pwd, prosody.paths.config or ".")); print("Source directory: "..relpath(pwd, prosody.paths.source or ".")); print("Plugin directories:") print(" "..(prosody.paths.plugins:gsub("([^;]+);?", function(path) path = configmanager.resolve_relative_path(pwd, path); local hgid, hgrepo = hg.check_id(path); if not hgid and hgrepo then return path.." - "..hgrepo .."!\n "; end -- 010452cfaf53 is the first commit in the prosody-modules repository hgrepo = hgrepo == "010452cfaf53" and "prosody-modules"; return path..(hgid and " - "..(hgrepo or "HG").." rev: "..hgid or "") .."\n "; end))); print(""); local have_pposix, pposix = pcall(require, "prosody.util.pposix"); if have_pposix and pposix.uname then print("# Operating system"); local uname, err = pposix.uname(); print(uname and uname.sysname .. " " .. uname.release or "Unknown POSIX", err or ""); print(""); end print("# Lua environment"); print("Lua version: ", _G._VERSION); print(""); print("Lua module search paths:"); for path in package.path:gmatch("[^;]+") do print(" "..path); end print(""); print("Lua C module search paths:"); for path in package.cpath:gmatch("[^;]+") do print(" "..path); end print(""); local luarocks_status = "Not installed" if pcall(require, "luarocks.loader") then luarocks_status = "Installed (2.x+)"; if package.loaded["luarocks.cfg"] then luarocks_status = "Installed ("..(package.loaded["luarocks.cfg"].program_version or "2.x+")..")"; end elseif pcall(require, "luarocks.require") then luarocks_status = "Installed (1.x)"; end print("LuaRocks: ", luarocks_status); print(""); print("# Network"); print(""); print("Backend: "..require "prosody.net.server".get_backend()); print(""); print("# Lua module versions"); local module_versions, longest_name = {}, 8; local library_versions = {}; dependencies.softreq"ssl"; dependencies.softreq"DBI"; dependencies.softreq"readline"; local friendly_names = { DBI = "LuaDBI"; lfs = "LuaFileSystem"; lunbound = "luaunbound"; lxp = "LuaExpat"; socket = "LuaSocket"; ssl = "LuaSec"; }; local alternate_version_fields = { -- These diverge from the module._VERSION convention readline = "Version"; } local lunbound = dependencies.softreq"lunbound"; local lxp = dependencies.softreq"lxp"; 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) and name ~= "_G" and not name:match("%.") then name = friendly_names[name] or name; if #name > longest_name then longest_name = #name; end local mod_version = module[version_field]; if tostring(mod_version):sub(1, #name+1) == name .. " " then mod_version = mod_version:sub(#name+2); end module_versions[name] = mod_version; end end if lunbound then if not module_versions["luaunbound"] then module_versions["luaunbound"] = "0.5 (?)"; end library_versions["libunbound"] = lunbound._LIBVER; end if lxp then library_versions["libexpat"] = lxp._EXPAT_VERSION; end if hashes then library_versions["libcrypto"] = hashes._LIBCRYPTO_VERSION; end for name, version in sorted_pairs(module_versions) do print(name..":"..string.rep(" ", longest_name-#name), version); end print(""); print("# library versions"); 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 print(name..":"..string.rep(" ", longest_name-#name), version); end print(""); end function commands.version(arg) local flags = { short_params = { h = "help"; ["?"] = "help", v = "verbose" } }; local opts = parse_args(arg, flags); if opts.help then show_usage("version [-v]", [[Show current Prosody version, or more]]); return 0; elseif opts.verbose then return commands.about(arg); end print("Prosody "..(prosody.version or "(unknown version)")); end function commands.lua_paths() local function shell_escape(s) return "'" .. tostring(s):gsub("'",[['\'']]) .. "'"; end print("LUA_PATH="..shell_escape(package.path)); print("LUA_CPATH="..shell_escape(package.cpath)); end function commands.reload(arg) local opts = parse_args(arg, only_help); if opts.help then show_usage([[reload]], [[Reload Prosody's configuration and re-open log files]]); return 0; end local shell = require "prosody.util.prosodyctl.shell"; if shell.available() then if arg[1] and arg[1]:match"^mod_" then -- TODO reword the usage text, document arg[1] = arg[1]:match("^mod_(.*)"); -- strip mod_ prefix table.insert(arg, 1, "module"); table.insert(arg, 2, "reload"); return shell.shell(arg); end return shell.shell({ "config", "reload" }); elseif arg[1] then show_message("Admin socket not found - is it enabled and is Prosody running?"); return 1; end local ok, running = prosodyctl.isrunning(); if not ok then show_message(error_messages[running]); return 1; elseif not running then show_message("Prosody is not running"); return 1; end if not opts.force then service_command_warning("reload"); end local ok, ret = prosodyctl.reload(); if ok then show_message("Prosody log files re-opened and config file reloaded. You may need to reload modules for some changes to take effect."); return 0; end show_message(error_messages[ret]); return 1; end -- ejabberdctl compatibility local unpack = table.unpack; function commands.register(arg) local user, host, password = unpack(arg); if (not (user and host)) or arg[1] == "--help" then if user ~= "--help" then if not user then show_message [[No username specified]] elseif not host then show_message [[Please specify which host you want to register the user on]]; end end show_usage("register USER HOST [PASSWORD]", "Register a user on the server, with the given password"); return 1; end if not password then password = read_password(); if not password then show_message [[Unable to register user with no password]]; return 1; end end local ok, msg = prosodyctl.adduser { user = user, host = host, password = password }; if ok then return 0; end show_message(error_messages[msg]) return 1; end function commands.unregister(arg) local user, host = unpack(arg); if (not (user and host)) or arg[1] == "--help" then if user ~= "--help" then if not user then show_message [[No username specified]] elseif not host then show_message [[Please specify which host you want to unregister the user from]]; end end show_usage("unregister USER HOST [PASSWORD]", "Permanently remove a user account from the server"); return 1; end local ok, msg = prosodyctl.deluser { user = user, host = host }; if ok then return 0; end show_message(error_messages[msg]) return 1; end --------------------- local async = require "prosody.util.async"; local server = require "prosody.net.server"; local watchers = { error = function (_, err) error(err); end; waiting = function () server.loop(); end; }; local command_runner = async.runner(function () if command and command:match("^mod_") then -- Is a command in a module local module_name = command:match("^mod_(.+)"); do local ret, err = modulemanager.load("*", module_name); if not ret then show_message("Failed to load module '"..module_name.."': "..err); os.exit(1); end end local module = modulemanager.get_module("*", module_name); if not module then show_message("Failed to load module '"..module_name.."': Unknown error"); os.exit(1); end if not modulemanager.module_has_method(module, "command") then show_message("Fail: mod_"..module_name.." does not support any commands"); os.exit(1); end local ok, ret = modulemanager.call_module_method(module, "command", arg); if ok then if type(ret) == "number" then os.exit(ret, true); elseif type(ret) == "string" then show_message(ret); end os.exit(0, true); -- :) else show_message("Failed to execute command: %s", error_messages[ret]); os.exit(1); -- :( end end if command and not commands[command] then local ok, command_module = pcall(require, "prosody.util.prosodyctl."..command); if ok and command_module[command] then commands[command] = command_module[command]; end end if not commands[command] then -- Show help for all commands function show_usage(usage, desc) print(string.format(" %-14s %s", usage, desc)); end print("prosodyctl - Manage a Prosody server"); print(""); print("Usage: "..arg[0].." COMMAND [OPTIONS]"); print(""); print("Where COMMAND may be one of:"); local hidden_commands = require "prosody.util.set".new{ "register", "unregister", "lua_paths" }; local commands_order = { "Process management:", "start"; "stop"; "restart"; "reload"; "status"; "shell", "User management:", "adduser"; "passwd"; "deluser"; "Plugin management:", "install"; "remove"; "list"; "Informative:", "check", "version", "Other:", "cert", }; -- These live in util.prosodyctl.$command so we have their short help here. local external_commands = { cert = "Certificate management commands", check = "Perform basic checks on your Prosody installation", shell = "Interact with a running Prosody", } local done = {}; if prosody.installed and has_init_system() then -- Hide start, stop, restart done[table.remove(commands_order, 2)] = true; done[table.remove(commands_order, 2)] = true; done[table.remove(commands_order, 2)] = true; end for _, command_name in ipairs(commands_order) do local command_func = commands[command_name]; if command_func then command_func{ "--help" }; done[command_name] = true; elseif external_commands[command_name] then show_usage(command_name, external_commands[command_name]); done[command_name] = true; else print"" print(command_name); end end for command_name, command_func in pairs(commands) do if not done[command_name] and not hidden_commands:contains(command_name) then command_func{ "--help" }; done[command_name] = true; end end os.exit(0, true); end os.exit(commands[command](arg), true); end, watchers); command_runner:run(true); prosody-13.0.1/PaxHeaders/spec0000644000000000000000000000013114773555365013251 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.683710901 30 ctime=1743706869.763711219 prosody-13.0.1/spec/0000755000175000017500000000000014773555365015531 5ustar00prosodyprosody00000000000000prosody-13.0.1/spec/PaxHeaders/core_configmanager_spec.lua0000644000000000000000000000011714773555365020657 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.683710901 prosody-13.0.1/spec/core_configmanager_spec.lua0000644000175000017500000000271714773555365023065 0ustar00prosodyprosody00000000000000 local configmanager = require "core.configmanager"; describe("core.configmanager", function() describe("#get()", function() it("should work", function() configmanager.set("example.com", "testkey", 123); assert.are.equal(123, configmanager.get("example.com", "testkey"), "Retrieving a set key"); configmanager.set("*", "testkey1", 321); assert.are.equal(321, configmanager.get("*", "testkey1"), "Retrieving a set global key"); assert.are.equal(321, configmanager.get("example.com", "testkey1"), "Retrieving a set key of undefined host, of which only a globally set one exists" ); configmanager.set("example.com", ""); -- Creates example.com host in config assert.are.equal(321, configmanager.get("example.com", "testkey1"), "Retrieving a set key, of which only a globally set one exists"); assert.are.equal(nil, configmanager.get(), "No parameters to get()"); assert.are.equal(nil, configmanager.get("undefined host"), "Getting for undefined host"); assert.are.equal(nil, configmanager.get("undefined host", "undefined key"), "Getting for undefined host & key"); end); end); describe("#set()", function() it("should work", function() assert.are.equal(false, configmanager.set("*"), "Set with no key"); assert.are.equal(true, configmanager.set("*", "set_test", "testkey"), "Setting a nil global value"); assert.are.equal(true, configmanager.set("*", "set_test", "testkey", 123), "Setting a global value"); end); end); end); prosody-13.0.1/spec/PaxHeaders/core_moduleapi_spec.lua0000644000000000000000000000011714773555365020036 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.683710901 prosody-13.0.1/spec/core_moduleapi_spec.lua0000644000175000017500000000577414773555365022252 0ustar00prosodyprosody00000000000000 package.loaded["core.configmanager"] = {}; package.loaded["core.statsmanager"] = {}; package.loaded["net.server"] = {}; local set = require "util.set"; _G.prosody = { hosts = {}, core_post_stanza = true }; local api = require "core.moduleapi"; local module = setmetatable({}, {__index = api}); local opt = nil; function module.log(_self) end function module.get_option(_self, name) if name == "opt" then return opt; else return nil; end end 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"); assert(module:get_option_boolean("opt") == returns.boolean, "boolean doesn't match"); if type(returns.array) == "table" then local target_array, returned_array = returns.array, module:get_option_array("opt"); assert(#target_array == #returned_array, "array length doesn't match"); for i=1,#target_array do assert(target_array[i] == returned_array[i], "array item doesn't match"); end else assert(module:get_option_array("opt") == returns.array, "array is returned (not nil)"); end if type(returns.set) == "table" then local target_items, returned_items = set.new(returns.set), module:get_option_set("opt"); assert(target_items == returned_items, "set doesn't match"); else assert(module:get_option_set("opt") == returns.set, "set is returned (not nil)"); end end describe("core.moduleapi", function() describe("#get_option_*()", function() it("should handle missing options", function() test_option_value(nil, {}); end); it("should return correctly handle boolean options", function() test_option_value(true, { boolean = true, string = "true", array = {true}, set = {true} }); test_option_value(false, { boolean = false, string = "false", array = {false}, set = {false} }); test_option_value("true", { boolean = true, string = "true", array = {"true"}, set = {"true"} }); test_option_value("false", { boolean = false, string = "false", array = {"false"}, set = {"false"} }); test_option_value(1, { boolean = true, string = "1", array = {1}, set = {1}, number = 1 }); test_option_value(0, { boolean = false, string = "0", array = {0}, set = {0}, number = 0 }); end); it("should return handle strings", function() test_option_value("hello world", { string = "hello world", array = {"hello world"}, set = {"hello world"} }); end); it("should return handle numbers", function() test_option_value(1234, { string = "1234", number = 1234, array = {1234}, set = {1234} }); end); it("should return handle arrays", function() test_option_value({1, 2, 3}, { boolean = true, string = "1", number = 1, array = {1, 2, 3}, set = {1, 2, 3} }); test_option_value({1, 2, 3, 3, 4}, {boolean = true, string = "1", number = 1, array = {1, 2, 3, 3, 4}, set = {1, 2, 3, 4} }); test_option_value({0, 1, 2, 3}, { boolean = false, string = "0", number = 0, array = {0, 1, 2, 3}, set = {0, 1, 2, 3} }); end); end) end) prosody-13.0.1/spec/PaxHeaders/core_storagemanager_spec.lua0000644000000000000000000000011714773555365021056 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.687710916 prosody-13.0.1/spec/core_storagemanager_spec.lua0000644000175000017500000005552114773555365023265 0ustar00prosodyprosody00000000000000local unpack = table.unpack; local st = require "prosody.util.stanza"; local function mock_prosody() _G.prosody = { core_post_stanza = function () end; events = require "prosody.util.events".new(); hosts = {}; paths = { data = "./data"; }; }; end local configs = { memory = { storage = "memory"; }; internal = { storage = "internal"; }; sqlite = { storage = "sql"; sql = { driver = "SQLite3", database = "prosody-tests.sqlite" }; }; mysql = { storage = "sql"; sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }; }; postgres = { storage = "sql"; sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }; }; }; 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 () for backend, backend_config in pairs(configs) do local tagged_name = "#"..backend; if backend ~= backend_config.storage then tagged_name = tagged_name.." #"..backend_config.storage; end insulate(tagged_name.." #storage backend", function () mock_prosody(); 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")); for k, v in pairs(backend_config) do config.set(test_host, k, v); end assert(hm.activate(test_host, {})); sm.initialize_host(test_host); mm.load(test_host, "storage_"..backend_config.storage); describe("key-value stores", function () -- These tests rely on being executed in order, disable any order -- randomization for this block randomize(false); local store; it("may be opened", function () store = assert(sm.open(test_host, "test")); 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 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); end); describe("map stores", function () -- These tests rely on being executed in order, disable any order -- randomization for this block randomize(false); local store, kv_store; it("may be opened", function () store = assert(sm.open(test_host, "test-map", "map")); end); it("may be opened as a keyval store", function () kv_store = assert(sm.open(test_host, "test-map", "keyval")); end); it("may set a specific key for a user", function () assert(store:set("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("user9999", "foo")); end); it("may find all users with a specific key", function () assert.is_function(store.get_all); assert(store:set("user9999b", "bar", "bar")); assert(store:set("user9999c", "foo", "blah")); local ret, err = store:get_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_all); do local ret, err = store:get_all(""); assert.is_nil(ret); assert.is_not_nil(err); end do local ret, err = store:get_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_all); do local ret, err = store:delete_all(""); assert.is_nil(ret); assert.is_not_nil(err); end do local ret, err = store:delete_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_all); assert(store:set("user9999b", "foo", "hello")); assert(store:delete_all("bar")); -- Ensure key was deleted do local ret, err = store:get("user9999b", "bar"); assert.is_nil(ret); assert.is_nil(err); end -- Ensure other users/keys are intact do local ret, err = store:get("user9999", "foo"); assert.equal("bar", ret); assert.is_nil(err); end do local ret, err = store:get("user9999b", "foo"); assert.equal("hello", ret); assert.is_nil(err); end do local ret, err = store:get("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("user9999", "foo", nil)); do local ret, err = store:get("user9999", "foo"); assert.is_nil(ret); assert.is_nil(err); end assert(store:set("user9999b", "foo", nil)); do local ret, err = store:get("user9999b", "foo"); assert.is_nil(ret); assert.is_nil(err); end 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); local archive; it("can be opened", function () archive = assert(sm.open(test_host, "test-archive", "archive")); end); local test_stanza = st.stanza("test", { xmlns = "urn:example:foo" }) :tag("foo"):up() :tag("foo"):up() :reset(); local test_time = 1539204123; local test_data = { { 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+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 () for _, data_item in ipairs(test_data) do local id = archive:append("user", unpack(data_item, 1, 4)); assert.truthy(id); data_item[1] = id; end end); describe("can be queried", function () it("for all items", function () -- luacheck: ignore 211/err local data, err = archive:find("user", {}); assert.truthy(data); local count = 0; for id, item, when in data do count = count + 1; assert.truthy(id); assert(st.is_stanza(item)); assert.equal("test", item.name); assert.equal("urn:example:foo", item.attr.xmlns); assert.equal(2, #item.tags); assert.equal(test_data[count][3], when); end assert.equal(#test_data, count); end); it("by JID", function () -- luacheck: ignore 211/err local data, err = archive:find("user", { with = "contact@example.com"; }); assert.truthy(data); local count = 0; for id, item, when in data do count = count + 1; assert.truthy(id); assert(st.is_stanza(item)); assert.equal("test", item.name); assert.equal("urn:example:foo", item.attr.xmlns); assert.equal(2, #item.tags); assert.equal(test_time-3, when); end assert.equal(1, count); end); it("by time (end)", function () -- luacheck: ignore 211/err local data, err = archive:find("user", { ["end"] = test_time; }); assert.truthy(data); local count = 0; for id, item, when in data do count = count + 1; assert.truthy(id); assert(st.is_stanza(item)); assert.equal("test", item.name); assert.equal("urn:example:foo", item.attr.xmlns); assert.equal(2, #item.tags); assert(test_time >= when); end assert.equal(4, count); end); it("by time (start)", function () -- luacheck: ignore 211/err local data, err = archive:find("user", { ["start"] = test_time; }); assert.truthy(data); local count = 0; for id, item, when in data do count = count + 1; assert.truthy(id); assert(st.is_stanza(item)); 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)); end 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-1; ["end"] = test_time+2; }); assert.truthy(data); local count = 0; for id, item, when in data do count = count + 1; assert.truthy(id); assert(st.is_stanza(item)); assert.equal("test", item.name); assert.equal("urn:example:foo", item.attr.xmlns); assert.equal(2, #item.tags); 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); it("by id (after)", function () -- luacheck: ignore 211/err local data, err = archive:find("user", { ["after"] = test_data[2][1]; }); assert.truthy(data); local count = 0; for id, item in data do count = count + 1; assert.truthy(id); assert.equal(test_data[2+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(5, count); end); it("by id (before)", function () -- luacheck: ignore 211/err local data, err = archive:find("user", { ["before"] = test_data[4][1]; }); assert.truthy(data); local count = 0; for id, item in data do count = count + 1; assert.truthy(id); assert.equal(test_data[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(3, count); end); it("by id (before and after) #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]; }); assert.truthy(data, err); local count = 0; for id, item in data do count = count + 1; assert.truthy(id); assert.equal(test_data[1+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); it("by multiple ids", function () assert.truthy(archive.caps and archive.caps.ids, "Multiple ID query") local data, err = archive:find("user", { ["ids"] = { test_data[2][1]; test_data[4][1]; }; }); assert.truthy(data, err); local count = 0; for id, item in data do count = count + 1; assert.truthy(id); assert.equal(test_data[count==1 and 2 or 4][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); it("can be queried in reverse", function () local data, err = archive:find("user", { reverse = true; limit = 3; }); assert.truthy(data, err); local i = #test_data; for id, item in data do assert.truthy(id); assert.equal(test_data[i][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); i = i - 1; end 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); it("can selectively delete items", function () local delete_id; do local data = assert(archive:find("user", {})); local count = 0; for id, item, when in data do --luacheck: ignore 213/item 213/when count = count + 1; if count == 2 then delete_id = id; end assert.truthy(id); end assert.equal(#test_data, count); end assert(archive:delete("user", { key = delete_id })); do local data = assert(archive:find("user", {})); local count = 0; for id, item, when in data do --luacheck: ignore 213/item 213/when count = count + 1; assert.truthy(id); assert.not_equal(delete_id, id); end assert.equal(#test_data-1, count); end end); it("can be purged", function () -- luacheck: ignore 211/err local ok, err = archive:delete("user"); assert.truthy(ok); local data, err = archive:find("user", { with = "contact@example.com"; }); assert.truthy(data, err); local count = 0; for id, item, when in data do -- luacheck: ignore id item when count = count + 1; end assert.equal(0, count); end); it("can truncate the oldest items", function () local username = "user-truncate"; for i = 1, 10 do assert(archive:append(username, nil, test_stanza, i, "contact@example.com")); end assert(archive:delete(username, { truncate = 3 })); do local data = assert(archive:find(username, {})); local count = 0; for id, item, when in data do --luacheck: ignore 213/when count = count + 1; assert.truthy(id); assert(st.is_stanza(item)); assert(when > 7, ("%d > 7"):format(when)); end assert.equal(3, count); end end); it("overwrites existing keys with new data", function () local prefix = ("a"):rep(50); local username = "user-overwrite"; local a1 = assert(archive:append(username, prefix.."-1", test_stanza, test_time, "contact@example.com")); local a2 = assert(archive:append(username, prefix.."-2", test_stanza, test_time, "contact@example.com")); local ids = { a1, a2, }; do local data = assert(archive:find(username, {})); local count = 0; for id, item, when in data do --luacheck: ignore 213/when count = count + 1; assert.truthy(id); assert.equals(ids[count], id); assert(st.is_stanza(item)); end assert.equal(2, count); end local new_stanza = st.clone(test_stanza); new_stanza.attr.foo = "bar"; assert(archive:append(username, a2, new_stanza, test_time+1, "contact2@example.com")); do local data = assert(archive:find(username, {})); local count = 0; for id, item, when in data do count = count + 1; assert.truthy(id); assert.equals(ids[count], id); assert(st.is_stanza(item)); if count == 2 then assert.equals(test_time+1, when); assert.equals("bar", item.attr.foo); end end assert.equal(2, count); end end); it("can contain multiple long unique keys #issue1073", function () local prefix = ("a"):rep(50); assert(archive:append("user-issue1073", prefix.."-1", test_stanza, test_time, "contact@example.com")); assert(archive:append("user-issue1073", prefix.."-2", test_stanza, test_time, "contact@example.com")); local data = assert(archive:find("user-issue1073", {})); local count = 0; for id, item, when in data do --luacheck: ignore 213/when count = count + 1; assert.truthy(id); assert(st.is_stanza(item)); end assert.equal(2, count); assert(archive:delete("user-issue1073")); end); it("can be treated as a map store", function () assert.falsy(archive:get("mapuser", "no-such-id")); assert.falsy(archive:set("mapuser", "no-such-id", test_stanza)); local id = archive:append("mapuser", nil, test_stanza, test_time, "contact@example.com"); do local stanza_roundtrip, when, with = archive:get("mapuser", id); assert.same(tostring(test_stanza), tostring(stanza_roundtrip), "same stanza is returned"); assert.equal(test_time, when, "same 'when' is returned"); assert.equal("contact@example.com", with, "same 'with' is returned"); end local replacement_stanza = st.stanza("test", { xmlns = "urn:example:foo" }) :tag("bar"):up() :reset(); assert(archive:set("mapuser", id, replacement_stanza, test_time+1)); do local replaced, when, with = archive:get("mapuser", id); assert.same(tostring(replacement_stanza), tostring(replaced), "replaced stanza is returned"); assert.equal(test_time+1, when, "modified 'when' is returned"); assert.equal("contact@example.com", with, "original 'with' is returned"); end end); it("the summary api works", function() assert.truthy(archive:delete("summary-user")); local first_sid = archive:append("summary-user", nil, test_stanza, test_time, "contact@example.com"); local second_sid = archive:append("summary-user", nil, test_stanza, test_time+1, "contact@example.com"); assert.truthy(first_sid and second_sid, "preparations failed") --- local user_summary, err = archive:summary("summary-user"); assert.is_table(user_summary, err); assert.same({ ["contact@example.com"] = 2 }, user_summary.counts, "summary.counts matches"); assert.same({ ["contact@example.com"] = test_time }, user_summary.earliest, "summary.earliest matches"); assert.same({ ["contact@example.com"] = test_time+1 }, user_summary.latest, "summary.latest matches"); if user_summary.body then assert.same({ ["contact@example.com"] = test_stanza:get_child_text("body") }, user_summary.body, "summary.body matches"); end end); end); end); end end); prosody-13.0.1/spec/PaxHeaders/inputs0000644000000000000000000000013114773555365014573 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.687710916 30 ctime=1743706869.687710916 prosody-13.0.1/spec/inputs/0000755000175000017500000000000014773555365017053 5ustar00prosodyprosody00000000000000prosody-13.0.1/spec/inputs/PaxHeaders/http0000644000000000000000000000013114773555365015552 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.687710916 30 ctime=1743706869.687710916 prosody-13.0.1/spec/inputs/http/0000755000175000017500000000000014773555365020032 5ustar00prosodyprosody00000000000000prosody-13.0.1/spec/inputs/http/PaxHeaders/httpstream-chunked-test.txt0000644000000000000000000000011714773555365023163 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.687710916 prosody-13.0.1/spec/inputs/http/httpstream-chunked-test.txt0000644000175000017500000002632414773555365025371 0ustar00prosodyprosody00000000000000HTTP/1.1 200 OK Cache-Control: max-age=0, must-revalidate, private Content-Type: application/json Date: Fri, 21 Aug 2020 12:18:51 GMT Expires: Fri, 21 Aug 2020 12:18:51 GMT Server: Apache/2.4.38 (Debian) Set-Cookie: PHPSESSID=00000000000000000000000000; path=/; HttpOnly Strict-Transport-Security: max-age=29030400 X-Powered-By: PHP/7.4.7 Transfer-Encoding: chunked 2b4d ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 0 prosody-13.0.1/spec/inputs/PaxHeaders/test_keys.lua0000644000000000000000000000011714773555365017371 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.687710916 prosody-13.0.1/spec/inputs/test_keys.lua0000644000175000017500000001626314773555365021600 0ustar00prosodyprosody00000000000000local 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; prosody-13.0.1/spec/PaxHeaders/json0000644000000000000000000000013114773555365014222 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.687710916 30 ctime=1743706869.699710965 prosody-13.0.1/spec/json/0000755000175000017500000000000014773555365016502 5ustar00prosodyprosody00000000000000prosody-13.0.1/spec/json/PaxHeaders/fail1.json0000644000000000000000000000011714773555365016172 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.687710916 prosody-13.0.1/spec/json/fail1.json0000644000175000017500000000007414773555365020372 0ustar00prosodyprosody00000000000000"A JSON payload should be an object or array, not a string."prosody-13.0.1/spec/json/PaxHeaders/fail10.json0000644000000000000000000000011714773555365016252 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.687710916 prosody-13.0.1/spec/json/fail10.json0000644000175000017500000000007214773555365020450 0ustar00prosodyprosody00000000000000{"Extra value after close": true} "misplaced quoted value"prosody-13.0.1/spec/json/PaxHeaders/fail11.json0000644000000000000000000000011714773555365016253 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.687710916 prosody-13.0.1/spec/json/fail11.json0000644000175000017500000000003514773555365020450 0ustar00prosodyprosody00000000000000{"Illegal expression": 1 + 2}prosody-13.0.1/spec/json/PaxHeaders/fail12.json0000644000000000000000000000011714773555365016254 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.691710933 prosody-13.0.1/spec/json/fail12.json0000644000175000017500000000003714773555365020453 0ustar00prosodyprosody00000000000000{"Illegal invocation": alert()}prosody-13.0.1/spec/json/PaxHeaders/fail13.json0000644000000000000000000000011714773555365016255 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.691710933 prosody-13.0.1/spec/json/fail13.json0000644000175000017500000000005314773555365020452 0ustar00prosodyprosody00000000000000{"Numbers cannot have leading zeroes": 013}prosody-13.0.1/spec/json/PaxHeaders/fail14.json0000644000000000000000000000011714773555365016256 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.691710933 prosody-13.0.1/spec/json/fail14.json0000644000175000017500000000003714773555365020455 0ustar00prosodyprosody00000000000000{"Numbers cannot be hex": 0x14}prosody-13.0.1/spec/json/PaxHeaders/fail15.json0000644000000000000000000000011714773555365016257 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.691710933 prosody-13.0.1/spec/json/fail15.json0000644000175000017500000000004214773555365020452 0ustar00prosodyprosody00000000000000["Illegal backslash escape: \x15"]prosody-13.0.1/spec/json/PaxHeaders/fail16.json0000644000000000000000000000011714773555365016260 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.691710933 prosody-13.0.1/spec/json/fail16.json0000644000175000017500000000001014773555365020446 0ustar00prosodyprosody00000000000000[\naked]prosody-13.0.1/spec/json/PaxHeaders/fail17.json0000644000000000000000000000011714773555365016261 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.691710933 prosody-13.0.1/spec/json/fail17.json0000644000175000017500000000004214773555365020454 0ustar00prosodyprosody00000000000000["Illegal backslash escape: \017"]prosody-13.0.1/spec/json/PaxHeaders/fail18.json0000644000000000000000000000011714773555365016262 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.691710933 prosody-13.0.1/spec/json/fail18.json0000644000175000017500000000006214773555365020457 0ustar00prosodyprosody00000000000000[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]]prosody-13.0.1/spec/json/PaxHeaders/fail19.json0000644000000000000000000000011714773555365016263 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.691710933 prosody-13.0.1/spec/json/fail19.json0000644000175000017500000000002614773555365020460 0ustar00prosodyprosody00000000000000{"Missing colon" null}prosody-13.0.1/spec/json/PaxHeaders/fail2.json0000644000000000000000000000011714773555365016173 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.691710933 prosody-13.0.1/spec/json/fail2.json0000644000175000017500000000002114773555365020363 0ustar00prosodyprosody00000000000000["Unclosed array"prosody-13.0.1/spec/json/PaxHeaders/fail20.json0000644000000000000000000000011714773555365016253 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.691710933 prosody-13.0.1/spec/json/fail20.json0000644000175000017500000000002714773555365020451 0ustar00prosodyprosody00000000000000{"Double colon":: null}prosody-13.0.1/spec/json/PaxHeaders/fail21.json0000644000000000000000000000011714773555365016254 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.691710933 prosody-13.0.1/spec/json/fail21.json0000644000175000017500000000004014773555365020445 0ustar00prosodyprosody00000000000000{"Comma instead of colon", null}prosody-13.0.1/spec/json/PaxHeaders/fail22.json0000644000000000000000000000011714773555365016255 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail22.json0000644000175000017500000000004114773555365020447 0ustar00prosodyprosody00000000000000["Colon instead of comma": false]prosody-13.0.1/spec/json/PaxHeaders/fail23.json0000644000000000000000000000011714773555365016256 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail23.json0000644000175000017500000000002414773555365020451 0ustar00prosodyprosody00000000000000["Bad value", truth]prosody-13.0.1/spec/json/PaxHeaders/fail24.json0000644000000000000000000000011714773555365016257 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail24.json0000644000175000017500000000002014773555365020446 0ustar00prosodyprosody00000000000000['single quote']prosody-13.0.1/spec/json/PaxHeaders/fail25.json0000644000000000000000000000011714773555365016260 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail25.json0000644000175000017500000000003514773555365020455 0ustar00prosodyprosody00000000000000[" tab character in string "]prosody-13.0.1/spec/json/PaxHeaders/fail26.json0000644000000000000000000000011714773555365016261 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail26.json0000644000175000017500000000004614773555365020460 0ustar00prosodyprosody00000000000000["tab\ character\ in\ string\ "]prosody-13.0.1/spec/json/PaxHeaders/fail27.json0000644000000000000000000000011714773555365016262 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail27.json0000644000175000017500000000001614773555365020456 0ustar00prosodyprosody00000000000000["line break"]prosody-13.0.1/spec/json/PaxHeaders/fail28.json0000644000000000000000000000011714773555365016263 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail28.json0000644000175000017500000000001714773555365020460 0ustar00prosodyprosody00000000000000["line\ break"]prosody-13.0.1/spec/json/PaxHeaders/fail29.json0000644000000000000000000000011714773555365016264 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail29.json0000644000175000017500000000000414773555365020455 0ustar00prosodyprosody00000000000000[0e]prosody-13.0.1/spec/json/PaxHeaders/fail3.json0000644000000000000000000000011714773555365016174 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail3.json0000644000175000017500000000004514773555365020372 0ustar00prosodyprosody00000000000000{unquoted_key: "keys must be quoted"}prosody-13.0.1/spec/json/PaxHeaders/fail30.json0000644000000000000000000000011714773555365016254 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail30.json0000644000175000017500000000000514773555365020446 0ustar00prosodyprosody00000000000000[0e+]prosody-13.0.1/spec/json/PaxHeaders/fail31.json0000644000000000000000000000011714773555365016255 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail31.json0000644000175000017500000000000714773555365020451 0ustar00prosodyprosody00000000000000[0e+-1]prosody-13.0.1/spec/json/PaxHeaders/fail32.json0000644000000000000000000000011714773555365016256 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail32.json0000644000175000017500000000005014773555365020450 0ustar00prosodyprosody00000000000000{"Comma instead if closing brace": true,prosody-13.0.1/spec/json/PaxHeaders/fail33.json0000644000000000000000000000011714773555365016257 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.695710949 prosody-13.0.1/spec/json/fail33.json0000644000175000017500000000001414773555365020451 0ustar00prosodyprosody00000000000000["mismatch"}prosody-13.0.1/spec/json/PaxHeaders/fail4.json0000644000000000000000000000011714773555365016175 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.699710965 prosody-13.0.1/spec/json/fail4.json0000644000175000017500000000002014773555365020364 0ustar00prosodyprosody00000000000000["extra comma",]prosody-13.0.1/spec/json/PaxHeaders/fail5.json0000644000000000000000000000011714773555365016176 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.699710965 prosody-13.0.1/spec/json/fail5.json0000644000175000017500000000003014773555365020366 0ustar00prosodyprosody00000000000000["double extra comma",,]prosody-13.0.1/spec/json/PaxHeaders/fail6.json0000644000000000000000000000011714773555365016177 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.699710965 prosody-13.0.1/spec/json/fail6.json0000644000175000017500000000003214773555365020371 0ustar00prosodyprosody00000000000000[ , "<-- missing value"]prosody-13.0.1/spec/json/PaxHeaders/fail7.json0000644000000000000000000000011714773555365016200 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.699710965 prosody-13.0.1/spec/json/fail7.json0000644000175000017500000000003214773555365020372 0ustar00prosodyprosody00000000000000["Comma after the close"],prosody-13.0.1/spec/json/PaxHeaders/fail8.json0000644000000000000000000000011714773555365016201 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.699710965 prosody-13.0.1/spec/json/fail8.json0000644000175000017500000000002014773555365020370 0ustar00prosodyprosody00000000000000["Extra close"]]prosody-13.0.1/spec/json/PaxHeaders/fail9.json0000644000000000000000000000011714773555365016202 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.699710965 prosody-13.0.1/spec/json/fail9.json0000644000175000017500000000002614773555365020377 0ustar00prosodyprosody00000000000000{"Extra comma": true,}prosody-13.0.1/spec/json/PaxHeaders/pass1.json0000644000000000000000000000011714773555365016225 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.699710965 prosody-13.0.1/spec/json/pass1.json0000644000175000017500000000264414773555365020432 0ustar00prosodyprosody00000000000000[ "JSON Test Pattern pass1", {"object with 1 member":["array with 1 element"]}, {}, [], -42, true, false, null, { "integer": 1234567890, "real": -9876.543210, "e": 0.123456789e-12, "E": 1.234567890E+34, "": 23456789012E66, "zero": 0, "one": 1, "space": " ", "quote": "\"", "backslash": "\\", "controls": "\b\f\n\r\t", "slash": "/ & \/", "alpha": "abcdefghijklmnopqrstuvwxyz", "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "digit": "0123456789", "0123456789": "digit", "special": "`1~!@#$%^&*()_+-={':[,]}|;.?", "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A", "true": true, "false": false, "null": null, "array":[ ], "object":{ }, "address": "50 St. James Street", "url": "http://www.JSON.org/", "comment": "// /* */": " ", " s p a c e d " :[1,2 , 3 , 4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7], "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}", "quotes": "" \u0022 %22 0x22 034 "", "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?" : "A key can be any string" }, 0.5 ,98.6 , 99.44 , 1066, 1e1, 0.1e1, 1e-1, 1e00,2e+00,2e-00 ,"rosebud"] prosody-13.0.1/spec/json/PaxHeaders/pass2.json0000644000000000000000000000011714773555365016226 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.699710965 prosody-13.0.1/spec/json/pass2.json0000644000175000017500000000006414773555365020425 0ustar00prosodyprosody00000000000000[[[[[[[[[[[[[[[[[[["Not too deep"]]]]]]]]]]]]]]]]]]]prosody-13.0.1/spec/json/PaxHeaders/pass3.json0000644000000000000000000000011714773555365016227 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.699710965 prosody-13.0.1/spec/json/pass3.json0000644000175000017500000000022414773555365020424 0ustar00prosodyprosody00000000000000{ "JSON Test Pattern pass3": { "The outermost value": "must be an object or array.", "In this test": "It is an object." } } prosody-13.0.1/spec/json/PaxHeaders/pass4.json0000644000000000000000000000011714773555365016230 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.699710965 prosody-13.0.1/spec/json/pass4.json0000644000175000017500000000010214773555365020420 0ustar00prosodyprosody00000000000000{ "one": [ ], "two": [], "three": [ ], "four": [ ] } prosody-13.0.1/spec/PaxHeaders/mod_bosh_spec.lua0000644000000000000000000000011714773555365016641 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.703710981 prosody-13.0.1/spec/mod_bosh_spec.lua0000644000175000017500000004404014773555365021042 0ustar00prosodyprosody00000000000000 -- Requires a host 'localhost' with SASL ANONYMOUS local bosh_url = "http://localhost:5280/http-bind" local logger = require "util.logger"; local debug = false; local print = print; if debug then logger.add_simple_sink(print, { --"debug"; "info"; "warn"; "error"; }); else print = function () end end describe("#mod_bosh", function () local server = require "net.server_select"; package.loaded["net.server"] = server; local async = require "util.async"; local timer = require "util.timer"; local http = require "net.http".new({ suppress_errors = false }); local function sleep(n) local wait, done = async.waiter(); timer.add_task(n, function () done() end); wait(); end local st = require "util.stanza"; local xml = require "util.xml"; local function request(url, opt, cb, auto_wait) local wait, done = async.waiter(); local ok, err; http:request(url, opt, function (...) ok, err = pcall(cb, ...); if not ok then print("CAUGHT", err) end done(); end); local function err_wait(throw) wait(); if throw ~= false and not ok then error(err); end return ok, err; end if auto_wait == false then return err_wait; else err_wait(); end end local function run_async(f) local err; local r = async.runner(); r:onerror(function (_, err_) print("EER", err_) err = err_; server.setquitting("once"); end) :onwaiting(function () --server.loop(); end) :run(function () f() server.setquitting("once"); end); server.loop(); if err then error(err); end if r.state ~= "ready" then error("Runner in unexpected state: "..r.state); end end it("test endpoint should be reachable", function () -- This is partly just to ensure the other tests have a chance to succeed -- (i.e. the BOSH endpoint is up and functioning) local function test() request(bosh_url, nil, function (resp, code) if code ~= 200 then error("Unable to reach BOSH endpoint "..bosh_url); end assert.is_string(resp); end); end run_async(test); end); it("should respond to past rids with past responses", function () local resp_1000_1, resp_1000_2 = "1", "2"; local function test_bosh() local sid; -- Set up BOSH session request(bosh_url, { body = tostring(st.stanza("body", { to = "localhost"; from = "test@localhost"; content = "text/xml; charset=utf-8"; hold = "1"; rid = "998"; wait = "10"; ["xml:lang"] = "en"; ["xmpp:version"] = "1.0"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }) :tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up() :tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" }) :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }) :tag("resource"):text("bosh-test1"):up() :up() :up() ); }, function (response_body) local resp = xml.parse(response_body); if not response_body:find("", 1, true) then print("ERR", resp:pretty_print()); error("Failed to set up BOSH session"); end sid = assert(resp.attr.sid); print("SID", sid); end); -- Receive some additional post-login stuff request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "999"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }) ) }, function (response_body) local resp = xml.parse(response_body); print("RESP 999", resp:pretty_print()); end); -- Send first long poll print "SEND 1000#1" local wait1000 = request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "1000"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; })) }, function (response_body) local resp = xml.parse(response_body); resp_1000_1 = resp; print("RESP 1000#1", resp:pretty_print()); end, false); -- Wait a couple of seconds sleep(2) -- Send an early request, causing rid 1000 to return early print "SEND 1001" local wait1001 = request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "1001"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; })) }, function (response_body) local resp = xml.parse(response_body); print("RESP 1001", resp:pretty_print()); end, false); -- Ensure we've received the response for rid 1000 wait1000(); -- Sleep a couple of seconds print "...pause..." sleep(2); -- Re-send rid 1000, we should get the same response print "SEND 1000#2" request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "1000"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; })) }, function (response_body) local resp = xml.parse(response_body); resp_1000_2 = resp; print("RESP 1000#2", resp:pretty_print()); end); local wait_final = request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "1002"; type = "terminate"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; })) }, function () end, false); print "WAIT 1001" wait1001(); wait_final(); print "DONE ALL" end run_async(test_bosh); assert.truthy(resp_1000_1); assert.same(resp_1000_1, resp_1000_2); end); it("should handle out-of-order requests", function () local function test() local sid; -- Set up BOSH session local wait, done = async.waiter(); http:request(bosh_url, { body = tostring(st.stanza("body", { to = "localhost"; from = "test@localhost"; content = "text/xml; charset=utf-8"; hold = "1"; rid = "1"; wait = "10"; ["xml:lang"] = "en"; ["xmpp:version"] = "1.0"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; })); }, function (response_body) local resp = xml.parse(response_body); sid = assert(resp.attr.sid, "Failed to set up BOSH session"); print("SID", sid); done(); end); print "WAIT 1" wait(); print "DONE 1" local rid2_response_received = false; -- Temporarily skip rid 2, to simulate missed request local wait3, done3 = async.waiter(); http:request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "3"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" }) :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up() :up() ) }, function (response_body) local resp = xml.parse(response_body); print("RESP 3", resp:pretty_print()); done3(); -- The server should not respond to this request until -- it has responded to rid 2 assert.is_true(rid2_response_received); end); print "SLEEPING" sleep(2); print "SLEPT" -- Send the "missed" rid 2 local wait2, done2 = async.waiter(); http:request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "2"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up() ) }, function (response_body) local resp = xml.parse(response_body); print("RESP 2", resp:pretty_print()); rid2_response_received = true; done2(); end); print "WAIT 2" wait2(); print "WAIT 3" wait3(); print "QUIT" end run_async(test); end); it("should work", function () local function test() local sid; -- Set up BOSH session local wait, done = async.waiter(); http:request(bosh_url, { body = tostring(st.stanza("body", { to = "localhost"; from = "test@localhost"; content = "text/xml; charset=utf-8"; hold = "1"; rid = "1"; wait = "10"; ["xml:lang"] = "en"; ["xmpp:version"] = "1.0"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; })); }, function (response_body) local resp = xml.parse(response_body); sid = assert(resp.attr.sid, "Failed to set up BOSH session"); print("SID", sid); done(); end); print "WAIT 1" wait(); print "DONE 1" local rid2_response_received = false; -- Send the "missed" rid 2 local wait2, done2 = async.waiter(); http:request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "2"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up() ) }, function (response_body) local resp = xml.parse(response_body); print("RESP 2", resp:pretty_print()); rid2_response_received = true; done2(); end); local wait3, done3 = async.waiter(); http:request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "3"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" }) :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up() :up() ) }, function (response_body) local resp = xml.parse(response_body); print("RESP 3", resp:pretty_print()); done3(); -- The server should not respond to this request until -- it has responded to rid 2 assert.is_true(rid2_response_received); end); print "SLEEPING" sleep(2); print "SLEPT" print "WAIT 2" wait2(); print "WAIT 3" wait3(); print "QUIT" end run_async(test); end); it("should handle aborted pending requests", function () local resp_1000_1, resp_1000_2 = "1", "2"; local function test_bosh() local sid; -- Set up BOSH session request(bosh_url, { body = tostring(st.stanza("body", { to = "localhost"; from = "test@localhost"; content = "text/xml; charset=utf-8"; hold = "1"; rid = "998"; wait = "10"; ["xml:lang"] = "en"; ["xmpp:version"] = "1.0"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }) :tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up() :tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" }) :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }) :tag("resource"):text("bosh-test1"):up() :up() :up() ); }, function (response_body) local resp = xml.parse(response_body); if not response_body:find("", 1, true) then print("ERR", resp:pretty_print()); error("Failed to set up BOSH session"); end sid = assert(resp.attr.sid); print("SID", sid); end); -- Receive some additional post-login stuff request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "999"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }) ) }, function (response_body) local resp = xml.parse(response_body); print("RESP 999", resp:pretty_print()); end); -- Send first long poll print "SEND 1000#1" local wait1000_1 = request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "1000"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; })) }, function (response_body) local resp = xml.parse(response_body); resp_1000_1 = resp; assert.is_nil(resp.attr.type); print("RESP 1000#1", resp:pretty_print()); end, false); -- Wait a couple of seconds sleep(2) -- Re-send rid 1000, we should eventually get a normal response (with no stanzas) print "SEND 1000#2" request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "1000"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; })) }, function (response_body) local resp = xml.parse(response_body); resp_1000_2 = resp; assert.is_nil(resp.attr.type); print("RESP 1000#2", resp:pretty_print()); end); wait1000_1(); print "DONE ALL" end run_async(test_bosh); assert.truthy(resp_1000_1); assert.same(resp_1000_1, resp_1000_2); end); it("should fail on requests beyond rid window", function () local function test_bosh() local sid; -- Set up BOSH session request(bosh_url, { body = tostring(st.stanza("body", { to = "localhost"; from = "test@localhost"; content = "text/xml; charset=utf-8"; hold = "1"; rid = "998"; wait = "10"; ["xml:lang"] = "en"; ["xmpp:version"] = "1.0"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }) :tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up() :tag("iq", { xmlns = "jabber:client", type = "set", id = "bind1" }) :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }) :tag("resource"):text("bosh-test1"):up() :up() :up() ); }, function (response_body) local resp = xml.parse(response_body); if not response_body:find("", 1, true) then print("ERR", resp:pretty_print()); error("Failed to set up BOSH session"); end sid = assert(resp.attr.sid); print("SID", sid); end); -- Receive some additional post-login stuff request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "999"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }) ) }, function (response_body) local resp = xml.parse(response_body); print("RESP 999", resp:pretty_print()); end); -- Send poll with a rid that's too high (current + 2, where only current + 1 is allowed) print "SEND 1002(!)" request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "1002"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; })) }, function (response_body) local resp = xml.parse(response_body); assert.equal("terminate", resp.attr.type); print("RESP 1002(!)", resp:pretty_print()); end); print "DONE ALL" end run_async(test_bosh); end); it("should always succeed for requests within the rid window", function () local function test() local sid; -- Set up BOSH session request(bosh_url, { body = tostring(st.stanza("body", { to = "localhost"; from = "test@localhost"; content = "text/xml; charset=utf-8"; hold = "1"; rid = "1"; wait = "10"; ["xml:lang"] = "en"; ["xmpp:version"] = "1.0"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; })); }, function (response_body) local resp = xml.parse(response_body); sid = assert(resp.attr.sid, "Failed to set up BOSH session"); print("SID", sid); end); print "DONE 1" request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "2"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }):tag("auth", { xmlns = "urn:ietf:params:xml:ns:xmpp-sasl", mechanism = "ANONYMOUS" }):up() ) }, function (response_body) local resp = xml.parse(response_body); print("RESP 2", resp:pretty_print()); end); local resp3; request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "3"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" }) :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up() :up() ) }, function (response_body) local resp = xml.parse(response_body); print("RESP 3#1", resp:pretty_print()); resp3 = resp; end); request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "4"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }):tag("iq", { xmlns = "jabber:client", type = "get", id = "ping1" }) :tag("ping", { xmlns = "urn:xmpp:ping" }):up() :up() ) }, function (response_body) local resp = xml.parse(response_body); print("RESP 4", resp:pretty_print()); end); request(bosh_url, { body = tostring(st.stanza("body", { sid = sid; rid = "3"; content = "text/xml; charset=utf-8"; ["xml:lang"] = "en"; xmlns = "http://jabber.org/protocol/httpbind"; ["xmlns:xmpp"] = "urn:xmpp:xbosh"; }):tag("iq", { xmlns = "jabber:client", type = "set", id = "bind" }) :tag("bind", { xmlns = "urn:ietf:params:xml:ns:xmpp-bind" }):up() :up() ) }, function (response_body) local resp = xml.parse(response_body); print("RESP 3#2", resp:pretty_print()); assert.not_equal("terminate", resp.attr.type); assert.same(resp3, resp); end); print "QUIT" end run_async(test); end); end); prosody-13.0.1/spec/PaxHeaders/muc_util_spec.lua0000644000000000000000000000011714773555365016670 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.703710981 prosody-13.0.1/spec/muc_util_spec.lua0000644000175000017500000000255414773555365021075 0ustar00prosodyprosody00000000000000local muc_util; local st = require "util.stanza"; do -- XXX Hack for lack of a mock moduleapi local env = setmetatable({ module = { _shared = {}; -- Close enough to the real module:shared() for our purposes here shared = function (self, name) local t = self._shared[name]; if t == nil then t = {}; self._shared[name] = t; end return t; end; } }, { __index = _ENV or _G }); muc_util = require "util.envload".envloadfile("plugins/muc/util.lib.lua", env)(); end describe("muc/util", function () describe("filter_muc_x()", function () it("correctly filters muc#user", function () local stanza = st.message({ to = "to", from = "from", id = "foo" }) :tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }) :tag("invite", { to = "user@example.com" }); assert.equal(1, #stanza.tags); assert.equal(stanza, muc_util.filter_muc_x(stanza)); assert.equal(0, #stanza.tags); end); it("correctly filters muc#user on a cloned stanza", function () local stanza = st.message({ to = "to", from = "from", id = "foo" }) :tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }) :tag("invite", { to = "user@example.com" }); assert.equal(1, #stanza.tags); local filtered = muc_util.filter_muc_x(st.clone(stanza)); assert.equal(1, #stanza.tags); assert.equal(0, #filtered.tags); end); end); end); prosody-13.0.1/spec/PaxHeaders/net_http_parser_spec.lua0000644000000000000000000000011714773555365020250 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.703710981 prosody-13.0.1/spec/net_http_parser_spec.lua0000644000175000017500000000757714773555365022467 0ustar00prosodyprosody00000000000000local http_parser = require "net.http.parser"; local sha1 = require "util.hashes".sha1; local parser_input_bytes = 3; local function CRLF(s) return (s:gsub("\n", "\r\n")); end local function test_stream(stream, expect) local chunks_processed = 0; local success_cb = spy.new(function (packet) assert.is_table(packet); if packet.body ~= false then assert.is_equal(expect.body, packet.body); end if expect.chunks then if chunks_processed == 0 then assert.is_true(packet.partial); packet.body_sink = { write = function (_, data) chunks_processed = chunks_processed + 1; assert.equal(expect.chunks[chunks_processed], data); return true; end; }; end end end); local function options_cb() return { -- Force streaming API mode body_size_limit = expect.chunks and 0 or nil; buffer_size_limit = 10*1024*2; }; end local parser = http_parser.new(success_cb, error, (stream[1] or stream):sub(1,4) == "HTTP" and "client" or "server", options_cb) if type(stream) == "string" then for chunk in stream:gmatch("."..string.rep(".?", parser_input_bytes-1)) do parser:feed(chunk); end else for _, chunk in ipairs(stream) do parser:feed(chunk); end end if expect.chunks then assert.equal(chunks_processed, #expect.chunks); end assert.spy(success_cb).was_called(expect.count or 1); end describe("net.http.parser", function() describe("parser", function() it("should handle requests with no content-length or body", function () test_stream( CRLF[[ GET / HTTP/1.1 Host: example.com ]], { body = ""; } ); end); it("should handle responses with empty body", function () test_stream( CRLF[[ HTTP/1.1 200 OK Content-Length: 0 ]], { body = ""; } ); end); it("should handle simple responses", function () test_stream( CRLF[[ HTTP/1.1 200 OK Content-Length: 7 Hello ]], { body = "Hello\r\n", count = 1; } ); end); it("should handle chunked encoding in responses", function () test_stream( CRLF[[ HTTP/1.1 200 OK Transfer-Encoding: chunked 1 H 1 e 2 ll 1 o 0 ]], { body = "Hello", count = 3; } ); end); it("should handle a stream of responses", function () test_stream( CRLF[[ HTTP/1.1 200 OK Content-Length: 5 Hello HTTP/1.1 200 OK Transfer-Encoding: chunked 1 H 1 e 2 ll 1 o 0 ]], { body = "Hello", count = 4; } ); end); it("should correctly find chunk boundaries", function () test_stream({ CRLF[[ HTTP/1.1 200 OK Transfer-Encoding: chunked ]].."3\r\n:)\n\r\n"}, { count = 1; -- Once (partial) chunks = { ":)\n" }; } ); end); it("should reject very large request heads", function() local finished = false; local success_cb = spy.new(function() finished = true; end) local error_cb = spy.new(function() finished = true; end) local parser = http_parser.new(success_cb, error_cb, "server", function() return { head_size_limit = 1024; body_size_limit = 1024; buffer_size_limit = 2048 }; end) parser:feed("GET / HTTP/1.1\r\n"); for i = 1, 64 do -- * header line > buffer_size_limit parser:feed(string.format("Header-%04d: Yet-AnotherValue\r\n", i)); if finished then -- should hit an error around half-way break end end if not finished then parser:feed("\r\n") end assert.spy(success_cb).was_called(0); assert.spy(error_cb).was_called(1); assert.spy(error_cb).was_called_with("header-too-large"); end) end); it("should handle large chunked responses", function () local data = io.open("spec/inputs/http/httpstream-chunked-test.txt", "rb"):read("*a"); -- Just a sanity check... text editors and things may mess with line endings, etc. assert.equal("25930f021785ae14053a322c2dbc1897c3769720", sha1(data, true), "test data malformed"); test_stream(data, { body = string.rep("~", 11085), count = 3; }); end); end); prosody-13.0.1/spec/PaxHeaders/net_http_server_spec.lua0000644000000000000000000000011714773555365020262 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.703710981 prosody-13.0.1/spec/net_http_server_spec.lua0000644000175000017500000000071614773555365022465 0ustar00prosodyprosody00000000000000describe("net.http.server", function () package.loaded["net.server"] = {} local server = require "net.http.server"; describe("events", function () it("should work with util.helpers", function () -- See #1044 server.add_handler("GET host/foo/*", function () end, 0); server.add_handler("GET host/foo/bar", function () end, 0); local helpers = require "util.helpers"; assert.is.string(helpers.show_events(server._events)); end); end); end); prosody-13.0.1/spec/PaxHeaders/net_resolvers_service_spec.lua0000644000000000000000000000011714773555365021461 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.703710981 prosody-13.0.1/spec/net_resolvers_service_spec.lua0000644000175000017500000001525514773555365023670 0ustar00prosodyprosody00000000000000local 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); prosody-13.0.1/spec/PaxHeaders/net_stun_spec.lua0000644000000000000000000000011714773555365016706 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.703710981 prosody-13.0.1/spec/net_stun_spec.lua0000644000175000017500000000556714773555365021122 0ustar00prosodyprosody00000000000000local hex = require "util.hex"; local function parse(pkt_desc) local result = {}; for line in pkt_desc:gmatch("([^\n]+)\n") do local b1, b2, b3, b4 = line:match("^%s*(%x%x) (%x%x) (%x%x) (%x%x)%s"); if b1 then table.insert(result, b1); table.insert(result, b2); table.insert(result, b3); table.insert(result, b4); end end return hex.decode(table.concat(result)); end local sample_packet = parse[[ 00 01 00 60 Request type and message length 21 12 a4 42 Magic cookie 78 ad 34 33 } c6 ad 72 c0 } Transaction ID 29 da 41 2e } 00 06 00 12 USERNAME attribute header e3 83 9e e3 } 83 88 e3 83 } aa e3 83 83 } Username value (18 bytes) and padding (2 bytes) e3 82 af e3 } 82 b9 00 00 } 00 15 00 1c NONCE attribute header 66 2f 2f 34 } 39 39 6b 39 } 35 34 64 36 } 4f 4c 33 34 } Nonce value 6f 4c 39 46 } 53 54 76 79 } 36 34 73 41 } 00 14 00 0b REALM attribute header 65 78 61 6d } 70 6c 65 2e } Realm value (11 bytes) and padding (1 byte) 6f 72 67 00 } 00 08 00 14 MESSAGE-INTEGRITY attribute header f6 70 24 65 } 6d d6 4a 3e } 02 b8 e0 71 } HMAC-SHA1 fingerprint 2e 85 c9 a2 } 8c a8 96 66 } ]]; describe("net.stun", function () local stun = require "net.stun"; it("works", function () local packet = stun.new_packet(); assert.is_string(packet:serialize()); end); it("can decode the sample packet", function () local packet = stun.new_packet():deserialize(sample_packet); assert(packet); local method, method_name = packet:get_method(); assert.equal(1, method); assert.equal("binding", method_name); assert.equal("example.org", packet:get_attribute("realm")); end); it("can generate the sample packet", function () -- These values, and the sample packet, come from RFC 5769 2.4 local username = string.char( -- U+30DE KATAKANA LETTER MA 0xE3, 0x83, 0x9E, -- U+30C8 KATAKANA LETTER TO 0xE3, 0x83, 0x88, -- U+30EA KATAKANA LETTER RI 0xE3, 0x83, 0xAA, -- U+30C3 KATAKANA LETTER SMALL TU 0xE3, 0x83, 0x83, -- U+30AF KATAKANA LETTER KU 0xE3, 0x82, 0xAF, -- U+30B9 KATAKANA LETTER SU 0xE3, 0x82, 0xB9 ); -- Password: "TheMtr" and "TheMatrIX" (without -- quotes) respectively before and after SASLprep processing local password = "TheMatrIX"; local realm = "example.org"; local p3 = stun.new_packet("binding", "request"); p3.transaction_id = hex.decode("78AD3433C6AD72C029DA412E"); p3:add_attribute("username", username); p3:add_attribute("nonce", "f//499k954d6OL34oL9FSTvy64sA"); p3:add_attribute("realm", realm); local key = stun.get_long_term_auth_key(realm, username, password); p3:add_message_integrity(key); assert.equal(sample_packet, p3:serialize()); end); end); prosody-13.0.1/spec/PaxHeaders/net_websocket_frames_spec.lua0000644000000000000000000000011714773555365021240 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.703710981 prosody-13.0.1/spec/net_websocket_frames_spec.lua0000644000175000017500000000454114773555365023443 0ustar00prosodyprosody00000000000000describe("net.websocket.frames", function () local nwf = require "net.websocket.frames"; local test_frames = { simple_empty = { ["opcode"] = 0; ["length"] = 0; ["data"] = ""; ["FIN"] = false; ["MASK"] = false; ["RSV1"] = false; ["RSV2"] = false; ["RSV3"] = false; }; simple_data = { ["opcode"] = 0; ["length"] = 5; ["data"] = "hello"; ["FIN"] = false; ["MASK"] = false; ["RSV1"] = false; ["RSV2"] = false; ["RSV3"] = false; }; simple_fin = { ["opcode"] = 0; ["length"] = 0; ["data"] = ""; ["FIN"] = true; ["MASK"] = false; ["RSV1"] = false; ["RSV2"] = false; ["RSV3"] = false; }; with_mask = { ["opcode"] = 0; ["length"] = 5; ["data"] = "hello"; ["key"] = " \0 \0"; ["FIN"] = true; ["MASK"] = true; ["RSV1"] = false; ["RSV2"] = false; ["RSV3"] = false; }; empty_with_mask = { ["opcode"] = 0; ["key"] = " \0 \0"; ["FIN"] = true; ["MASK"] = true; ["RSV1"] = false; ["RSV2"] = false; ["RSV3"] = false; }; ping = { ["opcode"] = 0x9; ["length"] = 4; ["data"] = "ping"; ["FIN"] = true; ["MASK"] = false; ["RSV1"] = false; ["RSV2"] = false; ["RSV3"] = false; }; pong = { ["opcode"] = 0xa; ["length"] = 4; ["data"] = "pong"; ["FIN"] = true; ["MASK"] = false; ["RSV1"] = false; ["RSV2"] = false; ["RSV3"] = false; }; } describe("build", function () local build = nwf.build; it("works", function () assert.equal("\0\0", build(test_frames.simple_empty)); assert.equal("\0\5hello", build(test_frames.simple_data)); assert.equal("\128\0", build(test_frames.simple_fin)); assert.equal("\128\133 \0 \0HeLlO", build(test_frames.with_mask)) assert.equal("\128\128 \0 \0", build(test_frames.empty_with_mask)) assert.equal("\137\4ping", build(test_frames.ping)); assert.equal("\138\4pong", build(test_frames.pong)); end); end); describe("parse", function () local parse = nwf.parse; it("works", function () assert.same(test_frames.simple_empty, parse("\0\0")); assert.same(test_frames.simple_data, parse("\0\5hello")); assert.same(test_frames.simple_fin, parse("\128\0")); assert.same(test_frames.with_mask, parse("\128\133 \0 \0HeLlO")); assert.same(test_frames.ping, parse("\137\4ping")); assert.same(test_frames.pong, parse("\138\4pong")); end); end); end); prosody-13.0.1/spec/PaxHeaders/scansion0000644000000000000000000000013114773555365015066 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.707710996 30 ctime=1743706869.731711093 prosody-13.0.1/spec/scansion/0000755000175000017500000000000014773555365017346 5ustar00prosodyprosody00000000000000prosody-13.0.1/spec/scansion/PaxHeaders/admins.txt0000644000000000000000000000011714773555365017163 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.707710996 prosody-13.0.1/spec/scansion/admins.txt0000644000175000017500000000002014773555365021352 0ustar00prosodyprosody00000000000000admin@localhost prosody-13.0.1/spec/scansion/PaxHeaders/basic.scs0000644000000000000000000000011714773555365016742 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.707710996 prosody-13.0.1/spec/scansion/basic.scs0000644000175000017500000000030414773555365021136 0ustar00prosodyprosody00000000000000# Basic login and initial presence [Client] Romeo jid: user@localhost password: password --------- Romeo connects Romeo sends: Romeo receives: Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/basic_message.scs0000644000000000000000000000011714773555365020446 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.707710996 prosody-13.0.1/spec/scansion/basic_message.scs0000644000175000017500000000725314773555365022654 0ustar00prosodyprosody00000000000000# Basic message routing and delivery [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: juliet@localhost password: password [Client] Juliet's phone jid: juliet@localhost password: password resource: mobile --------- # Act 1, scene 1 # The clients connect Romeo connects Juliet connects Juliet's phone connects # Romeo publishes his presence. Juliet has not, and so does not receive presence. Romeo sends: Romeo receives: # Romeo sends a message to Juliet's full JID Romeo sends: Hello Juliet! Juliet receives: Hello Juliet! # Romeo sends a message to Juliet's phone Romeo sends: Hello Juliet, on your phone. Juliet's phone receives: Hello Juliet, on your phone. # Scene 2 # This requires the server to support offline messages (which is optional). # Romeo sends a message to Juliet's bare JID. This is not immediately delivered, as she # has not published presence on either of her resources. Romeo sends: Hello Juliet, are you there? # Juliet sends presence on her phone, and should receive the message there Juliet's phone sends: Juliet's phone receives: Juliet's phone receives: Hello Juliet, are you there? # Romeo sends another bare-JID message, it should be delivered # instantly to Juliet's phone Romeo sends: Oh, hi! Juliet's phone receives: Oh, hi! # Juliet's laptop goes online, but with a negative priority Juliet sends: -1 Juliet receives: -1 Juliet's phone receives: -1 # Again, Romeo sends a message to her bare JID, but it should # only get delivered to her phone: Romeo sends: How are you? Juliet's phone receives: How are you? # Romeo sends direct to Juliet's full JID, and she should receive it Romeo sends: Are you hiding? Juliet receives: Are you hiding? # Juliet publishes non-negative presence Juliet sends: Juliet receives: Juliet's phone receives: # And now Romeo's bare JID messages get delivered to both resources # (server behaviour may vary here) Romeo sends: There! Juliet receives: There! Juliet's phone receives: There! # The End Romeo disconnects Juliet disconnects Juliet's phone disconnects prosody-13.0.1/spec/scansion/PaxHeaders/basic_roster.scs0000644000000000000000000000011714773555365020340 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.707710996 prosody-13.0.1/spec/scansion/basic_roster.scs0000644000175000017500000000241214773555365022536 0ustar00prosodyprosody00000000000000# Basic roster test [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: juliet@localhost password: password --------- Romeo connects Juliet connects Romeo sends: Romeo receives: Romeo sends: Romeo receives: # Add nurse to roster Romeo sends: # Receive the roster add result Romeo receives: # Receive the roster push Romeo receives: Romeo sends: # Fetch the roster, it should include nurse now Romeo sends: Romeo receives: Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/blocking.scs0000644000000000000000000000011714773555365017451 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.707710996 prosody-13.0.1/spec/scansion/blocking.scs0000644000175000017500000000674714773555365021666 0ustar00prosodyprosody00000000000000# XEP-0191: Blocking Command [Client] Romeo jid: blocker@localhost password: password [Client] Juliet jid: blockee@localhost password: password ----- # The parties connect Romeo connects Romeo sends: Romeo receives: Juliet connects Juliet sends: Juliet receives: # They add each other Romeo sends: Romeo receives: Juliet receives: Juliet sends: Romeo receives: Juliet sends: Juliet receives: Romeo receives: Romeo sends: Juliet receives: Romeo receives: # They can now talk Juliet sends: ohai Romeo receives: ohai # And now to the blockining Romeo sends: Romeo receives: Juliet receives: # Can"t talk anymore Romeo sends: hello? Romeo receives: You have blocked this JID Juliet sends: Juliet receives: Romeo sends: Juliet receives: Romeo receives: # Can talk again Romeo sends: hello! Juliet receives: hello! # Bye Juliet sends: Juliet disconnects Romeo receives: Romeo sends: Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/bookmarks2.scs0000644000000000000000000000011714773555365017733 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.711711013 prosody-13.0.1/spec/scansion/bookmarks2.scs0000644000175000017500000001255214773555365022137 0ustar00prosodyprosody00000000000000# 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/ <<< "" Juliet sends: OTy9GPCvBZRvqzOHmD/ThA1WbBH3tNoeKbdqKQCRPHc= f/rxDeTf6HyjQ382V3GEG/UfAs5IeclC05jBSBnVQCI= ucfqg/NrLj0omE+26hYMrbpcmxHcU4Z3hfAQIF+6tt0= Juliet receives: Juliet sends: Juliet sends: JC http://jabber.org/protocol/pubsub#publish-options true 255 never whitelist Juliet receives: JC Juliet receives: Juliet sends: JC http://jabber.org/protocol/pubsub#publish-options true 255 never whitelist Juliet receives: JC Juliet receives: Juliet sends: Juliet receives: Juliet receives: Juliet disconnects // vim: syntax=xml: prosody-13.0.1/spec/scansion/PaxHeaders/disco_self.scs0000644000000000000000000000011714773555365017773 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.711711013 prosody-13.0.1/spec/scansion/disco_self.scs0000644000175000017500000000114414773555365022172 0ustar00prosodyprosody00000000000000# Basic login and initial presence [Client] Romeo jid: discoverer@localhost password: password --------- Romeo connects Romeo sends: Romeo receives: Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/empty_bookmarks.scs0000644000000000000000000000011714773555365021067 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.711711013 prosody-13.0.1/spec/scansion/empty_bookmarks.scs0000644000175000017500000000107314773555365023267 0ustar00prosodyprosody00000000000000# mod_scansion_record on host 'localhost' recording started 2022-07-26T21:39:55Z [Client] Romeo password: password jid: juliet@localhost/UaksS4M1xYZB ----- Romeo connects Romeo sends: Romeo receives: Romeo disconnects # recording ended on 2022-07-26T21:40:45Z prosody-13.0.1/spec/scansion/PaxHeaders/extdisco.scs0000644000000000000000000000011714773555365017503 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.711711013 prosody-13.0.1/spec/scansion/extdisco.scs0000644000175000017500000000470614773555365021711 0ustar00prosodyprosody00000000000000# XEP-0215: External Service Discovery [Client] Romeo password: password jid: user@localhost/mFquWxSr ----- Romeo connects Romeo sends: Romeo receives: Romeo sends: Romeo receives: Romeo sends: Romeo receives: Romeo sends: Romeo receives: Romeo disconnects # recording ended on 2020-07-18T16:47:57Z prosody-13.0.1/spec/scansion/PaxHeaders/http_upload.scs0000644000000000000000000000011714773555365020204 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.711711013 prosody-13.0.1/spec/scansion/http_upload.scs0000644000175000017500000000552314773555365022410 0ustar00prosodyprosody00000000000000# XEP-0363 HTTP Upload with mod_http_file_share [Client] Romeo password: password jid: filesharingenthusiast@localhost/krxLaE3s ----- Romeo connects Romeo sends: Romeo receives:
Romeo sends: Romeo receives: File too large 10000000 Romeo sends: Romeo receives: File size must be positive integer Romeo sends: Romeo receives: Invalid filename Romeo sends: Romeo receives: File type not allowed Romeo disconnects # recording ended on 2021-01-27T22:10:46Z prosody-13.0.1/spec/scansion/PaxHeaders/issue1121.scs0000644000000000000000000000011714773555365017316 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.711711013 prosody-13.0.1/spec/scansion/issue1121.scs0000644000175000017500000000306314773555365021517 0ustar00prosodyprosody00000000000000# When removing roster contact, Prosody should send directed "unavailable" presence but sends global unavailable presence [Client] Romeo jid: romeo@localhost password: password [Client] Juliet jid: juliet@localhost password: password ----- Romeo connects Romeo sends Romeo receives Juliet connects Juliet sends Juliet receives Romeo sends Romeo receives Juliet receives Juliet sends Romeo receives Juliet sends Juliet receives Romeo receives Romeo sends Juliet receives Romeo receives Juliet sends Juliet receives Romeo receives Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/issue1224.scs0000644000000000000000000000011714773555365017322 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.711711013 prosody-13.0.1/spec/scansion/issue1224.scs0000644000175000017500000000601014773555365021516 0ustar00prosodyprosody00000000000000# MUC: Handle affiliation changes from buggy clients [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: # Submit config form Romeo sends: http://jabber.org/protocol/muc#roomconfig Romeo receives: Romeo sends: Romeo receives: Romeo receives: # Juliet connects, and joins the room Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: # Romeo makes Juliet a member of the room, however his client is buggy and only # specifies her nickname Romeo sends: Romeo receives: Romeo receives: Juliet receives: prosody-13.0.1/spec/scansion/PaxHeaders/issue505.scs0000644000000000000000000000011714773555365017243 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.715711029 prosody-13.0.1/spec/scansion/issue505.scs0000644000175000017500000000363614773555365021452 0ustar00prosodyprosody00000000000000# Issue 505: mod_muc doesn’t forward part statuses [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: # Submit config form Romeo sends: http://jabber.org/protocol/muc#roomconfig Romeo receives: # Juliet connects, and joins the room Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: Juliet sends: Farewell Romeo receives: Farewell prosody-13.0.1/spec/scansion/PaxHeaders/issue978-multi.scs0000644000000000000000000000011714773555365020411 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.715711029 prosody-13.0.1/spec/scansion/issue978-multi.scs0000644000175000017500000000567414773555365022624 0ustar00prosodyprosody00000000000000# Issue 978: MUC does not carry error into occupant leave status (multiple clients) [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password [Client] Juliet's phone jid: user2@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: # Submit config form Romeo sends: http://jabber.org/protocol/muc#roomconfig Romeo receives: # Juliet connects, and joins the room Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: # Juliet's phone connects, and joins the room Juliet's phone connects Juliet's phone sends: Juliet's phone receives: Juliet's phone receives: Juliet's phone receives: Romeo receives: # Juliet leaves with an error Juliet sends: Test error Romeo receives: prosody-13.0.1/spec/scansion/PaxHeaders/issue978.scs0000644000000000000000000000011714773555365017261 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.715711029 prosody-13.0.1/spec/scansion/issue978.scs0000644000175000017500000000431514773555365021463 0ustar00prosodyprosody00000000000000# Issue 978: MUC does not carry error into occupant leave status (single client) [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: # Submit config form Romeo sends: http://jabber.org/protocol/muc#roomconfig anyone Romeo receives: # Juliet connects, and joins the room Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: Juliet sends: Test error Romeo receives: Kicked: service unavailable: Test error prosody-13.0.1/spec/scansion/PaxHeaders/keep_full_sub_req.scs0000644000000000000000000000011714773555365021347 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.715711029 prosody-13.0.1/spec/scansion/keep_full_sub_req.scs0000644000175000017500000000204014773555365023542 0ustar00prosodyprosody00000000000000# server MUST keep a record of the complete presence stanza comprising the subscription request (#689) [Client] Alice jid: pars-a@localhost password: password [Client] Bob jid: pars-b@localhost password: password [Client] Bob's phone jid: pars-b@localhost/phone password: password --------- Alice connects Alice sends: Alice disconnects Bob connects Bob sends: Bob receives: Bob receives: Bob disconnects # Works if they reconnect too Bob's phone connects Bob's phone sends: Bob's phone receives: Bob's phone receives: Bob's phone disconnects prosody-13.0.1/spec/scansion/PaxHeaders/lastactivity.scs0000644000000000000000000000011714773555365020401 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.715711029 prosody-13.0.1/spec/scansion/lastactivity.scs0000644000175000017500000000147314773555365022605 0ustar00prosodyprosody00000000000000# XEP-0012: Last Activity / mod_lastactivity [Client] Romeo jid: romeo@localhost password: password ----- Romeo connects Romeo sends: Hello Romeo receives: Hello Romeo sends: Goodbye Romeo receives: Goodbye # mod_lastlog saves time + status message from the last unavailable presence Romeo sends: Romeo receives: Goodbye Romeo disconnects # recording ended on 2020-04-20T14:39:47Z prosody-13.0.1/spec/scansion/PaxHeaders/mam_extended.scs0000644000000000000000000000011714773555365020313 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.715711029 prosody-13.0.1/spec/scansion/mam_extended.scs0000644000175000017500000000671214773555365022520 0ustar00prosodyprosody00000000000000# MAM 0.7.x Extended features [Client] Romeo jid: extmamtester@localhost password: password --------- Romeo connects # Enable MAM so we can save some messages Romeo sends: Romeo receives: # Some messages to look for later Romeo sends: Hello Romeo sends: U there? # Metadata Romeo sends: Romeo receives: Romeo sends: Romeo receives: Hello Romeo receives: U there? # FIXME unstable tag order from util.rsm Romeo receives: # Get results in reverse order Romeo sends: Romeo receives: U there? Romeo receives: Hello # FIXME unstable tag order from util.rsm Romeo receives: prosody-13.0.1/spec/scansion/PaxHeaders/mam_prefs_prep.scs0000644000000000000000000000011714773555365020660 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.715711029 prosody-13.0.1/spec/scansion/mam_prefs_prep.scs0000644000175000017500000000113314773555365023055 0ustar00prosodyprosody00000000000000# mod_mam should apply JIDprep in prefs [Client] Romeo jid: romeo@localhost password: password ----- Romeo connects Romeo sends: JULIET@MONTAGUE.LIT MONTAGUE@MONTAGUE.LIT Romeo receives: juliet@montague.lit montague@montague.lit Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/muc_affiliation_notify.scs0000644000000000000000000000011714773555365022402 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.715711029 prosody-13.0.1/spec/scansion/muc_affiliation_notify.scs0000644000175000017500000000700214773555365024600 0ustar00prosodyprosody00000000000000# MUC: Notification of affiliation changes of non-occupants [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password [Client] Rosaline jid: user3@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: # Submit config form Romeo sends: http://jabber.org/protocol/muc#roomconfig Romeo receives: # Promote Juliet to member Romeo sends: # Juliet is not in the room, so an affiliation change message is received Romeo receives: # The affiliation change succeeded Romeo receives: # Juliet connects, and joins the room Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: # To check the status of the room is as expected, Romeo requests the member list Romeo sends: Romeo receives: # Romeo grants membership to Rosaline, who is not in the room Romeo sends: Romeo receives: Romeo receives: Romeo sends: Finished! Juliet receives: Finished! prosody-13.0.1/spec/scansion/PaxHeaders/muc_create_destroy.scs0000644000000000000000000000011714773555365021541 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.715711029 prosody-13.0.1/spec/scansion/muc_create_destroy.scs0000644000175000017500000002074414773555365023747 0ustar00prosodyprosody00000000000000# MUC creation, basic messages and destruction [Client] Romeo jid: romeo@localhost/mK0dD6Ha password: password [Client] Juliet jid: juliet@localhost/lVwkim_k password: password [Client] Admin jid: admin@localhost/DfNgg9VE password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: Romeo sends: Romeo receives: Juliet connects Romeo sends: Where are thou my Juliet? Romeo receives: Where are thou my Juliet? Juliet sends: Juliet receives: Juliet receives: Juliet receives: Where are thou my Juliet? Juliet receives: Romeo receives: Juliet sends: /me jumps out from behind a tree Romeo receives: /me jumps out from behind a tree Juliet receives: /me jumps out from behind a tree Juliet sends: Here I am! Romeo receives: Here I am! Juliet receives: Here I am! Romeo sends: What is this place? Romeo receives: What is this place? Juliet receives: What is this place? Juliet sends: I think we're in a script! Romeo receives: I think we're in a script! Juliet receives: I think we're in a script! Romeo sends: Oh no! Does that mean our love is not real?! Romeo receives: Oh no! Does that mean our love is not real?! Juliet receives: Oh no! Does that mean our love is not real?! Juliet sends: I refuse to accept this! Let's burn this place to the ground! Romeo receives: I refuse to accept this! Let's burn this place to the ground! Juliet receives: I refuse to accept this! Let's burn this place to the ground! Romeo sends: Yes! Romeo receives: Yes! Juliet receives: Yes! Romeo sends: We refuse to live in this fantasy! Juliet receives: We refuse to live in this fantasy! Romeo receives: We refuse to live in this fantasy! Romeo receives: Juliet disconnects Romeo sends: Romeo receives: Romeo receives: Romeo sends: Romeo receives: Admin connects Admin sends: elsewhere@conference.localhost Romeo receives: Romeo disconnects Admin receives: The following rooms were destroyed: elsewhere@conference.localhost Admin disconnects # recording ended on 2019-08-31T13:45:32Z prosody-13.0.1/spec/scansion/PaxHeaders/muc_mediated_invite.scs0000644000000000000000000000011714773555365021657 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.715711029 prosody-13.0.1/spec/scansion/muc_mediated_invite.scs0000644000175000017500000000332714773555365024063 0ustar00prosodyprosody00000000000000# MUC: Mediated invites [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: # Submit config form Romeo sends: http://jabber.org/protocol/muc#roomconfig Romeo receives: # Juliet connects Juliet connects Juliet sends: Juliet receives: # Romeo invites Juliet to join the room Romeo sends: Juliet receives: room@conference.localhost/Romeo invited you to the room room@conference.localhost prosody-13.0.1/spec/scansion/PaxHeaders/muc_members_only_change.scs0000644000000000000000000000011714773555365022525 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.715711029 prosody-13.0.1/spec/scansion/muc_members_only_change.scs0000644000175000017500000000554114773555365024731 0ustar00prosodyprosody00000000000000# MUC: Members-only rooms kick members who lose affiliation [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: # Submit config form, set the room to members-only Romeo sends: http://jabber.org/protocol/muc#roomconfig 1 Romeo receives: # Romeo adds Juliet to the member list Romeo sends: Romeo receives: Romeo receives: # Juliet connects, and joins the room Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: # Romeo removes Juliet's membership status Romeo sends: # As a non-member, Juliet must now be removed from the room Romeo receives: Romeo receives: Romeo disconnects Juliet disconnects prosody-13.0.1/spec/scansion/PaxHeaders/muc_members_only_deregister.scs0000644000000000000000000000011714773555365023435 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.719711045 prosody-13.0.1/spec/scansion/muc_members_only_deregister.scs0000644000175000017500000000576614773555365025652 0ustar00prosodyprosody00000000000000# MUC: Members-only rooms kick members who deregister [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: # Submit config form, set the room to members-only Romeo sends: http://jabber.org/protocol/muc#roomconfig 1 Romeo receives: # Romeo adds Juliet to the member list Romeo sends: Romeo receives: Romeo receives: # Juliet connects, and joins the room Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: # Tired of Romeo's company, Juliet unregisters from the room Juliet sends: Juliet receives: Juliet receives: Romeo receives: Romeo disconnects Juliet disconnects prosody-13.0.1/spec/scansion/PaxHeaders/muc_nickname_change.scs0000644000000000000000000000011714773555365021617 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.719711045 prosody-13.0.1/spec/scansion/muc_nickname_change.scs0000644000175000017500000000644714773555365024031 0ustar00prosodyprosody00000000000000# MUC: Change nickname # Make sure a role is not reset, see #1466 [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: Romeo sends: http://jabber.org/protocol/muc#roomconfig 1 Romeo receives: Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: Romeo sends: http://jabber.org/protocol/muc#roomconfig 0 Romeo receives: Juliet receives: Juliet sends: Juliet receives: Juliet receives: prosody-13.0.1/spec/scansion/PaxHeaders/muc_nickname_robotface.scs0000644000000000000000000000011714773555365022336 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.719711045 prosody-13.0.1/spec/scansion/muc_nickname_robotface.scs0000644000175000017500000000217614773555365024543 0ustar00prosodyprosody00000000000000# MUC: Prevent nicknames failing strict resourceprep [Client] Romeo jid: user@localhost password: password [Client] Roboteo jid: bot@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: Roboteo connects Roboteo sends: Roboteo receives: Nickname must pass strict validation prosody-13.0.1/spec/scansion/PaxHeaders/muc_outcast_reason.scs0000644000000000000000000000011714773555365021556 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.719711045 prosody-13.0.1/spec/scansion/muc_outcast_reason.scs0000644000175000017500000000335314773555365023761 0ustar00prosodyprosody00000000000000# Save ban reason [Client] Romeo password: password jid: user@localhost ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: Romeo sends: Hey calm down Romeo receives: Hey calm down Romeo receives: Romeo sends: Romeo receives: Hey calm down Romeo disconnects Romeo sends: prosody-13.0.1/spec/scansion/PaxHeaders/muc_password.scs0000644000000000000000000000011714773555365020367 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.719711045 prosody-13.0.1/spec/scansion/muc_password.scs0000644000175000017500000000711714773555365022574 0ustar00prosodyprosody00000000000000# MUC: Password-protected rooms [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: # Submit config form Romeo sends: http://jabber.org/protocol/muc#roomconfig cauldronburn Romeo receives: # Juliet connects, and tries to join the room (password-protected) Juliet connects Juliet sends: Juliet receives: # Retry with the correct password Juliet sends: cauldronburn Juliet receives: Juliet receives: Juliet receives: Romeo receives: # Ok, now Juliet leaves, and Romeo unsets the password Juliet sends: Romeo receives: Juliet receives: # Remove room password Romeo sends: http://jabber.org/protocol/muc#roomconfig # Config change success Romeo receives: # Notification of room configuration update Romeo receives: # Juliet tries to join (should succeed) Juliet sends: # Notification of Romeo's presence in the room Juliet receives: Juliet receives: # Room topic Juliet receives: Romeo receives: prosody-13.0.1/spec/scansion/PaxHeaders/muc_presence_probe.scs0000644000000000000000000000011714773555365021520 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.719711045 prosody-13.0.1/spec/scansion/muc_presence_probe.scs0000644000175000017500000001066714773555365023731 0ustar00prosodyprosody00000000000000# #1535 Let MUCs respond to presence probes [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password [Client] Mercutio jid: user3@localhost password: password ----- Romeo connects # Romeo joins the MUC Romeo sends: Romeo receives: Romeo receives: # Disable presences for non-mods Romeo sends: http://jabber.org/protocol/muc#roomconfig moderator Romeo receives: # Romeo probes himself Romeo sends: Romeo receives: # Juliet tries to probe Romeo before joining the room Juliet connects Juliet sends: Juliet receives: # Juliet tries to probe Mercutio (who's not in the MUC) before joining the room Juliet sends: Juliet receives: # Juliet joins the room Juliet sends: Juliet receives: Juliet receives: # Romeo probes Juliet Romeo sends: Romeo receives: # Mercutio tries to probe himself in a MUC before joining Mercutio connects Mercutio sends: Mercutio receives: # Romeo makes Mercutio a member and registers his nickname Romeo sends: Romeo receives: Romeo receives: # Romeo probes Mercutio, even though he's unavailable Romeo sends: Romeo receives: prosody-13.0.1/spec/scansion/PaxHeaders/muc_register.scs0000644000000000000000000000011614773555365020350 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.72371106 prosody-13.0.1/spec/scansion/muc_register.scs0000644000175000017500000003510114773555365022550 0ustar00prosodyprosody00000000000000# MUC: Room registration and reserved nicknames [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password [Client] Rosaline jid: user3@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: # Submit config form Romeo sends: http://jabber.org/protocol/muc#roomconfig Romeo receives: Romeo sends: Romeo receives: Romeo receives: # Juliet connects, and joins the room Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: # Juliet retrieves the registration form Juliet sends: Juliet receives: http://jabber.org/protocol/muc#register Juliet sends: http://jabber.org/protocol/muc#register Juliet Juliet receives: Juliet receives: # Juliet discovers her reserved nick Juliet sends: Juliet receives: # Juliet leaves the room: Juliet sends: Juliet receives: Romeo receives: # Rosaline connect and tries to join the room as Juliet Rosaline connects Rosaline sends: Rosaline receives: # In a heated moment, Juliet unregisters from the room Juliet sends: Juliet receives: # Romeo is notified of Juliet's sad decision Romeo receives: # Rosaline attempts once more to sneak into the room, disguised as Juliet Rosaline sends: Rosaline receives: Rosaline receives: Romeo receives: # On discovering the ruse, Romeo restores Juliet's nick and status within the room Romeo sends: # Rosaline is evicted from the room Romeo receives: This nickname is reserved # An out-of-room affiliation change is received for Juliet Romeo receives: Romeo receives: Rosaline receives: This nickname is reserved # Rosaline, frustrated, attempts to get back into the room... Rosaline sends: # ...but once again, is denied Rosaline receives: # Juliet, however, quietly joins the room with success Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: # Romeo checks whether he has reserved his own nick yet Romeo sends: # But no nick is returned, as he hasn't registered yet! Romeo receives: # Romeo updates his own registration Romeo sends: Romeo receives: http://jabber.org/protocol/muc#register Romeo sends: http://jabber.org/protocol/muc#register Romeo Romeo receives: Romeo receives: Juliet receives: # Romeo discovers his reserved nick Romeo sends: Romeo receives: # To check the status of the room is as expected, Romeo requests the member list Romeo sends: Romeo receives: Juliet sends: Juliet receives: Romeo receives: # Rosaline joins as herself Rosaline sends: Rosaline receives: Rosaline receives: Rosaline receives: Romeo receives: # Rosaline tries to register her own nickname, but unaffiliated # registration is disabled by default Rosaline sends: Rosaline receives: Rosaline sends: http://jabber.org/protocol/muc#register Romeo Rosaline receives: # Romeo reserves her nickname for her Romeo sends: Romeo receives: Romeo receives: Rosaline receives: # Romeo sets their their own nickname via admin query (see #1273) Romeo sends: Romeo receives: Romeo receives: prosody-13.0.1/spec/scansion/PaxHeaders/muc_show_offline.scs0000644000000000000000000000011614773555365021206 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.72371106 prosody-13.0.1/spec/scansion/muc_show_offline.scs0000644000175000017500000003576614773555365023427 0ustar00prosodyprosody00000000000000# MUC: Room registration and presence broadcast of unavailable members [Client] Romeo jid: user@localhost password: password [Client] Juliet jid: user2@localhost password: password [Client] Rosaline jid: user3@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: # Submit config form Romeo sends: http://jabber.org/protocol/muc#roomconfig none participant moderator Romeo receives: Romeo sends: Romeo receives: Romeo receives: # Juliet connects, and joins the room Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: # Juliet retrieves the registration form Juliet sends: Juliet receives: http://jabber.org/protocol/muc#register Juliet sends: http://jabber.org/protocol/muc#register Juliet Juliet receives: Juliet receives: # Juliet discovers her reserved nick Juliet sends: Juliet receives: # Juliet leaves the room: Juliet sends: Juliet receives: Romeo receives: # Rosaline connect and tries to join the room as Juliet Rosaline connects Rosaline sends: Rosaline receives: # In a heated moment, Juliet unregisters from the room Juliet sends: Juliet receives: # Romeo is notified of Juliet's sad decision Romeo receives: # Rosaline attempts once more to sneak into the room, disguised as Juliet Rosaline sends: Rosaline receives: Rosaline receives: Romeo receives: # On discovering the ruse, Romeo restores Juliet's nick and status within the room Romeo sends: # Rosaline is evicted from the room Romeo receives: This nickname is reserved # An out-of-room affiliation change is received for Juliet Romeo receives: Romeo receives: Rosaline receives: This nickname is reserved # Rosaline, frustrated, attempts to get back into the room... Rosaline sends: # ...but once again, is denied Rosaline receives: # Juliet, however, quietly joins the room with success Juliet sends: Juliet receives: Juliet receives: Juliet receives: Romeo receives: # Romeo checks whether he has reserved his own nick yet Romeo sends: # But no nick is returned, as he hasn't registered yet! Romeo receives: # Romeo updates his own registration Romeo sends: Romeo receives: http://jabber.org/protocol/muc#register Romeo sends: http://jabber.org/protocol/muc#register Romeo Romeo receives: Romeo receives: Juliet receives: # Romeo discovers his reserved nick Romeo sends: Romeo receives: # To check the status of the room is as expected, Romeo requests the member list Romeo sends: Romeo receives: Juliet sends: Juliet receives: Romeo receives: # Rosaline joins as herself Rosaline sends: Rosaline receives: Rosaline receives: Rosaline receives: Rosaline receives: Romeo receives: # Rosaline tries to register her own nickname, but unaffiliated # registration is disabled by default Rosaline sends: Rosaline receives: Rosaline sends: http://jabber.org/protocol/muc#register Romeo Rosaline receives: # Romeo reserves her nickname for her Romeo sends: Romeo receives: Romeo receives: Rosaline receives: # Romeo sets their their own nickname via admin query (see #1273) Romeo sends: Romeo receives: Romeo receives: prosody-13.0.1/spec/scansion/PaxHeaders/muc_subject_issue_667.scs0000644000000000000000000000011614773555365021775 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.72371106 prosody-13.0.1/spec/scansion/muc_subject_issue_667.scs0000644000175000017500000001017514773555365024201 0ustar00prosodyprosody00000000000000# #667 MUC message with subject and body SHALL NOT be interpreted as a subject change [Client] Romeo password: password jid: romeo@localhost ----- Romeo connects # and creates a room Romeo sends: Romeo receives: # the default (empty) subject Romeo receives: # this should be treated as a normal message Romeo sends: Greetings Hello everyone Romeo receives: Greetings Hello everyone # this should be treated as a normal message Romeo sends: New thread 498acea5-5894-473f-b4c6-c77319d11c75 Romeo receives: New thread 498acea5-5894-473f-b4c6-c77319d11c75 # Resync Romeo sends: # Presences Romeo receives: Romeo receives: Greetings Hello everyone Romeo receives: New thread 498acea5-5894-473f-b4c6-c77319d11c75 # the still empty subject Romeo receives: # this is a subject change Romeo sends: Something to talk about Romeo receives: Something to talk about # a message without Romeo sends: Lorem ipsum dolor sit amet Romeo receives: Lorem ipsum dolor sit amet # Resync Romeo sends: # Presences Romeo receives: # History # These have delay tags but we ignore those for now Romeo receives: Greetings Hello everyone Romeo receives: New thread 498acea5-5894-473f-b4c6-c77319d11c75 Romeo receives: Lorem ipsum dolor sit amet # Finally, the topic Romeo receives: Something to talk about Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/muc_whois_anyone_member.scs0000644000000000000000000000011614773555365022555 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.72371106 prosody-13.0.1/spec/scansion/muc_whois_anyone_member.scs0000644000175000017500000000653314773555365024764 0ustar00prosodyprosody00000000000000# MUC: Allow members to fetch the affiliation lists in open non-anonymous rooms [Client] Romeo jid: 4e2pm7er@localhost password: password [Client] Juliet jid: qnjm5253@localhost password: password [Client] Random jid: iqizbcus@localhost password: password ----- Romeo connects Juliet connects Random connects # Romeo joins and creates the MUC Romeo sends: Romeo receives: Romeo receives: # and configures it for private chat Romeo sends: http://jabber.org/protocol/muc#roomconfig 1 anyone Romeo receives: Romeo receives: # Juliet is made a member Romeo sends: # Juliet can read affiliations Juliet sends: Juliet receives: Juliet sends: Juliet receives: # Others can't read affiliations Random sends: Random receives: Juliet disconnects Romeo disconnects Random disconnects # recording ended on 2021-07-23T12:09:48Z prosody-13.0.1/spec/scansion/PaxHeaders/pep_itemreply.scs0000644000000000000000000000011614773555365020536 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.72371106 prosody-13.0.1/spec/scansion/pep_itemreply.scs0000644000175000017500000003030414773555365022736 0ustar00prosodyprosody00000000000000# PEP itemreply (publisher) configuration # This tests that itemreply == "publisher" will add the 'publisher' attribute # to notifications. Since this is not the default behaviour, the normal # publish and subscribe test cases cover testing that it is not included # otherwise. [Client] Romeo jid: pep-test-df6zdvkv@localhost password: password [Client] Juliet jid: pep-test-5k90xvps@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: Romeo sends: Romeo receives: Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet sends: Juliet receives: Romeo sends: Romeo sends: Romeo receives: Romeo receives: Juliet receives: Juliet sends: Juliet sends: Juliet receives: Juliet receives: Romeo receives: Romeo sends: Romeo sends: Romeo receives: Juliet receives: Juliet receives: Juliet receives: Juliet sends: Juliet receives: Juliet receives: Romeo receives: Romeo receives: Romeo receives: Juliet sends: Romeo sends: Romeo sends: Romeo sends: Romeo receives: Romeo receives: Romeo receives: Juliet receives: Romeo sends: Romeo receives: Romeo receives: Romeo receives: Romeo sends: Romeo sends: Romeo sends: Juliet receives: Juliet sends: Juliet sends: Beautiful CedarsThe SpinnersNot Quite Folk4http://jabber.org/protocol/pubsub#publish-optionstruepublisher Juliet receives: Romeo receives: Beautiful CedarsThe SpinnersNot Quite Folk4 Romeo sends: Romeo receives: Beautiful CedarsThe SpinnersNot Quite Folk4 Juliet disconnects Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/pep_nickname.scs0000644000000000000000000000011614773555365020311 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.72371106 prosody-13.0.1/spec/scansion/pep_nickname.scs0000644000175000017500000000406714773555365022520 0ustar00prosodyprosody00000000000000# Publishing a nickname in PEP and receiving a notification [Client] Romeo jid: romeo@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo sends: Romeo receives: Romeo receives: Romeo sends: Romeo receives: Romeo sends: Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/pep_publish_subscribe.scs0000644000000000000000000000011614773555365022233 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.72371106 prosody-13.0.1/spec/scansion/pep_publish_subscribe.scs0000644000175000017500000003055714773555365024445 0ustar00prosodyprosody00000000000000# PEP publish, subscribe and publish-options [Client] Romeo jid: pep-test-wjebo4kg@localhost password: password [Client] Juliet jid: pep-test-tqvqu_pv@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo receives: Romeo sends: Romeo receives: Juliet connects Juliet sends: Juliet receives: Juliet receives: Juliet sends: Juliet receives: Romeo sends: Romeo sends: Romeo receives: Romeo receives: Juliet receives: Juliet sends: Juliet sends: Juliet receives: Juliet receives: Romeo receives: Romeo sends: Romeo sends: Romeo receives: Juliet receives: Juliet receives: Juliet receives: Juliet sends: Juliet receives: Juliet receives: Romeo receives: Romeo receives: Romeo receives: Juliet sends: Romeo sends: Romeo sends: Romeo sends: Romeo receives: Romeo receives: Romeo receives: Juliet receives: Romeo sends: Romeo receives: Romeo receives: Romeo receives: Romeo sends: Romeo sends: Romeo sends: Juliet receives: Juliet sends: Juliet sends: Beautiful CedarsThe SpinnersNot Quite Folk4 Juliet receives: Juliet sends: http://jabber.org/protocol/pubsub#publish-optionstruewhitelist Juliet receives: Juliet sends: Romeo receives: Beautiful CedarsThe SpinnersNot Quite Folk4 Romeo sends: Romeo receives: Beautiful CedarsThe SpinnersNot Quite Folk4 Juliet disconnects Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/pep_pubsub_max.scs0000644000000000000000000000011614773555365020671 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.72371106 prosody-13.0.1/spec/scansion/pep_pubsub_max.scs0000644000175000017500000000204014773555365023065 0ustar00prosodyprosody00000000000000# PEP max_items=max [Client] Romeo jid: pep-test-maxitems@localhost password: password ----- Romeo connects Romeo sends: Hello http://jabber.org/protocol/pubsub#publish-options true open max Romeo receives: prosody-13.0.1/spec/scansion/PaxHeaders/presence_preapproval.scs0000644000000000000000000000011614773555365022077 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.72371106 prosody-13.0.1/spec/scansion/presence_preapproval.scs0000644000175000017500000000236714773555365024307 0ustar00prosodyprosody00000000000000# server supports contact subscription pre-approval (RFC 6121 3.4) [Client] Alice jid: preappove-a@localhost password: password [Client] Bob jid: preapprove-b@localhost password: password --------- Alice connects Alice sends: Alice receives: Alice sends: Bob connects Bob sends: Bob receives: Bob sends: Bob receives: Bob sends: Bob receives: Bob receives: Bob disconnects Alice sends: Alice receives: Alice disconnects Bob disconnects prosody-13.0.1/spec/scansion/PaxHeaders/prosody.cfg.lua0000644000000000000000000000011714773555365020107 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.727711077 prosody-13.0.1/spec/scansion/prosody.cfg.lua0000644000175000017500000001170114773555365022306 0ustar00prosodyprosody00000000000000--luacheck: ignore admins = FileLines("admins.txt") network_backend = ENV_PROSODY_NETWORK_BACKEND or "epoll" network_settings = Lua.require"prosody.util.json".decode(ENV_PROSODY_NETWORK_SETTINGS or "{}") modules_enabled = { -- Generally required "roster"; -- Allow users to have a roster. Recommended ;) "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in. --"tls"; -- Add support for secure TLS on c2s/s2s connections --"dialback"; -- s2s dialback support "disco"; -- Service discovery -- Not essential, but recommended "carbons"; -- Keep multiple clients in sync "pep"; -- Enables users to publish their avatar, mood, activity, playing music and more "private"; -- Private XML storage (for room bookmarks, etc.) "blocklist"; -- Allow users to block communications with other users "vcard4"; -- User profiles (stored in PEP) "vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard -- Nice to have "version"; -- Replies to server version requests "uptime"; -- Report how long server has been running "time"; -- Let others know the time here on this server "ping"; -- Replies to XMPP pings with pongs "register"; -- Allow users to register on this server using a client and change passwords "mam"; -- Store messages in an archive and allow users to access it --"csi_simple"; -- Simple Mobile optimizations -- Admin interfaces --"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands --"admin_telnet"; -- Opens telnet console interface on localhost port 5582 -- HTTP modules --"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP" --"websocket"; -- XMPP over WebSockets --"http_files"; -- Serve static files from a directory over HTTP -- Other specific functionality --"limits"; -- Enable bandwidth limiting for XMPP connections --"groups"; -- Shared roster support "server_contact_info"; -- Publish contact information for this service --"announce"; -- Send announcement to all online users --"welcome"; -- Welcome users who register accounts --"watchregistrations"; -- Alert admins of registrations --"motd"; -- Send a message to users when they log in --"legacyauth"; -- Legacy authentication. Only used by some old clients and bots. --"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use "lastactivity"; "external_services"; "tombstones"; "user_account_management"; -- Required for integration testing "debug_reset"; -- Useful for testing --"scansion_record"; -- Records things that happen in scansion test case format } contact_info = { abuse = { "mailto:abuse@localhost", "xmpp:abuse@localhost" }; admin = { "mailto:admin@localhost", "xmpp:admin@localhost" }; feedback = { "http://localhost/feedback.html", "mailto:feedback@localhost", "xmpp:feedback@localhost" }; sales = { "xmpp:sales@localhost" }; security = { "xmpp:security@localhost" }; status = { "gopher://status.localhost" }; support = { "https://localhost/support.html", "xmpp:support@localhost" }; } external_service_host = "default.example" external_service_port = 9876 external_service_secret = "" external_services = { {type = "stun"; transport = "udp"}; {type = "turn"; transport = "udp"; secret = true}; {type = "turn"; transport = "udp"; secret = "foo"}; {type = "ftp"; transport = "tcp"; port = 2121; username = "john"; password = "password"}; {type = "ftp"; transport = "tcp"; host = "ftp.example.com"; port = 21; username = "john"; password = "password"}; } modules_disabled = { "s2s"; } -- TLS is not used during the test, set certificate dir to the config directory -- (spec/scansion) to silence an error from the certificate indexer certificates = "." allow_registration = false c2s_require_encryption = false allow_unencrypted_plain_auth = true authentication = "insecure" insecure_open_authentication = "Yes please, I know what I'm doing!" storage = "memory" mam_smart_enable = true bounce_blocked_messages = true -- For the "sql" backend, you can uncomment *one* of the below to configure: --sql = { driver = "SQLite3", database = "prosody.sqlite" } -- Default. 'database' is the filename. --sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } --sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" } -- Logging configuration -- For advanced logging see https://prosody.im/doc/logging log = {"*console",debug = ENV_PROSODY_LOGFILE} pidfile = "prosody.pid" VirtualHost "localhost" hide_os_type = true -- absence tested for in version.scs Component "conference.localhost" "muc" storage = "memory" admins = { "Admin@localhost" } modules_enabled = { "muc_mam"; } Component "pubsub.localhost" "pubsub" storage = "memory" expose_publisher = true Component "upload.localhost" "http_file_share" http_file_share_size_limit = 10000000 http_file_share_allowed_file_types = { "text/plain", "image/*" } prosody-13.0.1/spec/scansion/PaxHeaders/pubsub_advanced.scs0000644000000000000000000000011714773555365021006 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.727711077 prosody-13.0.1/spec/scansion/pubsub_advanced.scs0000644000175000017500000001043214773555365023205 0ustar00prosodyprosody00000000000000# Pubsub: Node creation, publish, subscribe, affiliations and delete [Client] Balthasar jid: admin@localhost password: password [Client] Romeo jid: romeo@localhost password: password [Client] Juliet jid: juliet@localhost password: password --------- Romeo connects Romeo sends: Romeo receives: Balthasar connects Balthasar sends: Balthasar receives: Balthasar sends: Balthasar receives: Juliet connects Juliet sends: Juliet receives: Juliet sends: Juliet receives: Balthasar sends: Balthasar receives: Balthasar sends: Balthasar receives: Romeo sends: Soliloquy Lorem ipsum dolor sit amet Juliet receives: Soliloquy Lorem ipsum dolor sit amet Romeo receives: Juliet sends: Juliet receives: Balthasar sends: Balthasar receives: Romeo disconnects // vim: syntax=xml: prosody-13.0.1/spec/scansion/PaxHeaders/pubsub_basic.scs0000644000000000000000000000011714773555365020322 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.727711077 prosody-13.0.1/spec/scansion/pubsub_basic.scs0000644000175000017500000000445614773555365022532 0ustar00prosodyprosody00000000000000# Pubsub: Basic support [Client] Romeo jid: admin@localhost password: password // admin@localhost is assumed to have node creation privileges [Client] Juliet jid: juliet@localhost password: password --------- Romeo connects Romeo sends: Romeo receives: Juliet connects -- Juliet sends: -- -- -- -- -- -- -- Juliet receives: -- Juliet sends: Juliet receives: Romeo sends: Soliloquy Lorem ipsum dolor sit amet Romeo receives: Juliet receives: Soliloquy Lorem ipsum dolor sit amet Juliet sends: Juliet receives: Juliet disconnects Romeo sends: Romeo receives: Romeo disconnects // vim: syntax=xml: prosody-13.0.1/spec/scansion/PaxHeaders/pubsub_config.scs0000644000000000000000000000011714773555365020506 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.727711077 prosody-13.0.1/spec/scansion/pubsub_config.scs0000644000175000017500000002050414773555365022706 0ustar00prosodyprosody00000000000000# pubsub#title as name attribute in disco#items # Issue 1226 [Client] Romeo password: password jid: jqpcrbq@localhost ----- Romeo connects Romeo sends: Romeo receives: Romeo sends: Romeo receives: http://jabber.org/protocol/pubsub#node_config 1 1 presence publishers on_sub_and_presence 1 1 headline 1 1 none Romeo sends: http://jabber.org/protocol/pubsub#node_config Nice tunes 1 1 presence publishers never 1 1 headline 1 1 Romeo receives: Romeo sends: Romeo receives: Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/pubsub_createdelete.scs0000644000000000000000000000011714773555365021667 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.727711077 prosody-13.0.1/spec/scansion/pubsub_createdelete.scs0000644000175000017500000000247114773555365024072 0ustar00prosodyprosody00000000000000# Pubsub: Create and delete [Client] Romeo jid: admin@localhost password: password // admin@localhost is assumed to have node creation privileges --------- Romeo connects Romeo sends: Romeo receives: Romeo sends: Romeo receives: Romeo sends: Romeo receives: Romeo sends: Romeo receives: Romeo disconnects // vim: syntax=xml: prosody-13.0.1/spec/scansion/PaxHeaders/pubsub_max_items.scs0000644000000000000000000000011714773555365021227 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.727711077 prosody-13.0.1/spec/scansion/pubsub_max_items.scs0000644000175000017500000002061314773555365023430 0ustar00prosodyprosody00000000000000# Pubsub: Requesting the Most Recent Items (#1608) [Client] Alice jid: admin@localhost password: password --------- Alice connects Alice sends: Alice receives: Alice sends: Alice receives: Alice sends: Alice receives: http://jabber.org/protocol/pubsub#node_config 20 1 open publishers never 1 1 headline 1 1 none Alice sends: foo Alice receives: Alice sends: bar Alice receives: Alice sends: Alice receives: bar Alice sends: Alice receives: foo bar Alice sends: Alice receives: prosody-13.0.1/spec/scansion/PaxHeaders/pubsub_multi_items.scs0000644000000000000000000000011714773555365021574 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.727711077 prosody-13.0.1/spec/scansion/pubsub_multi_items.scs0000644000175000017500000002133214773555365023774 0ustar00prosodyprosody00000000000000# Pubsub: Requesting multiple specific items from a node (#1322) [Client] Alice jid: admin@localhost password: password --------- Alice connects Alice sends: Alice receives: Alice sends: Alice receives: Alice sends: Alice receives: http://jabber.org/protocol/pubsub#node_config 20 1 open publishers never 1 1 headline 1 1 none Alice sends: foo Alice receives: Alice sends: bar Alice receives: Alice sends: Alice receives: foo bar Alice sends: Alice receives: foo bar Alice sends: Alice receives: prosody-13.0.1/spec/scansion/PaxHeaders/pubsub_preconditions.scs0000644000000000000000000000011714773555365022121 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.727711077 prosody-13.0.1/spec/scansion/pubsub_preconditions.scs0000644000175000017500000002246714773555365024333 0ustar00prosodyprosody00000000000000# Pubsub preconditions are enforced [Client] Romeo password: password jid: jqpcrbq2@localhost ----- Romeo connects Romeo sends: Romeo receives: Romeo sends: Romeo receives: http://jabber.org/protocol/pubsub#node_config 1 1 presence publishers on_sub_and_presence 1 1 headline 1 1 none Romeo sends: http://jabber.org/protocol/pubsub#node_config Nice tunes 1 1 presence publishers never 1 1 headline 1 1 none Romeo receives: Romeo sends: Romeo receives: Romeo sends: http://jabber.org/protocol/pubsub#publish-options whitelist Romeo receives: Field does not match: access_model Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/pubsub_resend_on_sub.scs0000644000000000000000000000011714773555365022066 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.731711093 prosody-13.0.1/spec/scansion/pubsub_resend_on_sub.scs0000644000175000017500000000727314773555365024276 0ustar00prosodyprosody00000000000000# Pubsub: Send last item on subscribe #1436 [Client] Romeo jid: admin@localhost password: password // admin@localhost is assumed to have node creation privileges [Client] Juliet jid: juliet@localhost password: password --------- Romeo connects Romeo sends: Romeo receives: Romeo sends: http://jabber.org/protocol/pubsub#node_config never Romeo receives: Romeo sends: Soliloquy Lorem ipsum dolor sit amet Romeo receives: Juliet connects Juliet sends: Juliet receives: Juliet sends: Juliet receives: Romeo sends: http://jabber.org/protocol/pubsub#node_config on_sub Romeo receives: Juliet sends: Juliet receives: Juliet receives: Soliloquy Lorem ipsum dolor sit amet Juliet sends: Juliet receives: Juliet disconnects Romeo sends: Romeo receives: Romeo disconnects // vim: syntax=xml: prosody-13.0.1/spec/scansion/PaxHeaders/server_contact_info.scs0000644000000000000000000000011714773555365021715 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.731711093 prosody-13.0.1/spec/scansion/server_contact_info.scs0000644000175000017500000000475314773555365024125 0ustar00prosodyprosody00000000000000# XEP-0157: Contact Addresses for XMPP Services # mod_server_contact_info [Client] Romeo jid: romeo@localhost password: password ----- Romeo connects Romeo sends: # Ignore other disco#info features, identities etc Romeo receives: http://jabber.org/network/serverinfo mailto:abuse@localhost xmpp:abuse@localhost mailto:admin@localhost xmpp:admin@localhost http://localhost/feedback.html mailto:feedback@localhost xmpp:feedback@localhost xmpp:sales@localhost xmpp:security@localhost gopher://status.localhost https://localhost/support.html xmpp:support@localhost Romeo sends: http://jabber.org/network/serverinfo xmpp:admin@localhost Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/tombstones.scs0000644000000000000000000000011714773555365020056 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.731711093 prosody-13.0.1/spec/scansion/tombstones.scs0000644000175000017500000000130314773555365022252 0ustar00prosodyprosody00000000000000# Tombstones [Client] Romeo jid: romeo@localhost password: password [Client] Juliet jid: juliet-tombstones@localhost password: password --------- Romeo connects Juliet connects Juliet sends: # Scansion gets disconnected right after this with a stream error makes # scansion itself abort, so we preemptively disconnect to avoid that # Juliet receives: # Juliet disconnects Romeo sends: Romeo receives: Romeo receives: prosody-13.0.1/spec/scansion/PaxHeaders/uptime.scs0000644000000000000000000000011714773555365017164 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.731711093 prosody-13.0.1/spec/scansion/uptime.scs0000644000175000017500000000055014773555365021363 0ustar00prosodyprosody00000000000000# XEP-0012: Last Activity / mod_uptime [Client] Romeo jid: romeo@localhost password: password ----- Romeo connects Romeo sends: Romeo receives: Romeo disconnects prosody-13.0.1/spec/scansion/PaxHeaders/vcard_temp.scs0000644000000000000000000000011714773555365020005 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.731711093 prosody-13.0.1/spec/scansion/vcard_temp.scs0000644000175000017500000000300714773555365022204 0ustar00prosodyprosody00000000000000# XEP-0054 vCard-temp writable and readable by anyone # mod_scansion_record on host 'localhost' recording started 2018-10-20T15:00:12Z [Client] Romeo jid: romeo@localhost password: password [Client] Juliet jid: juliet@localhost password: password ----- Romeo connects # Romeo sets his vCard # FN and N are required by the schema and mod_vcard_legacy will always inject them Romeo sends: Romeo Montague Montague Romeo Romeo receives: Romeo sends: Romeo receives: Romeo Montague Montague Romeo Juliet connects Juliet sends: # Juliet can see Romeo's vCard since it's public Juliet receives: Romeo Montague Montague Romeo Juliet disconnects Romeo disconnects # recording ended on 2018-10-20T15:02:14Z prosody-13.0.1/spec/scansion/PaxHeaders/version.scs0000644000000000000000000000011714773555365017346 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.731711093 prosody-13.0.1/spec/scansion/version.scs0000644000175000017500000000111014773555365021536 0ustar00prosodyprosody00000000000000# XEP-0092: Software Version / mod_version [Client] Romeo password: password jid: romeo@localhost/dfaZpuxV ----- Romeo connects Romeo sends: # Version string would vary so we can't do an exact match atm # Inclusion of is disabled in the config, it should be absent Romeo receives: Prosody Romeo disconnects prosody-13.0.1/spec/PaxHeaders/tls0000644000000000000000000000013114773555365014053 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.731711093 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/0000755000175000017500000000000014773555365016333 5ustar00prosodyprosody00000000000000prosody-13.0.1/spec/tls/PaxHeaders/README0000644000000000000000000000011714773555365015014 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.731711093 prosody-13.0.1/spec/tls/README0000644000175000017500000000077014773555365017217 0ustar00prosodyprosody00000000000000These tests check that SSL/TLS configuration is working as expected. Just run ./run.sh in this directory (or from the top level, `make integration-test-tls`. Known issues: - The tests do not thorougly clean up after themselves (certs, logs, etc.). This is partly intentional, so they can be inspected in case of failures. - Certs are regenerated every time. Could be smarter about this. But it also helps to guard against incorrect Prosody instances running and hogging the ports, etc. prosody-13.0.1/spec/tls/PaxHeaders/config10000644000000000000000000000013114773555365015401 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.731711093 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config1/0000755000175000017500000000000014773555365017661 5ustar00prosodyprosody00000000000000prosody-13.0.1/spec/tls/config1/PaxHeaders/assert.sh0000644000000000000000000000011714773555365017317 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config1/assert.sh0000755000175000017500000000032414773555365021520 0ustar00prosodyprosody00000000000000#!/bin/bash #set -x . ../lib.sh expect_cert "certs/example.com.crt" "localhost:5222" "example.com" "xmpp" expect_cert "certs/share.example.com.crt" "localhost:5281" "share.example.com" "tls" exit "$failures" prosody-13.0.1/spec/tls/config1/PaxHeaders/prepare.sh0000644000000000000000000000011714773555365017454 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config1/prepare.sh0000755000175000017500000000041414773555365021655 0ustar00prosodyprosody00000000000000#!/bin/bash certs="./certs" for domain in {,share.}example.com; do openssl req -x509 \ -newkey rsa:4096 \ -keyout "${certs}/${domain}.key" \ -out "${certs}/${domain}.crt" \ -sha256 \ -days 365 \ -nodes \ -subj "/CN=${domain}" 2>/dev/null; done prosody-13.0.1/spec/tls/config1/PaxHeaders/prosody.cfg.lua0000644000000000000000000000011714773555365020422 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config1/prosody.cfg.lua0000644000175000017500000000017614773555365022625 0ustar00prosodyprosody00000000000000Include "prosody-default.cfg.lua" VirtualHost "example.com" enabled = true Component "share.example.com" "http_file_share" prosody-13.0.1/spec/tls/PaxHeaders/config20000644000000000000000000000013114773555365015402 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.735711109 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config2/0000755000175000017500000000000014773555365017662 5ustar00prosodyprosody00000000000000prosody-13.0.1/spec/tls/config2/PaxHeaders/assert.sh0000644000000000000000000000011714773555365017320 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config2/assert.sh0000755000175000017500000000032214773555365021517 0ustar00prosodyprosody00000000000000#!/bin/bash #set -x . ../lib.sh expect_cert "certs/xmpp.example.com.crt" "localhost:5281" "xmpp.example.com" "tls" expect_cert "certs/example.com.crt" "localhost:5222" "example.com" "xmpp" exit "$failures" prosody-13.0.1/spec/tls/config2/PaxHeaders/prepare.sh0000644000000000000000000000011714773555365017455 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config2/prepare.sh0000755000175000017500000000041314773555365021655 0ustar00prosodyprosody00000000000000#!/bin/bash certs="./certs" for domain in {,xmpp.}example.com; do openssl req -x509 \ -newkey rsa:4096 \ -keyout "${certs}/${domain}.key" \ -out "${certs}/${domain}.crt" \ -sha256 \ -days 365 \ -nodes \ -subj "/CN=${domain}" 2>/dev/null; done prosody-13.0.1/spec/tls/config2/PaxHeaders/prosody.cfg.lua0000644000000000000000000000011714773555365020423 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config2/prosody.cfg.lua0000644000175000017500000000021314773555365022616 0ustar00prosodyprosody00000000000000Include "prosody-default.cfg.lua" VirtualHost "example.com" enabled = true modules_enabled = { "http" } http_host = "xmpp.example.com" prosody-13.0.1/spec/tls/PaxHeaders/config30000644000000000000000000000013114773555365015403 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.735711109 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config3/0000755000175000017500000000000014773555365017663 5ustar00prosodyprosody00000000000000prosody-13.0.1/spec/tls/config3/PaxHeaders/assert.sh0000644000000000000000000000011714773555365017321 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config3/assert.sh0000755000175000017500000000223114773555365021521 0ustar00prosodyprosody00000000000000#!/bin/bash #set -x . ../lib.sh expect_cert "certs/xmpp.example.com.crt" "localhost:5281" "xmpp.example.com" "tls" expect_cert "certs/example.com.crt" "localhost:5222" "example.com" "xmpp" expect_cert "certs/example.com.crt" "localhost:5223" "example.com" "xmpps" # Weirdly configured host, just to test manual override behaviour expect_cert "certs/example.com.crt" "localhost:5222" "example.net" "xmpp" expect_cert "certs/example.com.crt" "localhost:5222" "example.net" "xmpp" expect_cert "certs/example.com.crt" "localhost:5223" "example.net" "tls" expect_cert "certs/example.com.crt" "localhost:5281" "example.net" "tls" # Three domains using a single cert with SANs expect_cert "certs/example.org.crt" "localhost:5222" "example.org" "xmpp" expect_cert "certs/example.org.crt" "localhost:5223" "example.org" "xmpps" expect_cert "certs/example.org.crt" "localhost:5269" "example.org" "xmpp-server" expect_cert "certs/example.org.crt" "localhost:5269" "share.example.org" "xmpp-server" expect_cert "certs/example.org.crt" "localhost:5269" "groups.example.org" "xmpp-server" expect_cert "certs/example.org.crt" "localhost:5281" "share.example.org" "tls" exit "$failures" prosody-13.0.1/spec/tls/config3/PaxHeaders/prepare.sh0000644000000000000000000000011714773555365017456 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config3/prepare.sh0000755000175000017500000000114714773555365021663 0ustar00prosodyprosody00000000000000#!/bin/bash certs="./certs" for domain in {,xmpp.}example.com example.net; do openssl req -x509 \ -newkey rsa:4096 \ -keyout "${certs}/${domain}.key" \ -out "${certs}/${domain}.crt" \ -sha256 \ -days 365 \ -nodes \ -quiet \ -subj "/CN=${domain}" 2>/dev/null; done for domain in example.org; do openssl req -x509 \ -newkey rsa:4096 \ -keyout "${certs}/${domain}.key" \ -out "${certs}/${domain}.crt" \ -sha256 \ -days 365 \ -nodes \ -subj "/CN=${domain}" \ -addext "subjectAltName = DNS:${domain}, DNS:groups.${domain}, DNS:share.${domain}" \ 2>/dev/null; done prosody-13.0.1/spec/tls/config3/PaxHeaders/prosody.cfg.lua0000644000000000000000000000011714773555365020424 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/config3/prosody.cfg.lua0000644000175000017500000000112014773555365022615 0ustar00prosodyprosody00000000000000Include "prosody-default.cfg.lua" c2s_direct_tls_ports = { 5223 } VirtualHost "example.com" enabled = true modules_enabled = { "http" } http_host = "xmpp.example.com" VirtualHost "example.net" ssl = { certificate = "certs/example.com.crt"; key = "certs/example.com.key"; } https_ssl = { certificate = "certs/example.com.crt"; key = "certs/example.com.key"; } c2s_direct_tls_ssl = { certificate = "certs/example.com.crt"; key = "certs/example.com.key"; } VirtualHost "example.org" Component "share.example.org" "http_file_share" Component "groups.example.org" "muc" prosody-13.0.1/spec/tls/PaxHeaders/lib.sh0000644000000000000000000000011714773555365015236 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/tls/lib.sh0000644000175000017500000000217714773555365017444 0ustar00prosodyprosody00000000000000#!/bin/bash test_name="$(basename "$PWD")" export failures=0 get_net_cert () { address="${1?}" sni="${2?}" proto="${3?}" local flags=() case "$proto" in "xmpp") flags=(-starttls xmpp -name "$sni");; "xmpps") flags=(-alpn xmpp-client);; "xmpp-server") flags=(-starttls xmpp-server -name "$sni");; "xmpps-server") flags=(-alpn xmpp-server);; "tls") ;; *) printf "EE: Unknown protocol: %s\n" "$proto" >&2; exit 1;; esac openssl s_client -connect "$address" -servername "$sni" "${flags[@]}" 2>/dev/null > ./prosody-default.cfg.lua ln -s ../../../plugins plugins mkdir -p certs data ./prepare.sh ../../../prosody -D sleep 1; echo "# Testing $config" ./assert.sh status=$? ../../../prosodyctl stop rm plugins #prosody-default.cfg.lua popd if [[ "$status" != "0" ]]; then echo -n "NOT "; any_failed=1 fi echo "OK: $config"; done if [[ "$any_failed" != "0" ]]; then echo "NOT OK: One or more TLS tests failed"; exit 1; fi echo "OK: All TLS tests passed"; exit 0; prosody-13.0.1/spec/PaxHeaders/utf8_sequences.txt0000644000000000000000000000011714773555365017034 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/utf8_sequences.txt0000644000175000017500000000624214773555365021237 0ustar00prosodyprosody00000000000000Should pass: 41 42 43 # Simple ASCII - abc Should pass: 41 42 c3 87 # "ABÇ" Should pass: 41 42 e1 b8 88 # "ABḈ" Should pass: 41 42 f0 9d 9c 8d # "AB𝜍" Should pass: F4 8F BF BF # Last valid sequence (U+10FFFF) Should fail: F4 90 80 80 # First invalid sequence (U+110000) Should fail: 80 81 82 83 # Invalid sequence (invalid start byte) Should fail: C2 C3 # Invalid sequence (invalid continuation byte) Should fail: C0 43 # Overlong sequence Should fail: F5 80 80 80 # U+140000 (out of range) Should fail: ED A0 80 # U+D800 (forbidden by RFC 3629) Should fail: ED BF BF # U+DFFF (forbidden by RFC 3629) Should pass: ED 9F BF # U+D7FF (U+D800 minus 1: allowed) Should pass: EE 80 80 # U+E000 (U+D7FF plus 1: allowed) Should fail: C0 # Invalid start byte Should fail: C1 # Invalid start byte Should fail: C2 # Incomplete sequence Should fail: F8 88 80 80 80 # 6-byte sequence Should pass: 7F # Last valid 1-byte sequence (U+00007F) Should pass: DF BF # Last valid 2-byte sequence (U+0007FF) Should pass: EF BF BF # Last valid 3-byte sequence (U+00FFFF) Should pass: 00 # First valid 1-byte sequence (U+000000) Should pass: C2 80 # First valid 2-byte sequence (U+000080) Should pass: E0 A0 80 # First valid 3-byte sequence (U+000800) Should pass: F0 90 80 80 # First valid 4-byte sequence (U+000800) Should fail: F8 88 80 80 80 # First 5-byte sequence - invalid per RFC 3629 Should fail: FC 84 80 80 80 80 # First 6-byte sequence - invalid per RFC 3629 Should pass: EF BF BD # U+00FFFD (replacement character) Should fail: 80 # First continuation byte Should fail: BF # Last continuation byte Should fail: 80 BF # 2 continuation bytes Should fail: 80 BF 80 # 3 continuation bytes Should fail: 80 BF 80 BF # 4 continuation bytes Should fail: 80 BF 80 BF 80 # 5 continuation bytes Should fail: 80 BF 80 BF 80 BF # 6 continuation bytes Should fail: 80 BF 80 BF 80 BF 80 # 7 continuation bytes Should fail: FE # Impossible byte Should fail: FF # Impossible byte Should fail: FE FE FF FF # Impossible bytes Should fail: C0 AF # Overlong "/" Should fail: E0 80 AF # Overlong "/" Should fail: F0 80 80 AF # Overlong "/" Should fail: F8 80 80 80 AF # Overlong "/" Should fail: FC 80 80 80 80 AF # Overlong "/" Should fail: C0 80 AF # Overlong "/" (invalid) Should fail: C1 BF # Overlong Should fail: E0 9F BF # Overlong Should fail: F0 8F BF BF # Overlong Should fail: F8 87 BF BF BF # Overlong Should fail: FC 83 BF BF BF BF # Overlong Should pass: EF BF BE # U+FFFE (invalid unicode, valid UTF-8) Should pass: EF BF BF # U+FFFF (invalid unicode, valid UTF-8) prosody-13.0.1/spec/PaxHeaders/util_argparse_spec.lua0000644000000000000000000000011714773555365017710 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/util_argparse_spec.lua0000644000175000017500000000662214773555365022115 0ustar00prosodyprosody00000000000000describe("parse", function() local parse setup(function() parse = require"util.argparse".parse; end); it("works", function() -- basic smoke test local opts = parse({ "--help" }); assert.same({ help = true }, opts); end); it("returns if no args", function() assert.same({}, parse({})); end); it("supports boolean flags", function() local opts, err = parse({ "--foo"; "--no-bar" }); assert.falsy(err); assert.same({ foo = true; bar = false }, opts); end); it("consumes input until the first argument", function() local arg = { "--foo"; "bar"; "--baz" }; local opts, err = parse(arg); assert.falsy(err); assert.same({ foo = true, "bar", "--baz" }, opts); assert.same({ "bar"; "--baz" }, arg); end); it("allows continuation beyond first positional argument", function() local arg = { "--foo"; "bar"; "--baz" }; local opts, err = parse(arg, { stop_on_positional = false }); assert.falsy(err); assert.same({ foo = true, baz = true, "bar" }, opts); -- All input should have been consumed: assert.same({ }, arg); end); it("expands short options", function() do local opts, err = parse({ "--foo"; "-b" }, { short_params = { b = "bar" } }); assert.falsy(err); assert.same({ foo = true; bar = true }, opts); end do -- Same test with strict mode enabled and all parameters declared local opts, err = parse({ "--foo"; "-b" }, { kv_params = { foo = true, bar = true }; short_params = { b = "bar" }, strict = true }); assert.falsy(err); assert.same({ foo = true; bar = true }, opts); end end); it("supports value arguments", function() local opts, err = parse({ "--foo"; "bar"; "--baz=moo" }, { value_params = { foo = true; bar = true } }); assert.falsy(err); assert.same({ foo = "bar"; baz = "moo" }, opts); end); it("supports value arguments in strict mode", function() local opts, err = parse({ "--foo"; "bar"; "--baz=moo" }, { strict = true, value_params = { foo = true; baz = true } }); assert.falsy(err); assert.same({ foo = "bar"; baz = "moo" }, opts); end); it("demands values for value params", function() local opts, err, where = parse({ "--foo" }, { value_params = { foo = true } }); assert.falsy(opts); assert.equal("missing-value", err); assert.equal("--foo", where); end); it("reports where the problem is", function() local opts, err, where = parse({ "-h" }); assert.falsy(opts); assert.equal("param-not-found", err); assert.equal("-h", where, "returned where"); end); it("supports array arguments", function () do local opts, err = parse({ "--item"; "foo"; "--item"; "bar" }, { array_params = { item = true } }); assert.falsy(err); assert.same({"foo","bar"}, opts.item); end do -- Same test with strict mode enabled local opts, err = parse({ "--item"; "foo"; "--item"; "bar" }, { array_params = { item = true }, strict = true }); assert.falsy(err); assert.same({"foo","bar"}, opts.item); end end) it("rejects unknown parameters in strict mode", function () local opts, err, err2 = parse({ "--item"; "foo"; "--item"; "bar", "--foobar" }, { array_params = { item = true }, strict = true }); assert.falsy(opts); assert.same("param-not-found", err); assert.same("--foobar", err2); end); it("accepts known kv parameters in strict mode", function () local opts, err = parse({ "--item=foo" }, { kv_params = { item = true }, strict = true }); assert.falsy(err); assert.same("foo", opts.item); end); end); prosody-13.0.1/spec/PaxHeaders/util_array_spec.lua0000644000000000000000000000011714773555365017222 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.735711109 prosody-13.0.1/spec/util_array_spec.lua0000644000175000017500000001075114773555365021425 0ustar00prosodyprosody00000000000000local array = require "util.array"; describe("util.array", function () describe("creation", function () describe("new", function () it("works", function () local a = array.new({"a", "b", "c"}); assert.same({"a", "b", "c"}, a); end); end); describe("from table", function () it("works", function () local a = array({"a", "b", "c"}); assert.same({"a", "b", "c"}, a); end); end); describe("from iterator", function () it("works", function () -- collects the first value, ie the keys local a = array(ipairs({true, true, true})); assert.same({1, 2, 3}, a); end); end); describe("collect", function () it("works", function () -- collects the first value, ie the keys local a = array.collect(ipairs({true, true, true})); assert.same({1, 2, 3}, a); end); end); end); describe("metatable", function () describe("operator", function () describe("addition", function () it("works", function () local a = array({ "a", "b" }); local b = array({ "c", "d" }); assert.same({"a", "b", "c", "d"}, a + b); end); end); describe("equality", function () it("works", function () local a1 = array({ "a", "b" }); local a2 = array({ "a", "b" }); local b = array({ "c", "d" }); assert.truthy(a1 == a2); assert.falsy(a1 == b); assert.falsy(a1 == { "a", "b" }, "Behavior of metatables changed in Lua 5.3"); end); end); describe("division", function () it("works", function () local a = array({ "a", "b", "c" }); local b = a / function (i) if i ~= "b" then return i .. "x" end end; assert.same({ "ax", "cx" }, b); end); end); end); end); describe("methods", function () describe("map", function () it("works", function () local a = array({ "a", "b", "c" }); local b = a:map(string.upper); assert.same({ "A", "B", "C" }, b); end); end); describe("filter", function () it("works", function () local a = array({ "a", "b", "c" }); a:filter(function (i) return i ~= "b" end); assert.same({ "a", "c" }, a); end); end); describe("sort", function () it("works", function () local a = array({ 5, 4, 3, 1, 2, }); a:sort(); assert.same({ 1, 2, 3, 4, 5, }, a); end); end); describe("unique", function () it("works", function () local a = array({ "a", "b", "c", "c", "a", "b" }); a:unique(); assert.same({ "a", "b", "c" }, a); end); end); describe("pluck", function () it("works", function () local a = array({ { a = 1, b = -1 }, { a = 2, b = -2 }, }); a:pluck("a"); assert.same({ 1, 2 }, a); end); end); describe("reverse", function () it("works", function () local a = array({ "a", "b", "c" }); a:reverse(); assert.same({ "c", "b", "a" }, a); end); end); -- TODO :shuffle describe("append", function () it("works", function () local a = array({ "a", "b", "c" }); a:append(array({ "d", "e", })); assert.same({ "a", "b", "c", "d", "e" }, a); end); end); describe("push", function () it("works", function () local a = array({ "a", "b", "c" }); a:push("d"):push("e"); assert.same({ "a", "b", "c", "d", "e" }, a); end); end); describe("pop", function () it("works", function () local a = array({ "a", "b", "c" }); assert.equal("c", a:pop()); assert.same({ "a", "b", }, a); end); end); describe("concat", function () it("works", function () local a = array({ "a", "b", "c" }); assert.equal("a,b,c", a:concat(",")); end); end); describe("length", function () it("works", function () local a = array({ "a", "b", "c" }); assert.equal(3, a:length()); end); end); describe("slice", function () it("works", function () local a = array({ "a", "b", "c" }); assert.equal(array.slice(a, 1, 2), array{ "a", "b" }); assert.equal(array.slice(a, 1, 3), array{ "a", "b", "c" }); assert.equal(array.slice(a, 2, 3), array{ "b", "c" }); assert.equal(array.slice(a, 2), array{ "b", "c" }); assert.equal(array.slice(a, -4), array{ "a", "b", "c" }); assert.equal(array.slice(a, -3), array{ "a", "b", "c" }); assert.equal(array.slice(a, -2), array{ "b", "c" }); assert.equal(array.slice(a, -1), array{ "c" }); end); it("can mutate", function () local a = array({ "a", "b", "c" }); assert.equal(a:slice(-1), array{"c"}); assert.equal(a, array{"c"}); end); end); end); -- TODO The various array.foo(array ina, array outa) functions end); prosody-13.0.1/spec/PaxHeaders/util_async_spec.lua0000644000000000000000000000011714773555365017221 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.739711124 prosody-13.0.1/spec/util_async_spec.lua0000644000175000017500000004424114773555365021425 0ustar00prosodyprosody00000000000000local async = require "util.async"; local match = require "luassert.match"; describe("util.async", function() local debug = false; local print = print; if debug then require "util.logger".add_simple_sink(print); else print = function () end end local function mock_watchers(event_log) local function generic_logging_watcher(name) return function (...) table.insert(event_log, { name = name, n = select("#", ...)-1, select(2, ...) }); end; end; return setmetatable(mock{ ready = generic_logging_watcher("ready"); waiting = generic_logging_watcher("waiting"); error = generic_logging_watcher("error"); }, { __index = function (_, event) -- Unexpected watcher called assert(false, "unexpected watcher called: "..event); end; }) end local function new(func) local event_log = {}; local spy_func = spy.new(func); return async.runner(spy_func, mock_watchers(event_log)), spy_func, event_log; end describe("#runner", function() it("should work", function() local r = new(function (item) assert(type(item) == "number") end); r:run(1); r:run(2); end); it("should be ready after creation", function () local r = new(function () end); assert.equal(r.state, "ready"); end); it("should do nothing if the queue is empty", function () local did_run; local r = new(function () did_run = true end); r:run(); assert.equal(r.state, "ready"); assert.is_nil(did_run); r:run("hello"); assert.is_true(did_run); end); it("should support queuing work items without running", function () local did_run; local r = new(function () did_run = true end); r:enqueue("hello"); assert.equal(r.state, "ready"); assert.is_nil(did_run); r:run(); assert.is_true(did_run); end); it("should support queuing multiple work items", function () local last_item; local r, s = new(function (item) last_item = item; end); r:enqueue("hello"); r:enqueue("there"); r:enqueue("world"); assert.equal(r.state, "ready"); r:run(); assert.equal(r.state, "ready"); assert.spy(s).was.called(3); assert.equal(last_item, "world"); end); it("should support all simple data types", function () local last_item; local r, s = new(function (item) last_item = item; end); local values = { {}, 123, "hello", true, false }; for i = 1, #values do r:enqueue(values[i]); end assert.equal(r.state, "ready"); r:run(); assert.equal(r.state, "ready"); assert.spy(s).was.called(#values); for i = 1, #values do assert.spy(s).was.called_with(values[i]); end assert.equal(last_item, values[#values]); end); it("should work with no parameters", function () local item = "fail"; local r = async.runner(); local f = spy.new(function () item = "success"; end); r:run(f); assert.spy(f).was.called(); assert.equal(item, "success"); end); it("supports a default error handler", function () local item = "fail"; local r = async.runner(); local f = spy.new(function () error("test error"); end); assert.error_matches(function () r:run(f); end, "test error"); assert.spy(f).was.called(); assert.equal(item, "fail"); end); describe("#errors", function () describe("should notify", function () local last_processed_item, last_error; local r; r = async.runner(function (item) if item == "error" then error({ e = "test error" }); end last_processed_item = item; end, mock{ ready = function () end; waiting = function () end; error = function (runner, err) assert.equal(r, runner); last_error = err; end; }); -- Simple item, no error r:run("hello"); assert.equal(r.state, "ready"); assert.equal(last_processed_item, "hello"); assert.spy(r.watchers.ready).was_not.called(); assert.spy(r.watchers.error).was_not.called(); -- Trigger an error inside the runner assert.equal(last_error, nil); r:run("error"); test("the correct watcher functions", function () -- Only the error watcher should have been called assert.spy(r.watchers.ready).was_not.called(); assert.spy(r.watchers.waiting).was_not.called(); assert.spy(r.watchers.error).was.called(1); end); test("with the correct error", function () -- The error watcher state should be correct, to -- demonstrate the error was passed correctly assert.is_table(last_error); assert.equal(last_error.e, "test error"); last_error = nil; end); assert.equal(r.state, "ready"); assert.equal(last_processed_item, "hello"); end); do local last_processed_item, last_error; local r; local wait, done; r = async.runner(function (item) if item == "error" then error({ e = "test error" }); elseif item == "wait" then wait, done = async.waiter(); wait(); error({ e = "post wait error" }); end last_processed_item = item; end, mock({ ready = function () end; waiting = function () end; error = function (runner, err) assert.equal(r, runner); last_error = err; end; })); randomize(false); --luacheck: ignore 113/randomize it("should not be fatal to the runner", function () r:run("world"); assert.equal(r.state, "ready"); assert.spy(r.watchers.ready).was_not.called(); assert.equal(last_processed_item, "world"); end); it("should work despite a #waiter", function () -- This test covers an important case where a runner -- throws an error while being executed outside of the -- main loop. This happens when it was blocked ('waiting'), -- and then released (via a call to done()). last_error = nil; r:run("wait"); assert.equal(r.state, "waiting"); assert.spy(r.watchers.waiting).was.called(1); done(); -- At this point an error happens (state goes error->ready) assert.equal(r.state, "ready"); assert.spy(r.watchers.error).was.called(1); assert.spy(r.watchers.ready).was.called(1); assert.is_table(last_error); assert.equal(last_error.e, "post wait error"); last_error = nil; r:run("hello again"); assert.spy(r.watchers.ready).was.called(1); assert.spy(r.watchers.waiting).was.called(1); assert.spy(r.watchers.error).was.called(1); assert.equal(r.state, "ready"); assert.equal(last_processed_item, "hello again"); end); end it("should continue to process work items", function () local last_item; local runner, runner_func = new(function (item) if item == "error" then error("test error"); end last_item = item; end); runner:enqueue("one"); runner:enqueue("error"); runner:enqueue("two"); runner:run(); assert.equal(runner.state, "ready"); assert.spy(runner_func).was.called(3); assert.spy(runner.watchers.error).was.called(1); assert.spy(runner.watchers.ready).was.called(0); assert.spy(runner.watchers.waiting).was.called(0); assert.equal(last_item, "two"); end); it("should continue to process work items during resume", function () local wait, done, last_item; local runner, runner_func = new(function (item) if item == "wait-error" then wait, done = async.waiter(); wait(); error("test error"); end last_item = item; end); runner:enqueue("one"); runner:enqueue("wait-error"); runner:enqueue("two"); runner:run(); done(); assert.equal(runner.state, "ready"); assert.spy(runner_func).was.called(3); assert.spy(runner.watchers.error).was.called(1); assert.spy(runner.watchers.waiting).was.called(1); assert.spy(runner.watchers.ready).was.called(1); assert.equal(last_item, "two"); end); end); end); describe("#waiter", function() it("should error outside of async context", function () assert.has_error(function () async.waiter(); end); end); it("should work", function () local wait, done; local r = new(function (item) assert(type(item) == "number") if item == 3 then wait, done = async.waiter(); wait(); end end); r:run(1); assert(r.state == "ready"); r:run(2); assert(r.state == "ready"); r:run(3); assert(r.state == "waiting"); done(); assert(r.state == "ready"); --for k, v in ipairs(l) do print(k,v) end end); it("should work", function () -------------------- local wait, done; local last_item = 0; local r = new(function (item) assert(type(item) == "number") assert(item == last_item + 1); last_item = item; if item == 3 then wait, done = async.waiter(); wait(); end end); r:run(1); assert(r.state == "ready"); r:run(2); assert(r.state == "ready"); r:run(3); assert(r.state == "waiting"); r:run(4); assert(r.state == "waiting"); done(); assert(r.state == "ready"); --for k, v in ipairs(l) do print(k,v) end end); it("should work", function () -------------------- local wait, done; local last_item = 0; local r = new(function (item) assert(type(item) == "number") assert((item == last_item + 1) or item == 3); last_item = item; if item == 3 then wait, done = async.waiter(); wait(); end end); r:run(1); assert(r.state == "ready"); r:run(2); assert(r.state == "ready"); r:run(3); assert(r.state == "waiting"); r:run(3); assert(r.state == "waiting"); r:run(3); assert(r.state == "waiting"); r:run(4); assert(r.state == "waiting"); for i = 1, 3 do done(); if i < 3 then assert(r.state == "waiting"); end end assert(r.state == "ready"); --for k, v in ipairs(l) do print(k,v) end end); it("should work", function () -------------------- local wait, done; local last_item = 0; local r = new(function (item) assert(type(item) == "number") assert((item == last_item + 1) or item == 3); last_item = item; if item == 3 then wait, done = async.waiter(); wait(); end end); r:run(1); assert(r.state == "ready"); r:run(2); assert(r.state == "ready"); r:run(3); assert(r.state == "waiting"); r:run(3); assert(r.state == "waiting"); for i = 1, 2 do done(); if i < 2 then assert(r.state == "waiting"); end end assert(r.state == "ready"); r:run(4); assert(r.state == "ready"); assert(r.state == "ready"); --for k, v in ipairs(l) do print(k,v) end end); it("should work with multiple runners in parallel", function () -- Now with multiple runners -------------------- local wait1, done1; local last_item1 = 0; local r1 = new(function (item) assert(type(item) == "number") assert((item == last_item1 + 1) or item == 3); last_item1 = item; if item == 3 then wait1, done1 = async.waiter(); wait1(); end end, "r1"); local wait2, done2; local last_item2 = 0; local r2 = new(function (item) assert(type(item) == "number") assert((item == last_item2 + 1) or item == 3); last_item2 = item; if item == 3 then wait2, done2 = async.waiter(); wait2(); end end, "r2"); r1:run(1); assert(r1.state == "ready"); r1:run(2); assert(r1.state == "ready"); r1:run(3); assert(r1.state == "waiting"); r1:run(3); assert(r1.state == "waiting"); r2:run(1); assert(r1.state == "waiting"); assert(r2.state == "ready"); r2:run(2); assert(r1.state == "waiting"); assert(r2.state == "ready"); r2:run(3); assert(r1.state == "waiting"); assert(r2.state == "waiting"); done2(); r2:run(3); assert(r1.state == "waiting"); assert(r2.state == "waiting"); done2(); r2:run(4); assert(r1.state == "waiting"); assert(r2.state == "ready"); for i = 1, 2 do done1(); if i < 2 then assert(r1.state == "waiting"); end end assert(r1.state == "ready"); r1:run(4); assert(r1.state == "ready"); assert(r1.state == "ready"); --for k, v in ipairs(l1) do print(k,v) end end); it("should work work with multiple runners in parallel", function () -------------------- local wait1, done1; local last_item1 = 0; local r1 = new(function (item) print("r1 processing ", item); assert(type(item) == "number") assert((item == last_item1 + 1) or item == 3); last_item1 = item; if item == 3 then wait1, done1 = async.waiter(); wait1(); end end, "r1"); local wait2, done2; local last_item2 = 0; local r2 = new(function (item) print("r2 processing ", item); assert.is_number(item); assert((item == last_item2 + 1) or item == 3); last_item2 = item; if item == 3 then wait2, done2 = async.waiter(); wait2(); end end, "r2"); r1:run(1); assert.equal(r1.state, "ready"); r1:run(2); assert.equal(r1.state, "ready"); r1:run(5); assert.equal(r1.state, "ready"); r1:run(3); assert.equal(r1.state, "waiting"); r1:run(5); -- Will error, when we get to it assert.equal(r1.state, "waiting"); done1(); assert.equal(r1.state, "ready"); r1:run(3); assert.equal(r1.state, "waiting"); r2:run(1); assert.equal(r1.state, "waiting"); assert.equal(r2.state, "ready"); r2:run(2); assert.equal(r1.state, "waiting"); assert.equal(r2.state, "ready"); r2:run(3); assert.equal(r1.state, "waiting"); assert.equal(r2.state, "waiting"); done2(); assert.equal(r1.state, "waiting"); assert.equal(r2.state, "ready"); r2:run(3); assert.equal(r1.state, "waiting"); assert.equal(r2.state, "waiting"); done2(); assert.equal(r1.state, "waiting"); assert.equal(r2.state, "ready"); r2:run(4); assert.equal(r1.state, "waiting"); assert.equal(r2.state, "ready"); done1(); assert.equal(r1.state, "ready"); r1:run(4); assert.equal(r1.state, "ready"); assert.equal(r1.state, "ready"); end); -- luacheck: ignore 211/rf -- FIXME what's rf? it("should support multiple done() calls", function () local processed_item; local wait, done; local r, rf = new(function (item) wait, done = async.waiter(4); wait(); processed_item = item; end); r:run("test"); for _ = 1, 3 do done(); assert.equal(r.state, "waiting"); assert.is_nil(processed_item); end done(); assert.equal(r.state, "ready"); assert.equal(processed_item, "test"); assert.spy(r.watchers.error).was_not.called(); end); it("should not allow done() to be called more than specified", function () local processed_item; local wait, done; local r, rf = new(function (item) wait, done = async.waiter(4); wait(); processed_item = item; end); r:run("test"); for _ = 1, 4 do done(); end assert.has_error(done); assert.equal(r.state, "ready"); assert.equal(processed_item, "test"); assert.spy(r.watchers.error).was_not.called(); end); it("should allow done() to be called before wait()", function () local processed_item; local r, rf = new(function (item) local wait, done = async.waiter(); done(); wait(); processed_item = item; end); r:run("test"); assert.equal(processed_item, "test"); assert.equal(r.state, "ready"); -- Since the observable state did not change, -- the watchers should not have been called assert.spy(r.watchers.waiting).was_not.called(); assert.spy(r.watchers.ready).was_not.called(); end); end); describe("#ready()", function () it("should return false outside an async context", function () assert.falsy(async.ready()); end); it("should return true inside an async context", function () local r = new(function () assert.truthy(async.ready()); end); r:run(true); assert.spy(r.func).was.called(); assert.spy(r.watchers.error).was_not.called(); end); end); describe("#sleep()", function () after_each(function () -- Restore to default async.set_schedule_function(nil); end); it("should fail if no scheduler configured", function () local r = new(function () async.sleep(5); end); r:run(true); assert.spy(r.watchers.error).was.called(); -- Set dummy scheduler async.set_schedule_function(function () end); local r2 = new(function () async.sleep(5); end); r2:run(true); assert.spy(r2.watchers.error).was_not.called(); end); it("should work", function () local queue = {}; local add_task = spy.new(function (t, f) table.insert(queue, { t, f }); end); async.set_schedule_function(add_task); local processed_item; local r = new(function (item) async.sleep(5); processed_item = item; end); r:run("test"); -- Nothing happened, because the runner is sleeping assert.is_nil(processed_item); assert.equal(r.state, "waiting"); assert.spy(add_task).was_called(1); assert.spy(add_task).was_called_with(match.is_number(), match.is_function()); assert.spy(r.watchers.waiting).was.called(); assert.spy(r.watchers.ready).was_not.called(); -- Pretend the timer has triggered, call the handler queue[1][2](); assert.equal(processed_item, "test"); assert.equal(r.state, "ready"); assert.spy(r.watchers.ready).was.called(); end); end); describe("#set_nexttick()", function () after_each(function () -- Restore to default async.set_nexttick(nil); end); it("should work", function () local queue = {}; local nexttick = spy.new(function (f) assert.is_function(f); table.insert(queue, f); end); async.set_nexttick(nexttick); local processed_item; local wait, done; local r = new(function (item) wait, done = async.waiter(); wait(); processed_item = item; end); r:run("test"); -- Nothing happened, because the runner is waiting assert.is_nil(processed_item); assert.equal(r.state, "waiting"); assert.spy(nexttick).was_called(0); assert.spy(r.watchers.waiting).was.called(); assert.spy(r.watchers.ready).was_not.called(); -- Mark the runner as ready, it should be scheduled for -- the next tick done(); assert.spy(nexttick).was_called(1); assert.spy(nexttick).was_called_with(match.is_function()); assert.equal(1, #queue); -- Pretend it's the next tick - call the pending function queue[1](); assert.equal(processed_item, "test"); assert.equal(r.state, "ready"); assert.spy(r.watchers.ready).was.called(); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_bitcompat_spec.lua0000644000000000000000000000011714773555365020066 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.739711124 prosody-13.0.1/spec/util_bitcompat_spec.lua0000644000175000017500000000144614773555365022272 0ustar00prosodyprosody00000000000000describe("util.bitcompat", function () -- bitcompat will pass through to an appropriate implementation. Our -- goal here is to check that whatever implementation is in use passes -- these basic sanity checks. local bit = require "util.bitcompat"; it("bor works", function () assert.equal(0xF0FF, bit.bor(0xF000, 0x00F0, 0x000F)); end); it("band works", function () assert.equal(0x0F, bit.band(0xFF, 0x1F, 0x0F)); end); it("bxor works", function () assert.equal(0x13, bit.bxor(0x10, 0x0F, 0x0C)); end); it("rshift works", function () assert.equal(0x0F, bit.rshift(0xFF, 4)); end); 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); prosody-13.0.1/spec/PaxHeaders/util_cache_spec.lua0000644000000000000000000000011714773555365017147 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.739711124 prosody-13.0.1/spec/util_cache_spec.lua0000644000175000017500000002601514773555365021352 0ustar00prosodyprosody00000000000000 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); local function expect_kv(key, value, actual_key, actual_value) assert.are.equal(key, actual_key, "key incorrect"); assert.are.equal(value, actual_value, "value incorrect"); end expect_kv(nil, nil, c:head()); expect_kv(nil, nil, c:tail()); assert.are.equal(c:count(), 0); c:set("one", 1) assert.are.equal(c:count(), 1); expect_kv("one", 1, c:head()); expect_kv("one", 1, c:tail()); c:set("two", 2) expect_kv("two", 2, c:head()); expect_kv("one", 1, c:tail()); c:set("three", 3) expect_kv("three", 3, c:head()); expect_kv("one", 1, c:tail()); c:set("four", 4) c:set("five", 5); assert.are.equal(c:count(), 5); expect_kv("five", 5, c:head()); expect_kv("one", 1, c:tail()); c:set("foo", nil); assert.are.equal(c:count(), 5); expect_kv("five", 5, c:head()); expect_kv("one", 1, c:tail()); assert.are.equal(c:get("one"), 1); expect_kv("five", 5, c:head()); expect_kv("one", 1, c:tail()); assert.are.equal(c:get("two"), 2); assert.are.equal(c:get("three"), 3); assert.are.equal(c:get("four"), 4); assert.are.equal(c:get("five"), 5); assert.are.equal(c:get("foo"), nil); assert.are.equal(c:get("bar"), nil); c:set("six", 6); assert.are.equal(c:count(), 5); expect_kv("six", 6, c:head()); expect_kv("two", 2, c:tail()); assert.are.equal(c:get("one"), nil); assert.are.equal(c:get("two"), 2); assert.are.equal(c:get("three"), 3); assert.are.equal(c:get("four"), 4); assert.are.equal(c:get("five"), 5); assert.are.equal(c:get("six"), 6); c:set("three", nil); assert.are.equal(c:count(), 4); assert.are.equal(c:get("one"), nil); assert.are.equal(c:get("two"), 2); assert.are.equal(c:get("three"), nil); assert.are.equal(c:get("four"), 4); assert.are.equal(c:get("five"), 5); assert.are.equal(c:get("six"), 6); c:set("seven", 7); assert.are.equal(c:count(), 5); assert.are.equal(c:get("one"), nil); assert.are.equal(c:get("two"), 2); assert.are.equal(c:get("three"), nil); assert.are.equal(c:get("four"), 4); assert.are.equal(c:get("five"), 5); assert.are.equal(c:get("six"), 6); assert.are.equal(c:get("seven"), 7); c:set("eight", 8); assert.are.equal(c:count(), 5); assert.are.equal(c:get("one"), nil); assert.are.equal(c:get("two"), nil); assert.are.equal(c:get("three"), nil); assert.are.equal(c:get("four"), 4); assert.are.equal(c:get("five"), 5); assert.are.equal(c:get("six"), 6); assert.are.equal(c:get("seven"), 7); assert.are.equal(c:get("eight"), 8); c:set("four", 4); assert.are.equal(c:count(), 5); assert.are.equal(c:get("one"), nil); assert.are.equal(c:get("two"), nil); assert.are.equal(c:get("three"), nil); assert.are.equal(c:get("four"), 4); assert.are.equal(c:get("five"), 5); assert.are.equal(c:get("six"), 6); assert.are.equal(c:get("seven"), 7); assert.are.equal(c:get("eight"), 8); c:set("nine", 9); assert.are.equal(c:count(), 5); assert.are.equal(c:get("one"), nil); assert.are.equal(c:get("two"), nil); assert.are.equal(c:get("three"), nil); assert.are.equal(c:get("four"), 4); assert.are.equal(c:get("five"), nil); assert.are.equal(c:get("six"), 6); assert.are.equal(c:get("seven"), 7); assert.are.equal(c:get("eight"), 8); assert.are.equal(c:get("nine"), 9); do local keys = { "nine", "four", "eight", "seven", "six" }; local values = { 9, 4, 8, 7, 6 }; local i = 0; for k, v in c:items() do i = i + 1; assert.are.equal(k, keys[i]); assert.are.equal(v, values[i]); end assert.are.equal(i, 5); c:set("four", "2+2"); assert.are.equal(c:count(), 5); assert.are.equal(c:get("one"), nil); assert.are.equal(c:get("two"), nil); assert.are.equal(c:get("three"), nil); assert.are.equal(c:get("four"), "2+2"); assert.are.equal(c:get("five"), nil); assert.are.equal(c:get("six"), 6); assert.are.equal(c:get("seven"), 7); assert.are.equal(c:get("eight"), 8); assert.are.equal(c:get("nine"), 9); end do local keys = { "four", "nine", "eight", "seven", "six" }; local values = { "2+2", 9, 8, 7, 6 }; local i = 0; for k, v in c:items() do i = i + 1; assert.are.equal(k, keys[i]); assert.are.equal(v, values[i]); end assert.are.equal(i, 5); c:set("foo", nil); assert.are.equal(c:count(), 5); assert.are.equal(c:get("one"), nil); assert.are.equal(c:get("two"), nil); assert.are.equal(c:get("three"), nil); assert.are.equal(c:get("four"), "2+2"); assert.are.equal(c:get("five"), nil); assert.are.equal(c:get("six"), 6); assert.are.equal(c:get("seven"), 7); assert.are.equal(c:get("eight"), 8); assert.are.equal(c:get("nine"), 9); end do local keys = { "four", "nine", "eight", "seven", "six" }; local values = { "2+2", 9, 8, 7, 6 }; local i = 0; for k, v in c:items() do i = i + 1; assert.are.equal(k, keys[i]); assert.are.equal(v, values[i]); end assert.are.equal(i, 5); c:set("four", nil); assert.are.equal(c:get("one"), nil); assert.are.equal(c:get("two"), nil); assert.are.equal(c:get("three"), nil); assert.are.equal(c:get("four"), nil); assert.are.equal(c:get("five"), nil); assert.are.equal(c:get("six"), 6); assert.are.equal(c:get("seven"), 7); assert.are.equal(c:get("eight"), 8); assert.are.equal(c:get("nine"), 9); end do local keys = { "nine", "eight", "seven", "six" }; local values = { 9, 8, 7, 6 }; local i = 0; for k, v in c:items() do i = i + 1; assert.are.equal(k, keys[i]); assert.are.equal(v, values[i]); end assert.are.equal(i, 4); end do local evicted_key, evicted_value; local c2 = cache.new(3, function (_key, _value) evicted_key, evicted_value = _key, _value; end); local function set(k, v, should_evict_key, should_evict_value) evicted_key, evicted_value = nil, nil; c2:set(k, v); assert.are.equal(evicted_key, should_evict_key); assert.are.equal(evicted_value, should_evict_value); end set("a", 1) set("a", 1) set("a", 1) set("a", 1) set("a", 1) set("b", 2) set("c", 3) set("b", 2) set("d", 4, "a", 1) set("e", 5, "c", 3) end do local evicted_key, evicted_value; local c3 = cache.new(1, function (_key, _value) evicted_key, evicted_value = _key, _value; if _key == "a" then -- Sanity check for what we're evicting assert.are.equal(_key, "a"); assert.are.equal(_value, 1); -- We're going to block eviction of this key/value, so set to nil... evicted_key, evicted_value = nil, nil; -- Returning false to block eviction return false end end); local function set(k, v, should_evict_key, should_evict_value) evicted_key, evicted_value = nil, nil; local ret = c3:set(k, v); assert.are.equal(evicted_key, should_evict_key); assert.are.equal(evicted_value, should_evict_value); return ret; end set("a", 1) set("a", 1) set("a", 1) set("a", 1) set("a", 1) -- Our on_evict prevents "a" from being evicted, causing this to fail... assert.are.equal(set("b", 2), false, "Failed to prevent eviction, or signal result"); expect_kv("a", 1, c3:head()); expect_kv("a", 1, c3:tail()); -- Check the final state is what we expect assert.are.equal(c3:get("a"), 1); assert.are.equal(c3:get("b"), nil); assert.are.equal(c3:count(), 1); end local c4 = cache.new(3, false); assert.are.equal(c4:set("a", 1), true); assert.are.equal(c4:set("a", 1), true); assert.are.equal(c4:set("a", 1), true); assert.are.equal(c4:set("a", 1), true); assert.are.equal(c4:set("b", 2), true); assert.are.equal(c4:set("c", 3), true); assert.are.equal(c4:set("d", 4), false); assert.are.equal(c4:set("d", 4), false); assert.are.equal(c4:set("d", 4), false); expect_kv("c", 3, c4:head()); expect_kv("a", 1, c4:tail()); local c5 = cache.new(3, function (k, v) --luacheck: ignore 212/v if k == "a" then return nil; elseif k == "b" then return true; end return false; end); assert.are.equal(c5:set("a", 1), true); assert.are.equal(c5:set("a", 1), true); assert.are.equal(c5:set("a", 1), true); assert.are.equal(c5:set("a", 1), true); assert.are.equal(c5:set("b", 2), true); assert.are.equal(c5:set("c", 3), true); assert.are.equal(c5:set("d", 4), true); -- "a" evicted (cb returned nil) assert.are.equal(c5:set("d", 4), true); -- nop assert.are.equal(c5:set("d", 4), true); -- nop assert.are.equal(c5:set("e", 5), true); -- "b" evicted (cb returned true) assert.are.equal(c5:set("f", 6), false); -- "c" won't evict (cb returned false) expect_kv("e", 5, c5:head()); expect_kv("c", 3, c5:tail()); end); it(":table works", function () local t = cache.new(3):table(); assert.is.table(t); t["a"] = "1"; assert.are.equal(t["a"], "1"); t["b"] = "2"; assert.are.equal(t["b"], "2"); t["c"] = "3"; assert.are.equal(t["c"], "3"); t["d"] = "4"; assert.are.equal(t["d"], "4"); assert.are.equal(t["a"], nil); local i = spy.new(function () end); for k, v in pairs(t) do i(k,v) end assert.spy(i).was_called(); assert.spy(i).was_called_with("b", "2"); 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); prosody-13.0.1/spec/PaxHeaders/util_crypto_spec.lua0000644000000000000000000000011714773555365017424 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.739711124 prosody-13.0.1/spec/util_crypto_spec.lua0000644000175000017500000001477214773555365021636 0ustar00prosodyprosody00000000000000local 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); prosody-13.0.1/spec/PaxHeaders/util_dataforms_spec.lua0000644000000000000000000000011714773555365020064 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.739711124 prosody-13.0.1/spec/util_dataforms_spec.lua0000644000175000017500000003304714773555365022272 0ustar00prosodyprosody00000000000000local dataforms = require "util.dataforms"; local st = require "util.stanza"; local jid = require "util.jid"; local iter = require "util.iterators"; describe("util.dataforms", function () local some_form, xform; setup(function () some_form = dataforms.new({ title = "form-title", instructions = "form-instructions", { type = "hidden", name = "FORM_TYPE", value = "xmpp:prosody.im/spec/util.dataforms#1", }; { type = "fixed"; value = "Fixed field"; }, { type = "boolean", label = "boolean-label", name = "boolean-field", value = true, }, { type = "fixed", label = "fixed-label", name = "fixed-field", value = "fixed-value", }, { type = "hidden", label = "hidden-label", name = "hidden-field", value = "hidden-value", }, { type = "jid-multi", label = "jid-multi-label", name = "jid-multi-field", value = { "jid@multi/value#1", "jid@multi/value#2", }, }, { type = "jid-single", label = "jid-single-label", name = "jid-single-field", value = "jid@single/value", }, { type = "list-multi", label = "list-multi-label", name = "list-multi-field", value = { "list-multi-option-value#1", "list-multi-option-value#3", }, options = { { label = "list-multi-option-label#1", value = "list-multi-option-value#1", default = true, }, { label = "list-multi-option-label#2", value = "list-multi-option-value#2", default = false, }, { label = "list-multi-option-label#3", value = "list-multi-option-value#3", default = true, }, } }, { type = "list-single", label = "list-single-label", name = "list-single-field", value = "list-single-value", options = { "list-single-value", "list-single-value#2", "list-single-value#3", } }, { type = "text-multi", label = "text-multi-label", name = "text-multi-field", value = "text\nmulti\nvalue", }, { type = "text-private", label = "text-private-label", name = "text-private-field", value = "text-private-value", }, { type = "text-single", label = "text-single-label", name = "text-single-field", value = "text-single-value", }, { -- XEP-0221 -- TODO Validate the XML produced by this. type = "text-single", label = "text-single-with-media-label", name = "text-single-with-media-field", media = { height = 24, width = 32, { type = "image/png", uri = "data:", }, }, }, }); xform = some_form:form(); end); it("XML serialization looks like it should", function () assert.truthy(xform); assert.truthy(st.is_stanza(xform)); assert.equal("x", xform.name); assert.equal("jabber:x:data", xform.attr.xmlns); 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, instructions = true, field = true, } for tag in xform:childtags() do assert.truthy(allowed_direct_children[tag.name], "unknown direct child"); end end); it("produced boolean field correctly", function () local f; for field in xform:childtags("field") do if field.attr.var == "boolean-field" then f = field; break; end end assert.truthy(st.is_stanza(f)); assert.equal("boolean-field", f.attr.var); assert.equal("boolean", f.attr.type); assert.equal("boolean-label", f.attr.label); assert.equal(1, iter.count(f:childtags("value"))); local val = f:get_child_text("value"); assert.truthy(val == "true" or val == "1"); end); it("produced fixed field correctly", function () local f; for field in xform:childtags("field") do if field.attr.var == "fixed-field" then f = field; break; end end assert.truthy(st.is_stanza(f)); assert.equal("fixed-field", f.attr.var); assert.equal("fixed", f.attr.type); assert.equal("fixed-label", f.attr.label); assert.equal(1, iter.count(f:childtags("value"))); assert.equal("fixed-value", f:get_child_text("value")); end); it("produced hidden field correctly", function () local f; for field in xform:childtags("field") do if field.attr.var == "hidden-field" then f = field; break; end end assert.truthy(st.is_stanza(f)); assert.equal("hidden-field", f.attr.var); assert.equal("hidden", f.attr.type); assert.equal("hidden-label", f.attr.label); assert.equal(1, iter.count(f:childtags("value"))); assert.equal("hidden-value", f:get_child_text("value")); end); it("produced jid-multi field correctly", function () local f; for field in xform:childtags("field") do if field.attr.var == "jid-multi-field" then f = field; break; end end assert.truthy(st.is_stanza(f)); assert.equal("jid-multi-field", f.attr.var); assert.equal("jid-multi", f.attr.type); assert.equal("jid-multi-label", f.attr.label); assert.equal(2, iter.count(f:childtags("value"))); local i = 0; for value in f:childtags("value") do i = i + 1; assert.equal(("jid@multi/value#%d"):format(i), value:get_text()); end end); it("produced jid-single field correctly", function () local f; for field in xform:childtags("field") do if field.attr.var == "jid-single-field" then f = field; break; end end assert.truthy(st.is_stanza(f)); assert.equal("jid-single-field", f.attr.var); assert.equal("jid-single", f.attr.type); assert.equal("jid-single-label", f.attr.label); assert.equal(1, iter.count(f:childtags("value"))); assert.equal("jid@single/value", f:get_child_text("value")); assert.truthy(jid.prep(f:get_child_text("value"))); end); it("produced list-multi field correctly", function () local f; for field in xform:childtags("field") do if field.attr.var == "list-multi-field" then f = field; break; end end assert.truthy(st.is_stanza(f)); assert.equal("list-multi-field", f.attr.var); assert.equal("list-multi", f.attr.type); assert.equal("list-multi-label", f.attr.label); assert.equal(2, iter.count(f:childtags("value"))); assert.equal("list-multi-option-value#1", f:get_child_text("value")); assert.equal(3, iter.count(f:childtags("option"))); end); it("produced list-single field correctly", function () local f; for field in xform:childtags("field") do if field.attr.var == "list-single-field" then f = field; break; end end assert.truthy(st.is_stanza(f)); assert.equal("list-single-field", f.attr.var); assert.equal("list-single", f.attr.type); assert.equal("list-single-label", f.attr.label); assert.equal(1, iter.count(f:childtags("value"))); assert.equal("list-single-value", f:get_child_text("value")); assert.equal(3, iter.count(f:childtags("option"))); end); it("produced text-multi field correctly", function () local f; for field in xform:childtags("field") do if field.attr.var == "text-multi-field" then f = field; break; end end assert.truthy(st.is_stanza(f)); assert.equal("text-multi-field", f.attr.var); assert.equal("text-multi", f.attr.type); assert.equal("text-multi-label", f.attr.label); assert.equal(3, iter.count(f:childtags("value"))); end); it("produced text-private field correctly", function () local f; for field in xform:childtags("field") do if field.attr.var == "text-private-field" then f = field; break; end end assert.truthy(st.is_stanza(f)); assert.equal("text-private-field", f.attr.var); assert.equal("text-private", f.attr.type); assert.equal("text-private-label", f.attr.label); assert.equal(1, iter.count(f:childtags("value"))); assert.equal("text-private-value", f:get_child_text("value")); end); it("produced text-single field correctly", function () local f; for field in xform:childtags("field") do if field.attr.var == "text-single-field" then f = field; break; end end assert.truthy(st.is_stanza(f)); assert.equal("text-single-field", f.attr.var); assert.equal("text-single", f.attr.type); assert.equal("text-single-label", f.attr.label); assert.equal(1, iter.count(f:childtags("value"))); assert.equal("text-single-value", f:get_child_text("value")); end); describe("get_type()", function () it("identifes dataforms", function () assert.equal(nil, dataforms.get_type(nil)); assert.equal(nil, dataforms.get_type("")); assert.equal(nil, dataforms.get_type({})); assert.equal(nil, dataforms.get_type(st.stanza("no-a-form"))); assert.equal("xmpp:prosody.im/spec/util.dataforms#1", dataforms.get_type(xform)); end); end); describe(":data", function () it("returns something", function () assert.truthy(some_form:data(xform)); end); end); describe("issue1177", function () local form_with_stuff; setup(function () form_with_stuff = dataforms.new({ { type = "list-single"; name = "abtest"; label = "A or B?"; options = { { label = "A", value = "a", default = true }, { label = "B", value = "b" }, }; }, }); end); it("includes options when value is included", function () local f = form_with_stuff:form({ abtest = "a" }); assert.truthy(f:find("field/option")); end); it("includes options when value is excluded", function () local f = form_with_stuff:form({}); assert.truthy(f:find("field/option")); end); end); describe("using current values in place of missing fields", function () it("gets back the previous values when given an empty form", function () local current = { ["list-multi-field"] = { "list-multi-option-value#2"; }; ["list-single-field"] = "list-single-value#2"; ["hidden-field"] = "hidden-value"; ["boolean-field"] = false; ["text-multi-field"] = "words\ngo\nhere"; ["jid-single-field"] = "alice@example.com"; ["text-private-field"] = "hunter2"; ["text-single-field"] = "text-single-value"; ["jid-multi-field"] = { "bob@example.net"; }; }; local expect = { -- FORM_TYPE = "xmpp:prosody.im/spec/util.dataforms#1"; -- does this need to be included? ["list-multi-field"] = { "list-multi-option-value#2"; }; ["list-single-field"] = "list-single-value#2"; ["hidden-field"] = "hidden-value"; ["boolean-field"] = false; ["text-multi-field"] = "words\ngo\nhere"; ["jid-single-field"] = "alice@example.com"; ["text-private-field"] = "hunter2"; ["text-single-field"] = "text-single-value"; ["jid-multi-field"] = { "bob@example.net"; }; }; local data, err = some_form:data(st.stanza("x", {xmlns="jabber:x:data"}), current); assert.is.table(data, err); assert.same(expect, data, "got back the same data"); end); end); describe("field 'var' property", function () it("works as expected", function () local f = dataforms.new { { var = "someprefix#the-field", name = "the_field", type = "text-single", } }; local x = f:form({the_field = "hello"}); assert.equal("someprefix#the-field", x:find"field@var"); assert.equal("hello", x:find"field/value#"); end); end); describe("number handling", function() it("handles numbers as booleans", function() local f = dataforms.new { { name = "boolean"; type = "boolean" } }; local x = f:form({ boolean = 0 }); assert.equal("0", x:find "field/value#"); x = f:form({ boolean = 1 }); assert.equal("1", x:find "field/value#"); end); end) describe("datatype validation", function () describe("integer", function () local f = dataforms.new { { name = "number", type = "text-single", datatype = "xs:integer", range_min = -10, range_max = 10, }, }; it("roundtrip works", function () local d = f:data(f:form({number = 1})); assert.equal(1, d.number); end); it("error handling works", function () local d,e = f:data(f:form({number = "nan"})); assert.not_equal(1, d.number); assert.table(e); assert.string(e.number); end); it("bounds-checking work works", function () local d,e = f:data(f:form({number = 100})); assert.not_equal(100, d.number); assert.table(e); assert.string(e.number); end); it("serializes larger ints okay", function () local x = f:form{number=1125899906842624} assert.equal("1125899906842624", x:find("field/value#")) end); end) describe("datetime", function () local f = dataforms.new { { name = "when"; type = "text-single"; datatype = "xs:dateTime" } } it("works", function () local x = f:form({ when = 1219439340 }); assert.equal("2008-08-22T21:09:00Z", x:find("field/value#")) local d, e = f:data(x); assert.is_nil(e); assert.same({ when = 1219439340 }, d); end); end) end); describe("media element", function () it("produced media element correctly", function () local f; for field in xform:childtags("field") do if field.attr.var == "text-single-with-media-field" then f = field; break; end end assert.truthy(st.is_stanza(f)); assert.equal("text-single-with-media-field", f.attr.var); assert.equal("text-single", f.attr.type); assert.equal("text-single-with-media-label", f.attr.label); assert.equal(0, iter.count(f:childtags("value"))); local m = f:get_child("media", "urn:xmpp:media-element"); assert.truthy(st.is_stanza(m)); assert.equal("24", m.attr.height); assert.equal("32", m.attr.width); assert.equal(1, iter.count(m:childtags("uri"))); local u = m:get_child("uri"); assert.truthy(st.is_stanza(u)); assert.equal("image/png", u.attr.type); assert.equal("data:", u:get_text()); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_datamanager_spec.lua0000644000000000000000000000011714773555365020350 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.739711124 prosody-13.0.1/spec/util_datamanager_spec.lua0000644000175000017500000000343214773555365022551 0ustar00prosodyprosody00000000000000describe("util.datamanager", function() local dm; setup(function() dm = require "util.datamanager"; dm.set_data_path("./data"); end); describe("keyvalue", function() local data = {hello = "world"}; do local ok, err = dm.store("keyval-user", "datamanager.test", "testdata", data); assert.truthy(ok, err); end do local read, err = dm.load("keyval-user", "datamanager.test", "testdata") assert.same(data, read, err); end do local ok, err = dm.store("keyval-user", "datamanager.test", "testdata", nil); assert.truthy(ok, err); end do local read, err = dm.load("keyval-user", "datamanager.test", "testdata") assert.is_nil(read, err); end end) describe("lists", function() do local ok, err = dm.list_store("list-user", "datamanager.test", "testdata", {}); assert.truthy(ok, err); end do local nothing, err = dm.list_load("list-user", "datamanager.test", "testdata"); assert.is_nil(nothing, err); assert.is_nil(err); end do local ok, err = dm.list_append("list-user", "datamanager.test", "testdata", {id = 1}); assert.truthy(ok, err); end do local ok, err = dm.list_append("list-user", "datamanager.test", "testdata", {id = 2}); assert.truthy(ok, err); end do local ok, err = dm.list_append("list-user", "datamanager.test", "testdata", {id = 3}); assert.truthy(ok, err); end do local list, err = dm.list_load("list-user", "datamanager.test", "testdata"); assert.same(list, {{id = 1}; {id = 2}; {id = 3}}, err); end do local ok, err = dm.list_store("list-user", "datamanager.test", "testdata", {}); assert.truthy(ok, err); end do local nothing, err = dm.list_load("list-user", "datamanager.test", "testdata"); assert.is_nil(nothing, err); assert.is_nil(err); end end) end) prosody-13.0.1/spec/PaxHeaders/util_datamapper_spec.lua0000644000000000000000000000011714773555365020222 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.739711124 prosody-13.0.1/spec/util_datamapper_spec.lua0000644000175000017500000001553514773555365022432 0ustar00prosodyprosody00000000000000local st local xml local map setup(function() st = require "util.stanza"; xml = require "util.xml"; map = require "util.datamapper"; end); describe("util.datamapper", function() local s, x, d local disco, disco_info, disco_schema setup(function() -- a convenience function for simple attributes, there's a few of them 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; 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}}}; }; state = { type = "string"; enum = { "active", "inactive", "gone", "composing", "paused", }; xml = {x_name_is_value = true; namespace = "http://jabber.org/protocol/chatstates"}; }; fallback = { type = "boolean"; xml = {x_name_is_value = true; name = "fallback"; namespace = "urn:xmpp:fallback:0"}; }; origin_id = { type = "string"; xml = {name = "origin-id"; namespace = "urn:xmpp:sid:0"; x_single_attribute = "id"}; }; react = { type = "object"; xml = {namespace = "urn:xmpp:reactions:0"; name = "reactions"}; properties = { to = {type = "string"; xml = {attribute = true; name = "id"}}; -- should be assumed to be array since it has 'items' reactions = { items = { xml = { name = "reaction" } } }; }; }; stanza_ids = { type = "array"; items = { xml = {name = "stanza-id"; namespace = "urn:xmpp:sid:0"}; type = "object"; properties = { id = attr; by = attr; }; }; }; }; }; x = xml.parse [[ Hello Because 👋 🐢 ]]; d = { to = "a@test"; from = "b@test"; type = "chat"; id = "1"; lang = "en"; body = "Hello"; delay = {from = "test"; stamp = "2021-03-07T15:59:08+00:00"; reason = "Because"}; state = "active"; fallback = true; origin_id = "qgkmMdPB"; stanza_ids = {{id = "abc1"; by = "muc"}; {id = "xyz2"; by = "host"}}; react = { to = "744f6e18-a57a-11e9-a656-4889e7820c76"; reactions = { "👋", "🐢", }; }; }; disco_schema = { ["$defs"] = { attr = { type = "string"; xml = { attribute = true } } }; type = "object"; xml = { name = "iq"; namespace = "jabber:client" }; properties = { to = attr; from = attr; type = attr; id = attr; disco = { type = "object"; xml = { name = "query"; namespace = "http://jabber.org/protocol/disco#info" }; properties = { features = { type = "array"; items = { type = "string"; xml = { name = "feature"; x_single_attribute = "var"; }; }; }; }; }; }; }; disco_info = xml.parse[[ wrong ]]; disco = { type="result"; id="disco1"; from="example.com"; disco = { features = { "urn:example:feature:1"; "urn:example:feature:2"; "urn:example:feature:3"; }; }; }; end); describe("parse", function() it("works", function() assert.same(d, map.parse(s, x)); end); it("handles arrays", function () assert.same(disco, map.parse(disco_schema, disco_info)); end); it("deals with locally built stanzas", function() -- FIXME this could also be argued to be a util.stanza problem local ver_schema = { type = "object"; xml = {name = "iq"}; properties = { type = {type = "string"; xml = {attribute = true}}; id = {type = "string"; xml = {attribute = true}}; version = { type = "object"; xml = {name = "query"; namespace = "jabber:iq:version"}; -- properties should be assumed to be strings properties = {name = true; version = {}; os = {}}; }; }; }; local ver_st = st.iq({type = "result"; id = "v1"}) :query("jabber:iq:version") :text_tag("name", "Prosody") :text_tag("version", "trunk") :text_tag("os", "Lua 5.3") :reset(); local data = {type = "result"; id = "v1"; version = {name = "Prosody"; version = "trunk"; os = "Lua 5.3"}} assert.same(data, map.parse(ver_schema, ver_st)); end); end); describe("unparse", function() it("works", function() local u = map.unparse(s, d); assert.equal("message", u.name); assert.same(x.attr, u.attr); assert.equal(x:get_child_text("body"), u:get_child_text("body")); assert.equal(x:get_child_text("delay", "urn:xmpp:delay"), u:get_child_text("delay", "urn:xmpp:delay")); assert.same(x:get_child("delay", "urn:xmpp:delay").attr, u:get_child("delay", "urn:xmpp:delay").attr); assert.same(x:get_child("origin-id", "urn:xmpp:sid:0").attr, u:get_child("origin-id", "urn:xmpp:sid:0").attr); assert.same(x:get_child("reactions", "urn:xmpp:reactions:0").attr, u:get_child("reactions", "urn:xmpp:reactions:0").attr); assert.same(2, #u:get_child("reactions", "urn:xmpp:reactions:0").tags); for _, tag in ipairs(x.tags) do if tag.name ~= "UNRELATED" then assert.truthy(u:get_child(tag.name, tag.attr.xmlns) or u:get_child(tag.name), tag:top_tag()) end end assert.equal(#x.tags-1, #u.tags) end); it("handles arrays", function () local u = map.unparse(disco_schema, disco); assert.equal("urn:example:feature:1", u:find("{http://jabber.org/protocol/disco#info}query/feature/@var")) local n = 0; for child in u:get_child("query", "http://jabber.org/protocol/disco#info"):childtags("feature") do n = n + 1; assert.equal(string.format("urn:example:feature:%d", n), child.attr.var); end end); end); end) prosody-13.0.1/spec/PaxHeaders/util_datetime_spec.lua0000644000000000000000000000011714773555365017700 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.739711124 prosody-13.0.1/spec/util_datetime_spec.lua0000644000175000017500000000717314773555365022107 0ustar00prosodyprosody00000000000000local util_datetime = require "util.datetime"; describe("util.datetime", function () it("should have been loaded", function () assert.is_table(util_datetime); end); describe("#date", function () local date = util_datetime.date; it("should exist", function () assert.is_function(date); end); it("should return a string", function () assert.is_string(date()); end); it("should look like a date", function () assert.truthy(string.find(date(), "^%d%d%d%d%-%d%d%-%d%d$")); end); it("should work", function () 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 () local time = util_datetime.time; it("should exist", function () assert.is_function(time); end); it("should return a string", function () assert.is_string(time()); end); it("should look like a timestamp", function () -- Note: Sub-second precision and timezones are ignored assert.truthy(string.find(time(), "^%d%d:%d%d:%d%d")); end); it("should work", function () 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; it("should exist", function () assert.is_function(datetime); end); it("should return a string", function () assert.is_string(datetime()); end); it("should look like a timestamp", function () -- Note: Sub-second precision and timezones are ignored 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("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; it("should exist", function () assert.is_function(parse); end); it("should work", function () -- Timestamp used by Go 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); prosody-13.0.1/spec/PaxHeaders/util_dbuffer_spec.lua0000644000000000000000000000011614773555365017520 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.74371114 prosody-13.0.1/spec/util_dbuffer_spec.lua0000644000175000017500000001421314773555365021721 0ustar00prosodyprosody00000000000000local dbuffer = require "util.dbuffer"; describe("util.dbuffer", function () describe("#new", function () it("has a constructor", function () assert.Function(dbuffer.new); 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)); end); it("won't create a negatively sized buffer", function () assert.falsy(dbuffer.new(-1)); end); end); describe(":write", function () 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 () it("supports optional bytes parameter", function () -- should return the frontmost chunk local b = dbuffer.new(); assert.truthy(b:write("hello")); assert.truthy(b:write(" ")); assert.truthy(b:write("world")); assert.equal("h", b:read(1)); assert.equal("ello", b:read()); 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 () it("works", function () local b = dbuffer.new(); b:write("hello\n"); b:write("world"); b:write("\n"); b:write("\n\n"); b:write("stuff"); b:write("more\nand more"); assert.equal(nil, b:read_until(".")); assert.equal(nil, b:read_until("%")); assert.equal("hello\n", b:read_until("\n")); assert.equal("world\n", b:read_until("\n")); assert.equal("\n", b:read_until("\n")); assert.equal("\n", b:read_until("\n")); assert.equal("stu", b:read(3)); assert.equal("ffmore\n", b:read_until("\n")); assert.equal(nil, b:read_until("\n")); assert.equal("and more", b:read_chunk()); end); end); describe(":discard", function () local b = dbuffer.new(); it("works", function () assert.truthy(b:write("hello world")); assert.truthy(b:discard(6)); assert.equal(5, b:length()); 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(); end); end); describe(":sub", function () -- Helper function to compare buffer:sub() with string:sub() local s = "hello world"; local function test_sub(b, x, y) local string_result, buffer_result = s:sub(x, y), b:sub(x, y); assert.equals(string_result, buffer_result, ("buffer:sub(%d, %s) does not match string:sub()"):format(x, y and ("%d"):format(y) or "nil")); end it("works", function () local b = dbuffer.new(); assert.truthy(b:write("hello world")); assert.equals("hello", b:sub(1, 5)); end); it("works after discard", function () local b = dbuffer.new(256); assert.truthy(b:write("foobar")); assert.equals("foobar", b:sub(1, 6)); assert.truthy(b:discard(3)); -- consume "foo" assert.equals("bar", b:sub(1, 3)); end); it("supports optional end parameter", function () local b = dbuffer.new(); assert.truthy(b:write("hello world")); assert.equals("hello world", b:sub(1)); assert.equals("world", b:sub(-5)); end); it("is equivalent to string:sub", function () local b = dbuffer.new(11); assert.truthy(b:write(s)); for i = -13, 13 do for j = -13, 13 do test_sub(b, i, j); 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 () -- Helper function to compare buffer:byte() with string:byte() 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(%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 () local b = dbuffer.new(11); assert.truthy(b:write(s)); test_byte(b, 1); 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); end end end); it("works with characters > 127", function () local b = dbuffer.new(); b:write(string.char(0, 140)); local r = { b:byte(1, 2) }; assert.same({ 0, 140 }, r); end); it("works on an empty buffer", function () local b = dbuffer.new(); assert.equal("", b:sub(1,1)); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_debug_spec.lua0000644000000000000000000000011614773555365017171 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.74371114 prosody-13.0.1/spec/util_debug_spec.lua0000644000175000017500000000450114773555365021371 0ustar00prosodyprosody00000000000000local dbg = require "util.debug"; describe("util.debug", function () describe("traceback()", function () it("works", function () local tb = dbg.traceback(); assert.is_string(tb); end); end); describe("get_traceback_table()", function () it("works", function () local count = 0; -- MUST stay in sync with the line numbers of these functions: local f1_defined, f3_defined = 43, 15; local function f3(f3_param) --luacheck: ignore 212/f3_param count = count + 1; for i = 1, 2 do local tb = dbg.get_traceback_table(i == 1 and coroutine.running() or nil, 0); assert.is_table(tb); --print(dbg.traceback(), "\n\n\n", require "util.serialization".serialize(tb, { fatal = false, unquoted = true})); local found_f1, found_f3; for _, frame in ipairs(tb) do if frame.info.linedefined == f1_defined then assert.equal(0, #frame.locals); assert.equal("f2", frame.upvalues[1].name); assert.equal("f1_upvalue", frame.upvalues[2].name); found_f1 = true; elseif frame.info.linedefined == f3_defined then assert.equal("f3_param", frame.locals[1].name); found_f3 = true; end end assert.is_true(found_f1); assert.is_true(found_f3); end end local function f2() local f2_local = "hello"; return f3(f2_local); end local f1_upvalue = "upvalue1"; local function f1() f2(f1_upvalue); end -- ok/err are caught and re-thrown so that -- busted gets to handle them in its own way local ok, err; local function hook() debug.sethook(); ok, err = pcall(f1); end -- Test the traceback is correct in various -- types of caller environments -- From a Lua hook debug.sethook(hook, "crl", 1); local a = string.sub("abcdef", 3, 4); assert.equal("cd", a); debug.sethook(); assert.equal(1, count); if not ok then error(err); end ok, err = nil, nil; -- From a signal handler (C hook) require "util.signal".signal("SIGUSR1", hook); require "util.signal".raise("SIGUSR1"); assert.equal(2, count); if not ok then error(err); end ok, err = nil, nil; -- Inside a coroutine local co = coroutine.create(function () hook(); end); coroutine.resume(co); if not ok then error(err); end assert.equal(3, count); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_encodings_spec.lua0000644000000000000000000000011614773555365020054 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.74371114 prosody-13.0.1/spec/util_encodings_spec.lua0000644000175000017500000000267714773555365022270 0ustar00prosodyprosody00000000000000 local encodings = require "util.encodings"; local utf8 = assert(encodings.utf8, "no encodings.utf8 module"); describe("util.encodings", function () describe("#encode()", function() it("should work", function () assert.is.equal(encodings.base64.encode(""), ""); assert.is.equal(encodings.base64.encode('coucou'), "Y291Y291"); assert.is.equal(encodings.base64.encode("\0\0\0"), "AAAA"); assert.is.equal(encodings.base64.encode("\255\255\255"), "////"); end); end); describe("#decode()", function() it("should work", function () assert.is.equal(encodings.base64.decode(""), ""); assert.is.equal(encodings.base64.decode("="), ""); assert.is.equal(encodings.base64.decode('Y291Y291'), "coucou"); assert.is.equal(encodings.base64.decode("AAAA"), "\0\0\0"); assert.is.equal(encodings.base64.decode("////"), "\255\255\255"); end); end); end); describe("util.encodings.utf8", function() describe("#valid()", function() it("should work", function() for line in io.lines("spec/utf8_sequences.txt") do local data = line:match(":%s*([^#]+)"):gsub("%s+", ""):gsub("..", function (c) return string.char(tonumber(c, 16)); end) local expect = line:match("(%S+):"); assert(expect == "pass" or expect == "fail", "unknown expectation: "..line:match("^[^:]+")); local valid = utf8.valid(data); assert.is.equal(valid, utf8.valid(data.." ")); assert.is.equal(valid, expect == "pass", line); end end); end); end); prosody-13.0.1/spec/PaxHeaders/util_envload_spec.lua0000644000000000000000000000011614773555365017533 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.74371114 prosody-13.0.1/spec/util_envload_spec.lua0000644000175000017500000000117714773555365021741 0ustar00prosodyprosody00000000000000describe("util.envload", function() local envload = require "util.envload"; describe("envload()", function() it("works", function() local f, err = envload.envload("return 'hello'", "@test", {}); assert.is_function(f, err); local ok, ret = pcall(f); assert.truthy(ok); assert.equal("hello", ret); end); it("lets you pass values in and out", function () local f, err = envload.envload("return thisglobal", "@test", { thisglobal = "yes, this one" }); assert.is_function(f, err); local ok, ret = pcall(f); assert.truthy(ok); assert.equal("yes, this one", ret); end); end) -- TODO envloadfile() end) prosody-13.0.1/spec/PaxHeaders/util_error_spec.lua0000644000000000000000000000011614773555365017234 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.74371114 prosody-13.0.1/spec/util_error_spec.lua0000644000175000017500000001540314773555365021437 0ustar00prosodyprosody00000000000000local errors = require "util.error" describe("util.error", function () describe("new()", function () it("works", function () local err = errors.new("bork", "bork bork"); assert.not_nil(err); assert.equal("cancel", err.type); assert.equal("undefined-condition", err.condition); assert.same("bork bork", err.context); end); describe("templates", function () it("works", function () local templates = { ["fail"] = { type = "wait", condition = "internal-server-error", code = 555; }; }; local err = errors.new("fail", { traceback = "in some file, somewhere" }, templates); assert.equal("wait", err.type); assert.equal("internal-server-error", err.condition); assert.equal(555, err.code); assert.same({ traceback = "in some file, somewhere" }, err.context); end); end); end); describe("is_error()", function () it("works", function () assert.truthy(errors.is_error(errors.new())); assert.falsy(errors.is_error("not an error")); end); end); describe("coerce", function () it("works", function () local ok, err = errors.coerce(nil, "it dun goofed"); assert.is_nil(ok); assert.truthy(errors.is_error(err)) end); end); describe("from_stanza", function () it("works", function () local st = require "util.stanza"; 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_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); describe("__tostring", function () it("doesn't throw", function () assert.has_no.errors(function () -- See 6f317e51544d tostring(errors.new()); end); end); end); describe("extra", function () it("keeps some extra fields", function () local err = errors.new({condition="gone",text="Sorry mate, it's all gone",extra={uri="file:///dev/null"}}); assert.is_table(err.extra); assert.equal("file:///dev/null", err.extra.uri); end); end) describe("init", function() it("basics works", function() local reg = errors.init("test", { broke = {type = "cancel"; condition = "internal-server-error"; text = "It broke :("}; nope = {type = "auth"; condition = "not-authorized"; text = "Can't let you do that Dave"}; }); local broke = reg.new("broke"); assert.equal("cancel", broke.type); assert.equal("internal-server-error", broke.condition); assert.equal("It broke :(", broke.text); assert.equal("test", broke.source); local nope = reg.new("nope"); assert.equal("auth", nope.type); assert.equal("not-authorized", nope.condition); assert.equal("Can't let you do that Dave", nope.text); end); it("compact mode works", function() local reg = errors.init("test", "spec", { broke = {"cancel"; "internal-server-error"; "It broke :("}; nope = {"auth"; "not-authorized"; "Can't let you do that Dave"; "sorry-dave"}; }); local broke = reg.new("broke"); assert.equal("cancel", broke.type); assert.equal("internal-server-error", broke.condition); assert.equal("It broke :(", broke.text); assert.is_nil(broke.extra); local nope = reg.new("nope"); assert.equal("auth", nope.type); assert.equal("not-authorized", nope.condition); assert.equal("Can't let you do that Dave", nope.text); assert.equal("spec", nope.extra.namespace); assert.equal("sorry-dave", nope.extra.condition); end); it("registry looks the same regardless of syntax", function() local normal = errors.init("test", { broke = {type = "cancel"; condition = "internal-server-error"; text = "It broke :("}; nope = { type = "auth"; condition = "not-authorized"; text = "Can't let you do that Dave"; extra = {namespace = "spec"; condition = "sorry-dave"}; }; }); local compact1 = errors.init("test", "spec", { broke = {"cancel"; "internal-server-error"; "It broke :("}; nope = {"auth"; "not-authorized"; "Can't let you do that Dave"; "sorry-dave"}; }); local compact2 = errors.init("test", { broke = {"cancel"; "internal-server-error"; "It broke :("}; nope = {"auth"; "not-authorized"; "Can't let you do that Dave"}; }); assert.same(normal.registry, compact1.registry); assert.same({ broke = {type = "cancel"; condition = "internal-server-error"; text = "It broke :("}; nope = {type = "auth"; condition = "not-authorized"; text = "Can't let you do that Dave"}; }, compact2.registry); end); describe(".wrap", function () local reg = errors.init("test", "spec", { myerror = { "cancel", "internal-server-error", "Oh no" }; }); it("is exposed", function () assert.is_function(reg.wrap); end); it("returns errors according to the registry", function () local e = reg.wrap("myerror"); assert.equal("cancel", e.type); assert.equal("internal-server-error", e.condition); assert.equal("Oh no", e.text); end); it("passes through existing errors", function () local e = reg.wrap(reg.new({ type = "auth", condition = "forbidden" })); assert.equal("auth", e.type); assert.equal("forbidden", e.condition); end); it("wraps arbitrary values", function () local e = reg.wrap(123); assert.equal("cancel", e.type); assert.equal("undefined-condition", e.condition); assert.equal(123, e.context.wrapped_error); end); end); describe(".coerce", function () local reg = errors.init("test", "spec", { myerror = { "cancel", "internal-server-error", "Oh no" }; }); it("is exposed", function () assert.is_function(reg.coerce); end); it("passes through existing errors", function () local function test() return nil, errors.new({ type = "auth", condition = "forbidden" }); end local ok, err = reg.coerce(test()); assert.is_nil(ok); assert.is_truthy(errors.is_error(err)); assert.equal("forbidden", err.condition); end); it("passes through successful return values", function () local function test() return 1, 2, 3, 4; end local one, two, three, four = reg.coerce(test()); assert.equal(1, one); assert.equal(2, two); assert.equal(3, three); assert.equal(4, four); end); it("wraps non-error objects", function () local function test() return nil, "myerror"; end local ok, err = reg.coerce(test()); assert.is_nil(ok); assert.is_truthy(errors.is_error(err)); assert.equal("internal-server-error", err.condition); assert.equal("Oh no", err.text); end); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_events_spec.lua0000644000000000000000000000011614773555365017407 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.74371114 prosody-13.0.1/spec/util_events_spec.lua0000644000175000017500000001676114773555365021622 0ustar00prosodyprosody00000000000000local events = require "util.events"; describe("util.events", function () it("should export a new() function", function () assert.is_function(events.new); end); describe("new()", function () it("should return return a new events object", function () local e = events.new(); assert.is_function(e.add_handler); assert.is_function(e.remove_handler); end); end); local e, h; describe("API", function () before_each(function () e = events.new(); h = spy.new(function () end); end); it("should call handlers when an event is fired", function () e.add_handler("myevent", h); e.fire_event("myevent"); assert.spy(h).was_called(); end); it("should not call handlers when a different event is fired", function () e.add_handler("myevent", h); e.fire_event("notmyevent"); assert.spy(h).was_not_called(); end); it("should pass the data argument to handlers", function () e.add_handler("myevent", h); e.fire_event("myevent", "mydata"); assert.spy(h).was_called_with("mydata"); end); it("should support non-string events", function () local myevent = {}; e.add_handler(myevent, h); e.fire_event(myevent, "mydata"); assert.spy(h).was_called_with("mydata"); end); it("should call handlers in priority order", function () local data = {}; e.add_handler("myevent", function () table.insert(data, "h1"); end, 5); e.add_handler("myevent", function () table.insert(data, "h2"); end, 3); e.add_handler("myevent", function () table.insert(data, "h3"); end); e.fire_event("myevent", "mydata"); assert.same(data, { "h1", "h2", "h3" }); end); it("should support non-integer priority values", function () local data = {}; e.add_handler("myevent", function () table.insert(data, "h1"); end, 1); e.add_handler("myevent", function () table.insert(data, "h2"); end, 0.5); e.add_handler("myevent", function () table.insert(data, "h3"); end, 0.25); e.fire_event("myevent", "mydata"); assert.same(data, { "h1", "h2", "h3" }); end); it("should support negative priority values", function () local data = {}; e.add_handler("myevent", function () table.insert(data, "h1"); end, 1); e.add_handler("myevent", function () table.insert(data, "h2"); end, 0); e.add_handler("myevent", function () table.insert(data, "h3"); end, -1); e.fire_event("myevent", "mydata"); assert.same(data, { "h1", "h2", "h3" }); end); it("should support removing handlers", function () e.add_handler("myevent", h); e.fire_event("myevent"); e.remove_handler("myevent", h); e.fire_event("myevent"); assert.spy(h).was_called(1); end); it("should support adding multiple handlers at the same time", function () local ht = { myevent1 = spy.new(function () end); myevent2 = spy.new(function () end); myevent3 = spy.new(function () end); }; e.add_handlers(ht); e.fire_event("myevent1"); e.fire_event("myevent2"); assert.spy(ht.myevent1).was_called(); assert.spy(ht.myevent2).was_called(); assert.spy(ht.myevent3).was_not_called(); end); it("should support removing multiple handlers at the same time", function () local ht = { myevent1 = spy.new(function () end); myevent2 = spy.new(function () end); myevent3 = spy.new(function () end); }; e.add_handlers(ht); e.remove_handlers(ht); e.fire_event("myevent1"); e.fire_event("myevent2"); assert.spy(ht.myevent1).was_not_called(); assert.spy(ht.myevent2).was_not_called(); assert.spy(ht.myevent3).was_not_called(); end); pending("should support adding handlers within an event handler") pending("should support removing handlers within an event handler") it("should support getting the current handlers for an event", function () e.add_handler("myevent", h); local handlers = e.get_handlers("myevent"); assert.equal(h, handlers[1]); end); describe("wrappers", function () local w before_each(function () w = spy.new(function (handlers, event_name, event_data) assert.is_function(handlers); assert.equal("myevent", event_name) assert.equal("abc", event_data); return handlers(event_name, event_data); end); end); it("should get called", function () e.add_wrapper("myevent", w); e.add_handler("myevent", h); e.fire_event("myevent", "abc"); assert.spy(w).was_called(1); assert.spy(h).was_called(1); end); it("should be removable", function () e.add_wrapper("myevent", w); e.add_handler("myevent", h); e.fire_event("myevent", "abc"); e.remove_wrapper("myevent", w); e.fire_event("myevent", "abc"); assert.spy(w).was_called(1); assert.spy(h).was_called(2); end); it("should allow multiple wrappers", function () local w2 = spy.new(function (handlers, event_name, event_data) return handlers(event_name, event_data); end); e.add_wrapper("myevent", w); e.add_handler("myevent", h); e.add_wrapper("myevent", w2); e.fire_event("myevent", "abc"); e.remove_wrapper("myevent", w); e.fire_event("myevent", "abc"); assert.spy(w).was_called(1); assert.spy(w2).was_called(2); assert.spy(h).was_called(2); end); it("should support a mix of global and event wrappers", function () local w2 = spy.new(function (handlers, event_name, event_data) return handlers(event_name, event_data); end); e.add_wrapper(false, w); e.add_handler("myevent", h); e.add_wrapper("myevent", w2); e.fire_event("myevent", "abc"); e.remove_wrapper(false, w); e.fire_event("myevent", "abc"); assert.spy(w).was_called(1); assert.spy(w2).was_called(2); assert.spy(h).was_called(2); end); end); describe("global wrappers", function () local w before_each(function () w = spy.new(function (handlers, event_name, event_data) assert.is_function(handlers); assert.equal("myevent", event_name) assert.equal("abc", event_data); return handlers(event_name, event_data); end); end); it("should get called", function () e.add_wrapper(false, w); e.add_handler("myevent", h); e.fire_event("myevent", "abc"); assert.spy(w).was_called(1); assert.spy(h).was_called(1); end); it("should be removable", function () e.add_wrapper(false, w); e.add_handler("myevent", h); e.fire_event("myevent", "abc"); e.remove_wrapper(false, w); e.fire_event("myevent", "abc"); assert.spy(w).was_called(1); assert.spy(h).was_called(2); end); end); describe("debug hooks", function () it("should get called", function () local d = spy.new(function (handler, event_name, event_data) --luacheck: ignore 212/event_name return handler(event_data); end); e.add_handler("myevent", h); e.fire_event("myevent"); assert.spy(h).was_called(1); assert.spy(d).was_called(0); assert.is_nil(e.set_debug_hook(d)); e.fire_event("myevent", { mydata = true }); assert.spy(h).was_called(2); assert.spy(d).was_called(1); assert.spy(d).was_called_with(h, "myevent", { mydata = true }); assert.equal(d, e.set_debug_hook(nil)); e.fire_event("myevent", { mydata = false }); assert.spy(h).was_called(3); assert.spy(d).was_called(1); end); it("setting should return any existing debug hook", function () local function f() end local function g() end assert.is_nil(e.set_debug_hook(f)); assert.is_equal(f, e.set_debug_hook(g)); assert.is_equal(g, e.set_debug_hook(f)); assert.is_equal(f, e.set_debug_hook(nil)); assert.is_nil(e.set_debug_hook(f)); end); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_format_spec.lua0000644000000000000000000000011614773555365017373 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.74371114 prosody-13.0.1/spec/util_format_spec.lua0000644000175000017500000007205714773555365021606 0ustar00prosodyprosody00000000000000local format = require "util.format".format; -- There are eight basic types in Lua: -- nil, boolean, number, string, function, userdata, thread, and table describe("util.format", function() describe("#format()", function() it("should work", function() assert.equal("hello", format("%s", "hello")); assert.equal("(nil)", format("%s")); assert.equal("(nil)", format("%d")); assert.equal("(nil)", format("%q")); assert.equal(" [(nil)]", format("", nil)); assert.equal("true", format("%s", true)); assert.equal("[true]", format("%d", true)); assert.equal("% [true]", format("%%", true)); assert.equal("{}", format("%q", {})); assert.equal("[1.5]", format("%d", 1.5)); assert.equal("[7.3786976294838e+19]", format("%d", 73786976294838206464)); end); it("escapes ascii control stuff", function () assert.equal("␁", format("%s", "\1")); assert.equal("[␁]", format("%d", "\1")); end); it("escapes invalid UTF-8", function () assert.equal("\"Hello w\\195rld\"", format("%s", "Hello w\195rld")); end); if _VERSION >= "Lua 5.4" then it("handles %p formats", function () assert.matches("a 0x%x+ b", format("%s %p %s", "a", {}, "b")); end) else it("does something with %p formats", function () assert.string(format("%p", {})); end) end it("escapes multi-line strings", function () assert.equal("Hello\n\tWorld", format("%s", "Hello\nWorld")) assert.equal("\"Hello\\nWorld\"", format("%q", "Hello\nWorld")) end) -- Tests generated with loops! describe("nil", function () describe("to %c", function () it("works", function () assert.equal("(nil)", format("%c", nil)) end); end); describe("to %d", function () it("works", function () assert.equal("(nil)", format("%d", nil)) end); end); describe("to %i", function () it("works", function () assert.equal("(nil)", format("%i", nil)) end); end); describe("to %o", function () it("works", function () assert.equal("(nil)", format("%o", nil)) end); end); describe("to %u", function () it("works", function () assert.equal("(nil)", format("%u", nil)) end); end); describe("to %x", function () it("works", function () assert.equal("(nil)", format("%x", nil)) end); end); describe("to %X", function () it("works", function () assert.equal("(nil)", format("%X", nil)) end); end); describe("to %a", function () it("works", function () assert.equal("(nil)", format("%a", nil)) end); end); describe("to %A", function () it("works", function () assert.equal("(nil)", format("%A", nil)) end); end); describe("to %e", function () it("works", function () assert.equal("(nil)", format("%e", nil)) end); end); describe("to %E", function () it("works", function () assert.equal("(nil)", format("%E", nil)) end); end); describe("to %f", function () it("works", function () assert.equal("(nil)", format("%f", nil)) end); end); describe("to %g", function () it("works", function () assert.equal("(nil)", format("%g", nil)) end); end); describe("to %G", function () it("works", function () assert.equal("(nil)", format("%G", nil)) end); end); describe("to %q", function () it("works", function () assert.equal("(nil)", format("%q", nil)) end); end); describe("to %s", function () it("works", function () assert.equal("(nil)", format("%s", nil)) end); end); end); describe("boolean", function () describe("to %c", function () it("works", function () assert.equal("[true]", format("%c", true)) assert.equal("[false]", format("%c", false)) end); end); describe("to %d", function () it("works", function () assert.equal("[true]", format("%d", true)) assert.equal("[false]", format("%d", false)) end); end); describe("to %i", function () it("works", function () assert.equal("[true]", format("%i", true)) assert.equal("[false]", format("%i", false)) end); end); describe("to %o", function () it("works", function () assert.equal("[true]", format("%o", true)) assert.equal("[false]", format("%o", false)) end); end); describe("to %u", function () it("works", function () assert.equal("[true]", format("%u", true)) assert.equal("[false]", format("%u", false)) end); end); describe("to %x", function () it("works", function () assert.equal("[true]", format("%x", true)) assert.equal("[false]", format("%x", false)) end); end); describe("to %X", function () it("works", function () assert.equal("[true]", format("%X", true)) assert.equal("[false]", format("%X", false)) end); end); describe("to %a", function () it("works", function () assert.equal("[true]", format("%a", true)) assert.equal("[false]", format("%a", false)) end); end); describe("to %A", function () it("works", function () assert.equal("[true]", format("%A", true)) assert.equal("[false]", format("%A", false)) end); end); describe("to %e", function () it("works", function () assert.equal("[true]", format("%e", true)) assert.equal("[false]", format("%e", false)) end); end); describe("to %E", function () it("works", function () assert.equal("[true]", format("%E", true)) assert.equal("[false]", format("%E", false)) end); end); describe("to %f", function () it("works", function () assert.equal("[true]", format("%f", true)) assert.equal("[false]", format("%f", false)) end); end); describe("to %g", function () it("works", function () assert.equal("[true]", format("%g", true)) assert.equal("[false]", format("%g", false)) end); end); describe("to %G", function () it("works", function () assert.equal("[true]", format("%G", true)) assert.equal("[false]", format("%G", false)) end); end); describe("to %q", function () it("works", function () assert.equal("true", format("%q", true)) assert.equal("false", format("%q", false)) end); end); describe("to %s", function () it("works", function () assert.equal("true", format("%s", true)) assert.equal("false", format("%s", false)) end); end); end); describe("number", function () describe("to %c", function () it("works", function () assert.equal("a", format("%c", 97)) assert.equal("[1.5]", format("%c", 1.5)) assert.equal("[7.3786976294838e+19]", format("%c", 73786976294838206464)) assert.equal("[inf]", format("%c", math.huge)) end); end); describe("to %d", function () it("works", function () assert.equal("97", format("%d", 97)) assert.equal("-12345", format("%d", -12345)) assert.equal("[1.5]", format("%d", 1.5)) assert.equal("[7.3786976294838e+19]", format("%d", 73786976294838206464)) assert.equal("[inf]", format("%d", math.huge)) assert.equal("2147483647", format("%d", 2147483647)) end); end); describe("to %i", function () it("works", function () assert.equal("97", format("%i", 97)) assert.equal("-12345", format("%i", -12345)) assert.equal("[1.5]", format("%i", 1.5)) assert.equal("[7.3786976294838e+19]", format("%i", 73786976294838206464)) assert.equal("[inf]", format("%i", math.huge)) assert.equal("2147483647", format("%i", 2147483647)) end); end); describe("to %o", function () it("works", function () assert.equal("141", format("%o", 97)) assert.equal("[-12345]", format("%o", -12345)) assert.equal("[1.5]", format("%o", 1.5)) assert.equal("[7.3786976294838e+19]", format("%o", 73786976294838206464)) assert.equal("[inf]", format("%o", math.huge)) assert.equal("17777777777", format("%o", 2147483647)) end); end); describe("to %u", function () it("works", function () assert.equal("97", format("%u", 97)) assert.equal("[-12345]", format("%u", -12345)) assert.equal("[1.5]", format("%u", 1.5)) assert.equal("[7.3786976294838e+19]", format("%u", 73786976294838206464)) assert.equal("[inf]", format("%u", math.huge)) assert.equal("2147483647", format("%u", 2147483647)) end); end); describe("to %x", function () it("works", function () assert.equal("61", format("%x", 97)) assert.equal("[-12345]", format("%x", -12345)) assert.equal("[1.5]", format("%x", 1.5)) assert.equal("[7.3786976294838e+19]", format("%x", 73786976294838206464)) assert.equal("[inf]", format("%x", math.huge)) assert.equal("7fffffff", format("%x", 2147483647)) end); end); describe("to %X", function () it("works", function () assert.equal("61", format("%X", 97)) assert.equal("[-12345]", format("%X", -12345)) assert.equal("[1.5]", format("%X", 1.5)) assert.equal("[7.3786976294838e+19]", format("%X", 73786976294838206464)) assert.equal("[inf]", format("%X", math.huge)) assert.equal("7FFFFFFF", format("%X", 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); 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 %e", function () it("works", function () assert.equal("9.700000e+01", format("%e", 97)) assert.equal("-1.234500e+04", format("%e", -12345)) assert.equal("1.500000e+00", format("%e", 1.5)) assert.equal("7.378698e+19", format("%e", 73786976294838206464)) assert.equal("inf", format("%e", math.huge)) assert.equal("2.147484e+09", format("%e", 2147483647)) end); end); describe("to %E", function () it("works", function () assert.equal("9.700000E+01", format("%E", 97)) assert.equal("-1.234500E+04", format("%E", -12345)) assert.equal("1.500000E+00", format("%E", 1.5)) assert.equal("7.378698E+19", format("%E", 73786976294838206464)) assert.equal("INF", format("%E", math.huge)) assert.equal("2.147484E+09", format("%E", 2147483647)) end); end); describe("to %f", function () it("works", function () assert.equal("97.000000", format("%f", 97)) assert.equal("-12345.000000", format("%f", -12345)) assert.equal("1.500000", format("%f", 1.5)) assert.equal("73786976294838206464.000000", format("%f", 73786976294838206464)) assert.equal("inf", format("%f", math.huge)) assert.equal("2147483647.000000", format("%f", 2147483647)) end); end); describe("to %g", function () it("works", function () assert.equal("97", format("%g", 97)) assert.equal("-12345", format("%g", -12345)) assert.equal("1.5", format("%g", 1.5)) assert.equal("7.3787e+19", format("%g", 73786976294838206464)) assert.equal("inf", format("%g", math.huge)) assert.equal("2.14748e+09", format("%g", 2147483647)) end); end); describe("to %G", function () it("works", function () assert.equal("97", format("%G", 97)) assert.equal("-12345", format("%G", -12345)) assert.equal("1.5", format("%G", 1.5)) assert.equal("7.3787E+19", format("%G", 73786976294838206464)) assert.equal("INF", format("%G", math.huge)) assert.equal("2.14748E+09", format("%G", 2147483647)) end); end); describe("to %q", function () it("works", function () assert.equal("97", format("%q", 97)) assert.equal("-12345", format("%q", -12345)) assert.equal("1.5", format("%q", 1.5)) assert.equal("7.37869762948382065e+19", format("%q", 73786976294838206464)) assert.equal("(1/0)", format("%q", math.huge)) assert.equal("2147483647", format("%q", 2147483647)) end); end); describe("to %s", function () it("works", function () assert.equal("97", format("%s", 97)) assert.equal("-12345", format("%s", -12345)) assert.equal("1.5", format("%s", 1.5)) assert.equal("7.3786976294838e+19", format("%s", 73786976294838206464)) assert.equal("inf", format("%s", math.huge)) assert.equal("2147483647", format("%s", 2147483647)) end); end); end); describe("string", function () describe("to %c", function () it("works", function () assert.equal("[hello]", format("%c", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%c", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%c", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%c", "n\195\182d\195\165tg\195")) end); end); describe("to %d", function () it("works", function () assert.equal("[hello]", format("%d", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%d", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%d", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%d", "n\195\182d\195\165tg\195")) end); end); describe("to %i", function () it("works", function () assert.equal("[hello]", format("%i", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%i", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%i", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%i", "n\195\182d\195\165tg\195")) end); end); describe("to %o", function () it("works", function () assert.equal("[hello]", format("%o", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%o", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%o", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%o", "n\195\182d\195\165tg\195")) end); end); describe("to %u", function () it("works", function () assert.equal("[hello]", format("%u", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%u", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%u", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%u", "n\195\182d\195\165tg\195")) end); end); describe("to %x", function () it("works", function () assert.equal("[hello]", format("%x", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%x", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%x", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%x", "n\195\182d\195\165tg\195")) end); end); describe("to %X", function () it("works", function () assert.equal("[hello]", format("%X", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%X", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%X", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%X", "n\195\182d\195\165tg\195")) end); end); describe("to %a", function () it("works", function () assert.equal("[hello]", format("%a", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%a", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%a", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%a", "n\195\182d\195\165tg\195")) end); end); describe("to %A", function () it("works", function () assert.equal("[hello]", format("%A", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%A", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%A", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%A", "n\195\182d\195\165tg\195")) end); end); describe("to %e", function () it("works", function () assert.equal("[hello]", format("%e", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%e", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%e", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%e", "n\195\182d\195\165tg\195")) end); end); describe("to %E", function () it("works", function () assert.equal("[hello]", format("%E", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%E", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%E", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%E", "n\195\182d\195\165tg\195")) end); end); describe("to %f", function () it("works", function () assert.equal("[hello]", format("%f", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%f", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%f", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%f", "n\195\182d\195\165tg\195")) end); end); describe("to %g", function () it("works", function () assert.equal("[hello]", format("%g", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%g", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%g", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%g", "n\195\182d\195\165tg\195")) end); end); describe("to %G", function () it("works", function () assert.equal("[hello]", format("%G", "hello")) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%G", "foo \001\002\003 bar")) assert.equal("[nödåtgärd]", format("%G", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%G", "n\195\182d\195\165tg\195")) end); end); describe("to %q", function () it("works", function () assert.equal("\"hello\"", format("%q", "hello")) assert.equal("\"foo \\001\\002\\003 bar\"", format("%q", "foo \001\002\003 bar")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\\164rd\"", format("%q", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%q", "n\195\182d\195\165tg\195")) end); end); describe("to %s", function () it("works", function () assert.equal("hello", format("%s", "hello")) assert.equal("foo \226\144\129\226\144\130\226\144\131 bar", format("%s", "foo \001\002\003 bar")) assert.equal("nödåtgärd", format("%s", "n\195\182d\195\165tg\195\164rd")) assert.equal("\"n\\195\\182d\\195\\165tg\\195\"", format("%s", "n\195\182d\195\165tg\195")) end); end); end); describe("function", function () describe("to %c", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%c", function() end)) end); end); describe("to %d", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%d", function() end)) end); end); describe("to %i", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%i", function() end)) end); end); describe("to %o", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%o", function() end)) end); end); describe("to %u", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%u", function() end)) end); end); describe("to %x", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%x", function() end)) end); end); describe("to %X", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%X", function() end)) end); end); describe("to %a", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%a", function() end)) end); end); describe("to %A", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%A", function() end)) end); end); describe("to %e", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%e", function() end)) end); end); describe("to %E", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%E", function() end)) end); end); describe("to %f", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%f", function() end)) end); end); describe("to %g", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%g", function() end)) end); end); describe("to %G", function () it("works", function () assert.matches("[function: 0[xX]%x+]", format("%G", function() end)) end); end); describe("to %q", function () it("works", function () assert.matches('%[%[function: 0[xX]%x+]]', format("%q", function() end)) end); end); describe("to %s", function () it("works", function () assert.matches("function: 0[xX]%x+", format("%s", function() end)) end); end); end); describe("thread", function () describe("to %c", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%c", coroutine.create(function() end))) end); end); describe("to %d", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%d", coroutine.create(function() end))) end); end); describe("to %i", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%i", coroutine.create(function() end))) end); end); describe("to %o", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%o", coroutine.create(function() end))) end); end); describe("to %u", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%u", coroutine.create(function() end))) end); end); describe("to %x", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%x", coroutine.create(function() end))) end); end); describe("to %X", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%X", coroutine.create(function() end))) end); end); describe("to %a", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%a", coroutine.create(function() end))) end); end); describe("to %A", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%A", coroutine.create(function() end))) end); end); describe("to %e", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%e", coroutine.create(function() end))) end); end); describe("to %E", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%E", coroutine.create(function() end))) end); end); describe("to %f", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%f", coroutine.create(function() end))) end); end); describe("to %g", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%g", coroutine.create(function() end))) end); end); describe("to %G", function () it("works", function () assert.matches("[thread: 0[xX]%x+]", format("%G", coroutine.create(function() end))) end); end); describe("to %q", function () it("works", function () assert.matches('_%[%[thread: 0[xX]%x+]]', format("%q", coroutine.create(function() end))) end); end); describe("to %s", function () it("works", function () assert.matches("thread: 0[xX]%x+", format("%s", coroutine.create(function() end))) end); end); end); describe("table", function () describe("to %c", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%c", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%c", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %d", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%d", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%d", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %i", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%i", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%i", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %o", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%o", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%o", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %u", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%u", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%u", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %x", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%x", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%x", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %X", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%X", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%X", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %a", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%a", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%a", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %A", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%A", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%A", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %e", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%e", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%e", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %E", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%E", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%E", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %f", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%f", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%f", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %g", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%g", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%g", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); describe("to %G", function () it("works", function () assert.matches("[table: 0[xX]%x+]", format("%G", { })) assert.equal("[foo \226\144\129\226\144\130\226\144\131 bar]", format("%G", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); 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}))) end); end); describe("to %s", function () it("works", function () assert.matches("table: 0[xX]%x+", format("%s", { })) assert.equal("foo \226\144\129\226\144\130\226\144\131 bar", format("%s", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_fsm_spec.lua0000644000000000000000000000011614773555365016670 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.74371114 prosody-13.0.1/spec/util_fsm_spec.lua0000644000175000017500000001505414773555365021075 0ustar00prosodyprosody00000000000000describe("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); prosody-13.0.1/spec/PaxHeaders/util_hashes_spec.lua0000644000000000000000000000011614773555365017356 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.74371114 prosody-13.0.1/spec/util_hashes_spec.lua0000644000175000017500000000775214773555365021571 0ustar00prosodyprosody00000000000000-- Test vectors from RFC 6070 local hashes = require "util.hashes"; 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" local S = "salt" local c = 1 local DK = "0c60c80f961f0e71f3a9b524af6012062fe037a6"; assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha1(P, S, c))); end); it("test vector 2", function () local P = "password" local S = "salt" local c = 2 local DK = "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"; assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha1(P, S, c))); end); it("test vector 3", function () local P = "password" local S = "salt" local c = 4096 local DK = "4b007901b765489abead49d926f721d065a429c1"; assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha1(P, S, c))); end); it("test vector 4 #SLOW", function () local P = "password" local S = "salt" local c = 16777216 local DK = "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984"; assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha1(P, S, c))); end); end); describe("PBKDF2-HMAC-SHA256", function () it("test vector 1", function () local P = "password"; local S = "salt"; local c = 1 local DK = "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b"; assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha256(P, S, c))); end); it("test vector 2", function () local P = "password"; local S = "salt"; local c = 2 local DK = "ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43"; assert.equal(DK, hex.encode(hashes.pbkdf2_hmac_sha256(P, S, c))); end); 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); prosody-13.0.1/spec/PaxHeaders/util_hashring_spec.lua0000644000000000000000000000011614773555365017706 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.74371114 prosody-13.0.1/spec/util_hashring_spec.lua0000644000175000017500000000455014773555365022112 0ustar00prosodyprosody00000000000000local hashring = require "util.hashring"; describe("util.hashring", function () randomize(false); local sha256 = require "util.hashes".sha256; local ring = hashring.new(128, sha256); it("should fail to get a node that does not exist", function () assert.is_nil(ring:get_node("foo")) end); it("should support adding nodes", function () ring:add_node("node1"); end); it("should return a single node for all keys if only one node exists", function () for i = 1, 100 do assert.is_equal("node1", ring:get_node(tostring(i))) end end); it("should support adding a second node", function () ring:add_node("node2"); end); it("should fail to remove a non-existent node", function () assert.is_falsy(ring:remove_node("node3")); end); it("should succeed to remove a node", function () assert.is_truthy(ring:remove_node("node1")); end); it("should return the only node for all keys", function () for i = 1, 100 do assert.is_equal("node2", ring:get_node(tostring(i))) end end); it("should support adding multiple nodes", function () ring:add_nodes({ "node1", "node3", "node4", "node5" }); end); it("should disrupt a minimal number of keys on node removal", function () local orig_ring = ring:clone(); local node_tallies = {}; local n = 1000; for i = 1, n do local key = tostring(i); local node = ring:get_node(key); node_tallies[node] = (node_tallies[node] or 0) + 1; end --[[ for node, key_count in pairs(node_tallies) do print(node, key_count, ("%.2f%%"):format((key_count/n)*100)); end ]] ring:remove_node("node5"); local disrupted_keys = 0; for i = 1, n do local key = tostring(i); if orig_ring:get_node(key) ~= ring:get_node(key) then disrupted_keys = disrupted_keys + 1; end end assert.is_equal(node_tallies["node5"], disrupted_keys); end); it("should support removing multiple nodes", function () ring:remove_nodes({"node2", "node3", "node4", "node5"}); end); it("should return a single node for all keys if only one node remains", function () for i = 1, 100 do assert.is_equal("node1", ring:get_node(tostring(i))) 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); prosody-13.0.1/spec/PaxHeaders/util_hmac_spec.lua0000644000000000000000000000011714773555365017014 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.747711157 prosody-13.0.1/spec/util_hmac_spec.lua0000644000175000017500000001236714773555365021224 0ustar00prosodyprosody00000000000000-- Test cases from RFC 4231 -- Yes, the lines are long, it's annoying to split the long hex things. -- luacheck: ignore 631 local hmac = require "util.hmac"; local hex = require "util.hex"; describe("Test case 1", function () local Key = hex.decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); local Data = hex.decode("4869205468657265"); describe("HMAC-SHA-256", function () it("works", function() assert.equal("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", hmac.sha256(Key, Data, true)) end); end); describe("HMAC-SHA-512", function () it("works", function() assert.equal("87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854", hmac.sha512(Key, Data, true)) end); end); end); describe("Test case 2", function () local Key = hex.decode("4a656665"); local Data = hex.decode("7768617420646f2079612077616e7420666f72206e6f7468696e673f"); describe("HMAC-SHA-256", function () it("works", function() assert.equal("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", hmac.sha256(Key, Data, true)) end); end); describe("HMAC-SHA-512", function () it("works", function() assert.equal("164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737", hmac.sha512(Key, Data, true)) end); end); end); describe("Test case 3", function () local Key = hex.decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); local Data = hex.decode("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"); describe("HMAC-SHA-256", function () it("works", function() assert.equal("773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe", hmac.sha256(Key, Data, true)) end); end); describe("HMAC-SHA-512", function () it("works", function() assert.equal("fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb", hmac.sha512(Key, Data, true)) end); end); end); describe("Test case 4", function () local Key = hex.decode("0102030405060708090a0b0c0d0e0f10111213141516171819"); local Data = hex.decode("cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd"); describe("HMAC-SHA-256", function () it("works", function() assert.equal("82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b", hmac.sha256(Key, Data, true)) end); end); describe("HMAC-SHA-512", function () it("works", function() assert.equal("b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd", hmac.sha512(Key, Data, true)) end); end); end); describe("Test case 5", function () local Key = hex.decode("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c"); local Data = hex.decode("546573742057697468205472756e636174696f6e"); describe("HMAC-SHA-256", function () it("works", function() assert.equal("a3b6167473100ee06e0c796c2955552b", hmac.sha256(Key, Data, true):sub(1,128/4)) end); end); describe("HMAC-SHA-512", function () it("works", function() assert.equal("415fad6271580a531d4179bc891d87a6", hmac.sha512(Key, Data, true):sub(1,128/4)) end); end); end); describe("Test case 6", function () local Key = hex.decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); local Data = hex.decode("54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374"); describe("HMAC-SHA-256", function () it("works", function() assert.equal("60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54", hmac.sha256(Key, Data, true)) end); end); describe("HMAC-SHA-512", function () it("works", function() assert.equal("80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598", hmac.sha512(Key, Data, true)) end); end); end); describe("Test case 7", function () local Key = hex.decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); local Data = hex.decode("5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e"); describe("HMAC-SHA-256", function () it("works", function() assert.equal("9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2", hmac.sha256(Key, Data, true)) end); end); describe("HMAC-SHA-512", function () it("works", function() assert.equal("e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58", hmac.sha512(Key, Data, true)) end); end); end); prosody-13.0.1/spec/PaxHeaders/util_http_spec.lua0000644000000000000000000000011714773555365017063 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.747711157 prosody-13.0.1/spec/util_http_spec.lua0000644000175000017500000001115514773555365021265 0ustar00prosodyprosody00000000000000 local http = require "util.http"; describe("util.http", function() describe("#urlencode()", function() it("should not change normal characters", function() assert.are.equal(http.urlencode("helloworld123"), "helloworld123"); end); it("should escape spaces", function() assert.are.equal(http.urlencode("hello world"), "hello%20world"); end); it("should escape important URL characters", function() assert.are.equal(http.urlencode("This & that = something"), "This%20%26%20that%20%3d%20something"); end); end); describe("#urldecode()", function() it("should not change normal characters", function() assert.are.equal("helloworld123", http.urldecode("helloworld123"), "Normal characters not escaped"); end); it("should decode spaces", function() assert.are.equal("hello world", http.urldecode("hello%20world"), "Spaces escaped"); end); it("should decode important URL characters", function() assert.are.equal("This & that = something", http.urldecode("This%20%26%20that%20%3d%20something"), "Important URL chars escaped"); end); it("should decode both lower and uppercase", function () assert.are.equal("This & that = {something}.", http.urldecode("This%20%26%20that%20%3D%20%7Bsomething%7D%2E"), "Important URL chars escaped"); end); end); describe("#formencode()", function() it("should encode basic data", function() assert.are.equal(http.formencode({ { name = "one", value = "1"}, { name = "two", value = "2" } }), "one=1&two=2", "Form encoded"); 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); describe("#formdecode()", function() it("should decode basic data", function() local t = http.formdecode("one=1&two=2"); assert.are.same(t, { { name = "one", value = "1" }; { name = "two", value = "2" }; one = "1"; two = "2"; }); end); it("should decode special characters", function() local t = http.formdecode("one+two=1&two+one%26=2"); assert.are.same(t, { { name = "one two", value = "1" }; { name = "two one&", value = "2" }; ["one two"] = "1"; ["two one&"] = "2"; }); end); end); describe("normalize_path", function () it("root path is always '/'", function () assert.equal("/", http.normalize_path("/")); assert.equal("/", http.normalize_path("")); assert.equal("/", http.normalize_path("/", true)); assert.equal("/", http.normalize_path("", true)); end); it("works", function () assert.equal("/foo", http.normalize_path("foo")); assert.equal("/foo", http.normalize_path("/foo")); assert.equal("/foo", http.normalize_path("foo/")); assert.equal("/foo", http.normalize_path("/foo/")); end); it("is_dir works", function () assert.equal("/foo/", http.normalize_path("foo", true)); assert.equal("/foo/", http.normalize_path("/foo", true)); assert.equal("/foo/", http.normalize_path("foo/", true)); assert.equal("/foo/", http.normalize_path("/foo/", true)); end); end); describe("contains_token", function () it("is present in field", function () assert.is_true(http.contains_token("foo", "foo")); assert.is_true(http.contains_token("foo, bar", "foo")); assert.is_true(http.contains_token("foo,bar", "foo")); assert.is_true(http.contains_token("bar, foo,baz", "foo")); end); it("is absent from field", function () assert.is_false(http.contains_token("bar", "foo")); assert.is_false(http.contains_token("fooo", "foo")); assert.is_false(http.contains_token("foo o,bar", "foo")); end); it("is weird", 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); prosody-13.0.1/spec/PaxHeaders/util_human_io_spec.lua0000644000000000000000000000011714773555365017703 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.747711157 prosody-13.0.1/spec/util_human_io_spec.lua0000644000175000017500000000601614773555365022105 0ustar00prosodyprosody00000000000000describe("util.human.io", function () local human_io setup(function () human_io = require "util.human.io"; end); describe("table", function () it("alignment works", function () local row = human_io.table({ { width = 3, align = "right" }, { width = 3, }, }); assert.equal(" 1 | . ", row({ 1, "." })); assert.equal(" 10 | .. ", row({ 10, ".." })); assert.equal("100 | ...", row({ 100, "..." })); assert.equal("10… | ..…", row({ 1000, "...." })); end); end); describe("ellipsis", function() it("works", function() assert.equal("…", human_io.ellipsis("abc", 1)); assert.equal("a…", human_io.ellipsis("abc", 2)); assert.equal("abc", human_io.ellipsis("abc", 3)); assert.equal("…", human_io.ellipsis("räksmörgås", 1)); assert.equal("r…", human_io.ellipsis("räksmörgås", 2)); assert.equal("rä…", human_io.ellipsis("räksmörgås", 3)); assert.equal("räk…", human_io.ellipsis("räksmörgås", 4)); assert.equal("räks…", human_io.ellipsis("räksmörgås", 5)); assert.equal("räksm…", human_io.ellipsis("räksmörgås", 6)); assert.equal("räksmö…", human_io.ellipsis("räksmörgås", 7)); assert.equal("räksmör…", human_io.ellipsis("räksmörgås", 8)); assert.equal("räksmörg…", human_io.ellipsis("räksmörgås", 9)); 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); prosody-13.0.1/spec/PaxHeaders/util_human_units_spec.lua0000644000000000000000000000011714773555365020436 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.747711157 prosody-13.0.1/spec/util_human_units_spec.lua0000644000175000017500000000107614773555365022641 0ustar00prosodyprosody00000000000000local units = require "util.human.units"; describe("util.human.units", function () describe("format", function () it("formats numbers with SI units", function () assert.equal("1 km", units.format(1000, "m")); assert.equal("1 GJ", units.format(1000000000, "J")); assert.equal("1 ms", units.format(1/1000, "s")); assert.equal("10 ms", units.format(10/1000, "s")); assert.equal("1 ns", units.format(1/1000000000, "s")); assert.equal("1 KiB", units.format(1024, "B", 'b')); assert.equal("1 MiB", units.format(1024*1024, "B", 'b')); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_indexedbheap_spec.lua0000644000000000000000000000011714773555365020524 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.747711157 prosody-13.0.1/spec/util_indexedbheap_spec.lua0000644000175000017500000000157514773555365022733 0ustar00prosodyprosody00000000000000local ibh = require"util.indexedbheap"; local function verify_heap_property(priorities) for k in ipairs(priorities) do local parent = priorities[k]; local childA = priorities[2*k]; local childB = priorities[2*k+1]; -- print("-", parent, childA, childB) assert(childA == nil or childA > parent, "heap property violated"); assert(childB == nil or childB > parent, "heap property violated"); end end local h setup(function () h = ibh.create(); end) describe("util.indexedbheap", function () it("item can be moved from end to top", function () verify_heap_property(h); h:insert("a", 1); verify_heap_property(h); h:insert("b", 2); verify_heap_property(h); h:insert("c", 3); verify_heap_property(h); local id = h:insert("*", 10); verify_heap_property(h); h:reprioritize(id, 0); verify_heap_property(h); assert.same({ 0, "*", id }, { h:pop() }); end) end); prosody-13.0.1/spec/PaxHeaders/util_interpolation_spec.lua0000644000000000000000000000011714773555365020773 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.747711157 prosody-13.0.1/spec/util_interpolation_spec.lua0000644000175000017500000000345714773555365023203 0ustar00prosodyprosody00000000000000local template = [[ {greet!?Hi}, {name?world}! ]]; local expect1 = [[ Hello, WORLD! ]]; local expect2 = [[ Hello, world! ]]; local expect3 = [[ Hi, YOU! ]]; local template_array = [[ {foo#{idx}. {item} }]] local expect_array = [[ 1. HELLO 2. WORLD ]] local template_func_pipe = [[ {foo|sort#{idx}. {item} }]] local expect_func_pipe = [[ 1. A 2. B 3. C 4. D ]] local template_map = [[ {foo%{idx}: {item!} }]] local expect_map = [[ FOO: bar ]] local template_not = [[ {thing~Thing is falsy}{thing&Thing is truthy} ]] local expect_not_true = [[ Thing is truthy ]] local expect_not_nil = [[ Thing is falsy ]] local expect_not_false = [[ Thing is falsy ]] describe("util.interpolation", function () it("renders", function () local render = require "util.interpolation".new("%b{}", string.upper, { sort = function (t) table.sort(t) return t end }); assert.equal(expect1, render(template, { greet = "Hello", name = "world" })); assert.equal(expect2, render(template, { greet = "Hello" })); assert.equal(expect3, render(template, { name = "you" })); assert.equal(expect_array, render(template_array, { foo = { "Hello", "World" } })); assert.equal(expect_func_pipe, render(template_func_pipe, { foo = { "c", "a", "d", "b", } })); -- assert.equal("", render(template_func_pipe, { foo = nil })); -- FIXME assert.equal(expect_map, render(template_map, { foo = { foo = "bar" } })); assert.equal(expect_not_true, render(template_not, { thing = true })); assert.equal(expect_not_nil, render(template_not, { thing = nil })); assert.equal(expect_not_false, render(template_not, { thing = false })); end); it("fixes #1623", function () local render = require "util.interpolation".new("%b{}", string.upper, { x = string.lower }); assert.equal("", render("{foo?}", { })) assert.equal("", render("{foo|x?}", { })) end); end); prosody-13.0.1/spec/PaxHeaders/util_ip_spec.lua0000644000000000000000000000011714773555365016514 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.747711157 prosody-13.0.1/spec/util_ip_spec.lua0000644000175000017500000001112514773555365020713 0ustar00prosodyprosody00000000000000 local ip = require "util.ip"; local new_ip = ip.new_ip; local match = ip.match; local parse_cidr = ip.parse_cidr; local commonPrefixLength = ip.commonPrefixLength; describe("util.ip", function() describe("#match()", function() it("should work", function() local _ = new_ip; local ip = _"10.20.30.40"; assert.are.equal(match(ip, _"10.0.0.0", 8), true); assert.are.equal(match(ip, _"10.0.0.0", 16), false); assert.are.equal(match(ip, _"10.0.0.0", 24), false); assert.are.equal(match(ip, _"10.0.0.0", 32), false); assert.are.equal(match(ip, _"10.20.0.0", 8), true); assert.are.equal(match(ip, _"10.20.0.0", 16), true); assert.are.equal(match(ip, _"10.20.0.0", 24), false); assert.are.equal(match(ip, _"10.20.0.0", 32), false); assert.are.equal(match(ip, _"0.0.0.0", 32), false); assert.are.equal(match(ip, _"0.0.0.0", 0), true); assert.are.equal(match(ip, _"0.0.0.0"), false); assert.are.equal(match(ip, _"10.0.0.0", 255), false, "excessive number of bits"); assert.are.equal(match(ip, _"10.0.0.0", -8), true, "negative number of bits"); assert.are.equal(match(ip, _"10.0.0.0", -32), true, "negative number of bits"); assert.are.equal(match(ip, _"10.0.0.0", 0), true, "zero bits"); assert.are.equal(match(ip, _"10.0.0.0"), false, "no specified number of bits (differing ip)"); assert.are.equal(match(ip, _"10.20.30.40"), true, "no specified number of bits (same ip)"); assert.are.equal(match(_"127.0.0.1", _"127.0.0.1"), true, "simple ip"); 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); describe("#parse_cidr()", function() it("should work", function() assert.are.equal(new_ip"0.0.0.0", new_ip"0.0.0.0") local function assert_cidr(cidr, ip, bits) local parsed_ip, parsed_bits = parse_cidr(cidr); assert.are.equal(new_ip(ip), parsed_ip, cidr.." parsed ip is "..ip); assert.are.equal(bits, parsed_bits, cidr.." parsed bits is "..tostring(bits)); end assert_cidr("0.0.0.0", "0.0.0.0", nil); assert_cidr("127.0.0.1", "127.0.0.1", nil); assert_cidr("127.0.0.1/0", "127.0.0.1", 0); assert_cidr("127.0.0.1/8", "127.0.0.1", 8); assert_cidr("127.0.0.1/32", "127.0.0.1", 32); assert_cidr("127.0.0.1/256", "127.0.0.1", 256); assert_cidr("::/48", "::", 48); end); end); describe("#new_ip()", function() it("should work", function() local v4, v6 = "IPv4", "IPv6"; local function assert_proto(s, proto) local ip = new_ip(s); if proto then assert.are.equal(ip and ip.proto, proto, "protocol is correct for "..("%q"):format(s)); else assert.are.equal(ip, nil, "address is invalid"); end end assert_proto("127.0.0.1", v4); assert_proto("::1", v6); assert_proto("", nil); assert_proto("abc", nil); assert_proto(" ", nil); end); end); describe("#commonPrefixLength()", function() it("should work", function() local function assert_cpl6(a, b, len, v4) local ipa, ipb = new_ip(a), new_ip(b); if v4 then len = len+96; end assert.are.equal(commonPrefixLength(ipa, ipb), len, "common prefix length of "..a.." and "..b.." is "..len); assert.are.equal(commonPrefixLength(ipb, ipa), len, "common prefix length of "..b.." and "..a.." is "..len); end local function assert_cpl4(a, b, len) return assert_cpl6(a, b, len, "IPv4"); end assert_cpl4("0.0.0.0", "0.0.0.0", 32); assert_cpl4("255.255.255.255", "0.0.0.0", 0); assert_cpl4("255.255.255.255", "255.255.0.0", 16); assert_cpl4("255.255.255.255", "255.255.255.255", 32); assert_cpl4("255.255.255.255", "255.255.255.255", 32); assert_cpl6("::1", "::1", 128); 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); prosody-13.0.1/spec/PaxHeaders/util_iterators_spec.lua0000644000000000000000000000011714773555365020120 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.747711157 prosody-13.0.1/spec/util_iterators_spec.lua0000644000175000017500000000300414773555365022314 0ustar00prosodyprosody00000000000000local iter = require "util.iterators"; describe("util.iterators", function () describe("join", function () it("should produce a joined iterator", function () local expect = { "a", "b", "c", 1, 2, 3 }; local output = {}; for x in iter.join(iter.values({"a", "b", "c"})):append(iter.values({1, 2, 3})) do table.insert(output, x); 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 () it("should produce sorted pairs", function () local orig = { b = 1, c = 2, a = "foo", d = false }; local n, last_key = 0, nil; for k, v in iter.sorted_pairs(orig) do n = n + 1; if last_key then assert(k > last_key, "Expected "..k.." > "..last_key) end assert.equal(orig[k], v); last_key = k; end assert.equal("d", last_key); assert.equal(4, n); end); it("should allow a custom sort function", function () local orig = { b = 1, c = 2, a = "foo", d = false }; local n, last_key = 0, nil; for k, v in iter.sorted_pairs(orig, function (a, b) return a > b end) do n = n + 1; if last_key then assert(k < last_key, "Expected "..k.." > "..last_key) end assert.equal(orig[k], v); last_key = k; end assert.equal("a", last_key); assert.equal(4, n); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_jid_spec.lua0000644000000000000000000000011714773555365016652 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.747711157 prosody-13.0.1/spec/util_jid_spec.lua0000644000175000017500000002332214773555365021053 0ustar00prosodyprosody00000000000000 local jid = require "util.jid"; describe("util.jid", function() describe("#join()", function() it("should work", function() assert.are.equal(jid.join("a", "b", "c"), "a@b/c", "builds full JID"); assert.are.equal(jid.join("a", "b", nil), "a@b", "builds bare JID"); assert.are.equal(jid.join(nil, "b", "c"), "b/c", "builds full host JID"); assert.are.equal(jid.join(nil, "b", nil), "b", "builds bare host JID"); assert.are.equal(jid.join(nil, nil, nil), nil, "invalid JID is nil"); assert.are.equal(jid.join("a", nil, nil), nil, "invalid JID is nil"); assert.are.equal(jid.join(nil, nil, "c"), nil, "invalid JID is nil"); assert.are.equal(jid.join("a", nil, "c"), nil, "invalid JID is nil"); end); it("should reject invalid arguments", function () assert.has_error(function () jid.join(false, "bork", nil) end) assert.has_error(function () jid.join(nil, "bork", false) end) assert.has_error(function () jid.join(false, false, false) end) end) end); describe("#split()", function() it("should work", function() local function test(input_jid, expected_node, expected_server, expected_resource) local rnode, rserver, rresource = jid.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 -- 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); end); it("should reject invalid arguments", function () assert.has_error(function () jid.split(false) end) end) end); describe("#prepped_split()", function() local function test(input_jid, expected_node, expected_server, expected_resource) local rnode, rserver, rresource = jid.prepped_split(input_jid); assert.are.equal(expected_node, rnode, "split("..tostring(input_jid)..") failed"); assert.are.equal(expected_server, rserver, "split("..tostring(input_jid)..") failed"); assert.are.equal(expected_resource, rresource, "split("..tostring(input_jid)..") failed"); end it("should work", function() -- Valid JIDs test("node@server", "node", "server", nil ); test("node@server/resource", "node", "server", "resource" ); test("server", nil, "server", nil ); test("server/resource", nil, "server", "resource" ); test("server/resource@foo", nil, "server", "resource@foo" ); test("server/resource@foo/bar", nil, "server", "resource@foo/bar"); -- Always invalid JIDs test(nil, nil, nil, nil); test("node@/server", nil, nil, nil); test("@server", nil, nil, nil); test("@server/resource", nil, nil, nil); test("@/resource", nil, nil, nil); test("@server/", nil, nil, nil); test("server/", nil, nil, nil); test("/resource", nil, nil, nil); end); it("should reject invalid arguments", function () assert.has_error(function () jid.prepped_split(false) end) end) it("should strip empty root label", function () test("node@server.", "node", "server", nil); end); it("should fail for JIDs that fail stringprep", function () test("node@invalid-\128-server", nil, nil, nil); test("node@invalid-\194\128-server", nil, nil, nil); test("@server", nil, nil, nil); test("node@server/invalid-\000-resource", nil, nil, nil); end); end); describe("#bare()", function() it("should work", function() assert.are.equal(jid.bare("user@host"), "user@host", "bare JID remains bare"); assert.are.equal(jid.bare("host"), "host", "Host JID remains host"); assert.are.equal(jid.bare("host/resource"), "host", "Host JID with resource becomes host"); assert.are.equal(jid.bare("user@host/resource"), "user@host", "user@host JID with resource becomes user@host"); assert.are.equal(jid.bare("user@/resource"), nil, "invalid JID is nil"); assert.are.equal(jid.bare("@/resource"), nil, "invalid JID is nil"); assert.are.equal(jid.bare("@/"), nil, "invalid JID is nil"); assert.are.equal(jid.bare("/"), nil, "invalid JID is nil"); assert.are.equal(jid.bare(""), nil, "invalid JID is nil"); assert.are.equal(jid.bare("@"), nil, "invalid JID is nil"); assert.are.equal(jid.bare("user@"), nil, "invalid JID is nil"); assert.are.equal(jid.bare("user@@"), nil, "invalid JID is nil"); assert.are.equal(jid.bare("user@@host"), nil, "invalid JID is nil"); assert.are.equal(jid.bare("user@@host/resource"), nil, "invalid JID is nil"); assert.are.equal(jid.bare("user@host/"), nil, "invalid JID is nil"); end); it("should reject invalid arguments", function () assert.has_error(function () jid.bare(false) end) end) end); describe("#compare()", function() it("should work", function() assert.are.equal(jid.compare("host", "host"), true, "host should match"); assert.are.equal(jid.compare("host", "other-host"), false, "host should not match"); assert.are.equal(jid.compare("other-user@host/resource", "host"), true, "host should match"); assert.are.equal(jid.compare("other-user@host", "user@host"), false, "user should not match"); assert.are.equal(jid.compare("user@host", "host"), true, "host should match"); assert.are.equal(jid.compare("user@host/resource", "host"), true, "host should match"); assert.are.equal(jid.compare("user@host/resource", "user@host"), true, "user and host should match"); assert.are.equal(jid.compare("user@other-host", "host"), false, "host should not match"); assert.are.equal(jid.compare("user@other-host", "user@host"), false, "host should not match"); end); end); local jid_escaping_test_vectors = { -- From https://xmpp.org/extensions/xep-0106.xml#examples sans @example.com [[space cadet]], [[space\20cadet]], [[call me "ishmael"]], [[call\20me\20\22ishmael\22]], [[at&t guy]], [[at\26t\20guy]], [[d'artagnan]], [[d\27artagnan]], [[/.fanboy]], [[\2f.fanboy]], [[::foo::]], [[\3a\3afoo\3a\3a]], [[]], [[\3cfoo\3e]], [[user@host]], [[user\40host]], [[c:\net]], [[c\3a\net]], [[c:\\net]], [[c\3a\\net]], [[c:\cool stuff]], [[c\3a\cool\20stuff]], [[c:\5commas]], [[c\3a\5c5commas]], -- Section 4.2 [[\3and\2is\5cool]], [[\5c3and\2is\5c5cool]], -- From aioxmpp [[\5c]], [[\5c5c]], -- [[\5C]], [[\5C]], [[\2plus\2is\4]], [[\2plus\2is\4]], [[foo\bar]], [[foo\bar]], [[foo\41r]], [[foo\41r]], -- additional test vectors [[call\20me]], [[call\5c20me]], }; describe("#escape()", function () it("should work", function () for i = 1, #jid_escaping_test_vectors, 2 do local original = jid_escaping_test_vectors[i]; local escaped = jid_escaping_test_vectors[i+1]; assert.are.equal(escaped, jid.escape(original), ("Escapes '%s' -> '%s'"):format(original, escaped)); end end); end) describe("#unescape()", function () it("should work", function () for i = 1, #jid_escaping_test_vectors, 2 do local original = jid_escaping_test_vectors[i]; local escaped = jid_escaping_test_vectors[i+1]; assert.are.equal(original, jid.unescape(escaped), ("Unescapes '%s' -> '%s'"):format(escaped, original)); end end); end) it("should work with nodes", function() local function test(_jid, expected_node) assert.are.equal(jid.node(_jid), expected_node, "Unexpected node for "..tostring(_jid)); end test("example.com", nil); test("foo.example.com", nil); test("foo.example.com/resource", nil); test("foo.example.com/some resource", nil); test("foo.example.com/some@resource", nil); test("foo@foo.example.com/some@resource", "foo"); test("foo@example/some@resource", "foo"); test("foo@example/@resource", "foo"); test("foo@example@resource", nil); test("foo@example", "foo"); test("foo", nil); test(nil, nil); end); it("should work with hosts", function() local function test(_jid, expected_host) assert.are.equal(jid.host(_jid), expected_host, "Unexpected host for "..tostring(_jid)); end test("example.com", "example.com"); test("foo.example.com", "foo.example.com"); test("foo.example.com/resource", "foo.example.com"); test("foo.example.com/some resource", "foo.example.com"); test("foo.example.com/some@resource", "foo.example.com"); test("foo@foo.example.com/some@resource", "foo.example.com"); test("foo@example/some@resource", "example"); test("foo@example/@resource", "example"); test("foo@example@resource", nil); test("foo@example", "example"); test("foo", "foo"); test(nil, nil); end); it("should work with resources", function() local function test(_jid, expected_resource) assert.are.equal(jid.resource(_jid), expected_resource, "Unexpected resource for "..tostring(_jid)); end test("example.com", nil); test("foo.example.com", nil); test("foo.example.com/resource", "resource"); test("foo.example.com/some resource", "some resource"); test("foo.example.com/some@resource", "some@resource"); test("foo@foo.example.com/some@resource", "some@resource"); test("foo@example/some@resource", "some@resource"); test("foo@example/@resource", "@resource"); test("foo@example@resource", nil); test("foo@example", nil); test("foo", nil); test("/foo", nil); test("@x/foo", nil); test("@/foo", nil); test(nil, nil); end); end); prosody-13.0.1/spec/PaxHeaders/util_json_spec.lua0000644000000000000000000000011714773555365017055 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.747711157 prosody-13.0.1/spec/util_json_spec.lua0000644000175000017500000000420214773555365021252 0ustar00prosodyprosody00000000000000 local json = require "util.json"; local array = require "util.array"; describe("util.json", function() describe("#encode()", function() it("should work", function() local function test(f, j, e) if e then assert.are.equal(f(j), e); end assert.are.equal(f(j), f(json.decode(f(j)))); end test(json.encode, json.null, "null") test(json.encode, {}, "{}") test(json.encode, {a=1}); test(json.encode, {a={1,2,3}}); test(json.encode, {1}, "[1]"); end); end); describe("#decode()", function() it("should work", function() local empty_array = json.decode("[]"); assert.are.equal(type(empty_array), "table"); assert.are.equal(#empty_array, 0); assert.are.equal(next(empty_array), nil); end); end); describe("testcases", function() local valid_data = {}; local invalid_data = {}; local skip = "fail1.json fail9.json fail18.json fail15.json fail13.json fail25.json fail26.json fail27.json fail28.json fail17.json pass1.json"; setup(function() local lfs = require "lfs"; local path = "spec/json"; for name in lfs.dir(path) do if name:match("%.json$") then local f = assert(io.open(path.."/"..name)); local content = assert(f:read("*a")); assert(f:close()); if skip:find(name) then --luacheck: ignore 542 -- Skip elseif name:match("^pass") then valid_data[name] = content; elseif name:match("^fail") then invalid_data[name] = content; end end end end) it("should pass valid testcases", function() for name, content in pairs(valid_data) do local parsed, err = json.decode(content); assert(parsed, name..": "..tostring(err)); end end); it("should fail invalid testcases", function() for name, content in pairs(invalid_data) do local parsed, err = json.decode(content); assert(not parsed, name..": "..tostring(err)); end end); end) describe("util.array integration", function () it("works", function () assert.equal("[]", json.encode(array())); assert.equal("[1,2,3]", json.encode(array({1,2,3}))); assert.equal(getmetatable(array()), getmetatable(json.decode("[]"))); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_jsonpointer_spec.lua0000644000000000000000000000011714773555365020456 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.751711172 prosody-13.0.1/spec/util_jsonpointer_spec.lua0000644000175000017500000000220014773555365022647 0ustar00prosodyprosody00000000000000describe("util.jsonpointer", function() local json, jp; setup(function() json = require "util.json"; jp = require "util.jsonpointer"; end) describe("resolve()", function() local example; setup(function() example = json.decode([[{ "foo": ["bar", "baz"], "": 0, "a/b": 1, "c%d": 2, "e^f": 3, "g|h": 4, "i\\j": 5, "k\"l": 6, " ": 7, "m~n": 8 }]]) 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")); assert.same(3, jp.resolve(example, "/e^f")); assert.same(4, jp.resolve(example, "/g|h")); assert.same(5, jp.resolve(example, "/i\\j")); assert.same(6, jp.resolve(example, "/k\"l")); assert.same(7, jp.resolve(example, "/ ")); assert.same(8, jp.resolve(example, "/m~0n")); end) end) end) prosody-13.0.1/spec/PaxHeaders/util_jsonschema_spec.lua0000644000000000000000000000011714773555365020236 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.751711172 prosody-13.0.1/spec/util_jsonschema_spec.lua0000644000175000017500000001021414773555365022433 0ustar00prosodyprosody00000000000000local 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-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 -- Tests to skip and short reason why (NYI = not yet implemented) local skip = { ["additionalProperties.json:0:2"] = "distinguishing objects from arrays", ["additionalProperties.json:0:5"] = "NYI", ["additionalProperties.json:1:0"] = "NYI", ["anchor.json"] = "$anchor NYI", ["const.json:1"] = "deepcompare", ["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", ["dependentSchemas.json:2:2"] = "NYI", -- minProperties ["dynamicRef.json"] = "NYI", ["enum.json:1:3"] = "deepcompare", ["id.json"] = "NYI", ["maxProperties.json"] = "NYI", ["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"] = "util.jsonpointer recursive issue?", ["ref.json:11"] = "NYI", ["ref.json:12:1"] = "FIXME", ["ref.json:13"] = "NYI", ["ref.json:14"] = "NYI", ["ref.json:15"] = "NYI", ["ref.json:16"] = "NYI", ["ref.json:17"] = "NYI", ["ref.json:18"] = "NYI", ["ref.json:19"] = "NYI", ["ref.json:26"] = "NYI", ["ref.json:27"] = "NYI", ["ref.json:28"] = "NYI", ["ref.json:3:2"] = "FIXME investigate, util.jsonpath issue?", ["required.json:4"] = "JavaScript specific and distinguishing objects from arrays", ["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", ["type.json:4:6"] = "null is weird", ["type.json:9:4"] = "null is weird", ["type.json:9:6"] = "null is weird", ["unevaluatedItems.json"] = "NYI", ["unevaluatedProperties.json"] = "NYI", ["uniqueItems.json:0:10"] = "deepcompare", ["uniqueItems.json:0:12"] = "deepcompare", ["uniqueItems.json:0:14"] = "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", }; local function label(s, i) return string.format("%s:%d", s, i-1); end describe("util.jsonschema.validate", function() for test_case_file in lfs.dir(test_suite_dir) do -- print(skip[test_case_file] and "do " or "skip", test_case_file) if test_case_file:sub(-5) == ".json" and not skip[test_case_file] then describe(test_case_file, function() local test_cases; setup(function() local f = assert(io.open(test_suite_dir .. "/" .. test_case_file)); local rawdata = assert(f:read("*a"), "failed to read " .. test_case_file) test_cases = assert(json.decode(rawdata), "failed to parse " .. test_case_file) end) describe("tests", function() for i, schema_test in ipairs(test_cases) do local generic_label = label(test_case_file, i); describe(schema_test.description or generic_label, function() for j, test in ipairs(schema_test.tests) do local specific_label = label(generic_label, j); ((skip[generic_label] or skip[specific_label]) and pending or it)(test.description, function() assert.equal(test.valid, js.validate(schema_test.schema, test.data), specific_label .. " " .. test.description); end) end end) end end) end) end end end); prosody-13.0.1/spec/PaxHeaders/util_jwt_spec.lua0000644000000000000000000000011714773555365016710 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.751711172 prosody-13.0.1/spec/util_jwt_spec.lua0000644000175000017500000002336414773555365021117 0ustar00prosodyprosody00000000000000local 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 () local key = "secret"; local token = jwt.sign(key, { payload = "this" }); assert.string(token); local ok, parsed = jwt.verify(key, token); assert.truthy(ok) assert.same({ payload = "this" }, parsed); end); it("rejects invalid", function () local key = "secret"; local token = jwt.sign("wrong", { payload = "this" }); assert.string(token); 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); prosody-13.0.1/spec/PaxHeaders/util_multitable_spec.lua0000644000000000000000000000011714773555365020246 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.751711172 prosody-13.0.1/spec/util_multitable_spec.lua0000644000175000017500000000341314773555365022446 0ustar00prosodyprosody00000000000000 local multitable = require "util.multitable"; describe("util.multitable", function() describe("#new()", function() it("should create a multitable", function() local mt = multitable.new(); assert.is_table(mt, "Multitable is a table"); assert.is_function(mt.add, "Multitable has method add"); assert.is_function(mt.get, "Multitable has method get"); assert.is_function(mt.remove, "Multitable has method remove"); end); end); describe("#get()", function() it("should allow getting correctly", function() local function has_items(list, ...) local should_have = {}; if select('#', ...) > 0 then assert.is_table(list, "has_items: list is table", 3); else assert.is.falsy(list and #list > 0, "No items, and no list"); return true, "has-all"; end for n=1,select('#', ...) do should_have[select(n, ...)] = true; end for _, item in ipairs(list) do if not should_have[item] then return false, "too-many"; end should_have[item] = nil; end if next(should_have) then return false, "not-enough"; end return true, "has-all"; end local function assert_has_all(message, list, ...) return assert.are.equal(select(2, has_items(list, ...)), "has-all", message or "List has all expected items, and no more", 2); end local mt = multitable.new(); local trigger1, trigger2, trigger3 = {}, {}, {}; local item1, item2, item3 = {}, {}, {}; assert_has_all("Has no items with trigger1", mt:get(trigger1)); mt:add(1, 2, 3, item1); assert_has_all("Has item1 for 1, 2, 3", mt:get(1, 2, 3), item1); end); end); -- Doesn't support nil --[[ mt:add(nil, item1); mt:add(nil, item2); mt:add(nil, item3); assert_has_all("Has all items with (nil)", mt:get(nil), item1, item2, item3); ]] end); prosody-13.0.1/spec/PaxHeaders/util_paseto_spec.lua0000644000000000000000000000011714773555365017377 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.751711172 prosody-13.0.1/spec/util_paseto_spec.lua0000644000175000017500000003734314773555365021610 0ustar00prosodyprosody00000000000000-- 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); prosody-13.0.1/spec/PaxHeaders/util_paths_spec.lua0000644000000000000000000000011714773555365017223 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.751711172 prosody-13.0.1/spec/util_paths_spec.lua0000644000175000017500000000242114773555365021421 0ustar00prosodyprosody00000000000000local sep = package.config:match("(.)\n"); describe("util.paths", function () local paths = require "util.paths"; describe("#join()", function () it("returns single component as-is", function () assert.equal("foo", paths.join("foo")); end); it("joins paths", function () assert.equal("foo"..sep.."bar", paths.join("foo", "bar")) end); it("joins longer paths", function () assert.equal("foo"..sep.."bar"..sep.."baz", paths.join("foo", "bar", "baz")) end); it("joins even longer paths", function () assert.equal("foo"..sep.."bar"..sep.."baz"..sep.."moo", paths.join("foo", "bar", "baz", "moo")) end); end) describe("#glob_to_pattern()", function () it("works", function () assert.equal("^thing.%..*$", paths.glob_to_pattern("thing?.*")) end); end) describe("#resolve_relative_path()", function () it("returns absolute paths as-is", function () if sep == "/" then assert.equal("/tmp/path", paths.resolve_relative_path("/run", "/tmp/path")); elseif sep == "\\" then assert.equal("C:\\Program Files", paths.resolve_relative_path("A:\\", "C:\\Program Files")); end end); it("resolves relative paths", function () if sep == "/" then assert.equal("/run/path", paths.resolve_relative_path("/run", "path")); end end); end) end) prosody-13.0.1/spec/PaxHeaders/util_poll_spec.lua0000644000000000000000000000011714773555365017052 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.751711172 prosody-13.0.1/spec/util_poll_spec.lua0000644000175000017500000000135714773555365021257 0ustar00prosodyprosody00000000000000describe("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); prosody-13.0.1/spec/PaxHeaders/util_promise_spec.lua0000644000000000000000000000011714773555365017562 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.751711172 prosody-13.0.1/spec/util_promise_spec.lua0000644000175000017500000005124614773555365021771 0ustar00prosodyprosody00000000000000local promise = require "util.promise"; describe("util.promise", function () --luacheck: ignore 212/resolve 212/reject describe("new()", function () it("returns a promise object", 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"); end); local cb = spy.new(function (v) assert.equal("foo", v); end); p:next(cb); assert.spy(cb).was_called(1); end); it("notifies on fulfillment of pending promises", function () local r; local p = promise.new(function (resolve) r = resolve; end); local cb = spy.new(function (v) assert.equal("foo", v); end); p:next(cb); assert.spy(cb).was_called(0); 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; local p = promise.new(function (resolve) r = resolve; end); local cb1 = spy.new(function (v) assert.equal("foo", v); return "bar"; end); local cb2 = spy.new(function (v) assert.equal("bar", v); result = v; end); p:next(cb1):next(cb2); assert.spy(cb1).was_called(0); assert.spy(cb2).was_called(0); r("foo"); assert.spy(cb1).was_called(1); assert.spy(cb2).was_called(1); assert.equal("bar", result); end); it("supports multiple :next() calls on the same promise", function () local r; local result; local p = promise.new(function (resolve) r = resolve; end); local cb1 = spy.new(function (v) assert.equal("foo", v); result = v; end); local cb2 = spy.new(function (v) assert.equal("foo", v); result = v; end); p:next(cb1); p:next(cb2); assert.spy(cb1).was_called(0); assert.spy(cb2).was_called(0); r("foo"); assert.spy(cb1).was_called(1); assert.spy(cb2).was_called(1); assert.equal("foo", result); end); it("automatically rejects on error", function () local r; local p = promise.new(function (resolve) r = resolve; error("oh no"); end); local cb = spy.new(function () end); local err_cb = spy.new(function (v) assert.equal("oh no", v); end); p:next(cb, err_cb); assert.spy(cb).was_called(0); assert.spy(err_cb).was_called(1); r("foo"); assert.spy(cb).was_called(0); assert.spy(err_cb).was_called(1); end); it("supports reject()", function () local r, result; local p = promise.new(function (resolve, reject) r = reject; end); local cb = spy.new(function () end); local err_cb = spy.new(function (v) result = v; assert.equal("oh doh", v); end); p:next(cb, err_cb); assert.spy(cb).was_called(0); assert.spy(err_cb).was_called(0); r("oh doh"); assert.spy(cb).was_called(0); assert.spy(err_cb).was_called(1); assert.equal("oh doh", result); end); it("supports chaining of rejected promises", function () local r, result; local p = promise.new(function (resolve, reject) r = reject; end); local cb = spy.new(function () end); local err_cb = spy.new(function (v) result = v; assert.equal("oh doh", v); return "ok" end); local cb2 = spy.new(function (v) result = v; end); local err_cb2 = spy.new(function () end); p:next(cb, err_cb):next(cb2, err_cb2) assert.spy(cb).was_called(0); assert.spy(err_cb).was_called(0); assert.spy(cb2).was_called(0); assert.spy(err_cb2).was_called(0); r("oh doh"); assert.spy(cb).was_called(0); assert.spy(err_cb).was_called(1); assert.spy(cb2).was_called(1); assert.spy(err_cb2).was_called(0); assert.equal("ok", result); end); it("propagates errors down the chain, even when some handlers are not provided", function () local r, result; local test_error = {}; local p = promise.new(function (resolve, reject) r = reject; end); local cb = spy.new(function () end); local err_cb = spy.new(function (e) result = e end); local p2 = p:next(function () error(test_error) end); local p3 = p2:next(cb) p3:catch(err_cb); assert.spy(cb).was_called(0); assert.spy(err_cb).was_called(0); r("oh doh"); assert.spy(cb).was_called(0); assert.spy(err_cb).was_called(1); assert.spy(err_cb).was_called_with("oh doh"); assert.equal("oh doh", result); end); it("propagates values down the chain, even when some handlers are not provided", function () local r; local p = promise.new(function (resolve, reject) r = resolve; end); local cb = spy.new(function () end); local err_cb = spy.new(function () end); local p2 = p:next(function (v) return v; end); local p3 = p2:catch(err_cb) p3:next(cb); assert.spy(cb).was_called(0); assert.spy(err_cb).was_called(0); r(1337); assert.spy(cb).was_called(1); assert.spy(cb).was_called_with(1337); assert.spy(err_cb).was_called(0); end); it("fulfilled promises do not call error handlers and do propagate value", function () local p = promise.resolve("foo"); local cb = spy.new(function () end); local p2 = p:catch(cb); assert.spy(cb).was_called(0); local cb2 = spy.new(function () end); p2:catch(cb2); assert.spy(cb2).was_called(0); end); it("rejected promises do not call fulfilled handlers and do propagate reason", function () local p = promise.reject("foo"); local cb = spy.new(function () end); local p2 = p:next(cb); assert.spy(cb).was_called(0); local cb2 = spy.new(function () end); local cb2_err = spy.new(function () end); p2:next(cb2, cb2_err); assert.spy(cb2).was_called(0); assert.spy(cb2_err).was_called(1); assert.spy(cb2_err).was_called_with("foo"); end); describe("allows callbacks to return", function () it("pending promises", function () local r; local p = promise.resolve() local cb = spy.new(function () return promise.new(function (resolve) r = resolve; end); end); local cb2 = spy.new(function () end); p:next(cb):next(cb2); assert.spy(cb).was_called(1); assert.spy(cb2).was_called(0); r("hello"); assert.spy(cb).was_called(1); assert.spy(cb2).was_called(1); assert.spy(cb2).was_called_with("hello"); end); it("resolved promises", function () local p = promise.resolve() local cb = spy.new(function () return promise.resolve("hello"); end); local cb2 = spy.new(function () end); p:next(cb):next(cb2); assert.spy(cb).was_called(1); assert.spy(cb2).was_called(1); assert.spy(cb2).was_called_with("hello"); end); it("rejected promises", function () local p = promise.resolve() local cb = spy.new(function () return promise.reject("hello"); end); local cb2 = spy.new(function () return promise.reject("goodbye"); end); local cb3 = spy.new(function () end); p:next(cb):catch(cb2):catch(cb3); assert.spy(cb).was_called(1); assert.spy(cb2).was_called(1); assert.spy(cb2).was_called_with("hello"); assert.spy(cb3).was_called(1); assert.spy(cb3).was_called_with("goodbye"); end); it("ordinary values", function () local p = promise.resolve() local cb = spy.new(function () return "hello" end); local cb2 = spy.new(function () end); p:next(cb):next(cb2); assert.spy(cb).was_called(1); assert.spy(cb2).was_called(1); assert.spy(cb2).was_called_with("hello"); end); it("nil", function () local p = promise.resolve() local cb = spy.new(function () return end); local cb2 = spy.new(function () end); p:next(cb):next(cb2); assert.spy(cb).was_called(1); assert.spy(cb2).was_called(1); assert.spy(cb2).was_called_with(nil); end); end); describe("race()", function () it("works with fulfilled promises", function () local p1, p2 = promise.resolve("yep"), promise.resolve("nope"); local p = promise.race({ p1, p2 }); local result; p:next(function (v) result = v; end); assert.equal("yep", result); end); it("works with pending promises", function () local r1, r2; local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end); local p = promise.race({ p1, p2 }); local result; local cb = spy.new(function (v) result = v; end); p:next(cb); assert.spy(cb).was_called(0); r2("yep"); r1("nope"); assert.spy(cb).was_called(1); assert.equal("yep", result); end); end); describe("all()", function () it("works with fulfilled promises", function () local p1, p2 = promise.resolve("yep"), promise.resolve("nope"); local p = promise.all({ p1, p2 }); local result; p:next(function (v) result = v; end); assert.same({ "yep", "nope" }, result); end); it("works with pending promises", function () local r1, r2; local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end); local p = promise.all({ p1, p2 }); local result; local cb = spy.new(function (v) result = v; end); p:next(cb); assert.spy(cb).was_called(0); r2("yep"); assert.spy(cb).was_called(0); r1("nope"); assert.spy(cb).was_called(1); assert.same({ "nope", "yep" }, result); end); it("rejects if any promise rejects", function () local r1, r2; local p1 = promise.new(function (resolve, reject) r1 = reject end); local p2 = promise.new(function (resolve, reject) r2 = reject end); local p = promise.all({ p1, p2 }); local result; local cb = spy.new(function (v) result = v; end); local cb_err = spy.new(function (v) result = v; end); p:next(cb, cb_err); assert.spy(cb).was_called(0); assert.spy(cb_err).was_called(0); r2("fail"); assert.spy(cb).was_called(0); assert.spy(cb_err).was_called(1); r1("nope"); assert.spy(cb).was_called(0); assert.spy(cb_err).was_called(1); assert.equal("fail", 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); local p = promise.all({ [true] = p1, [false] = p2 }); local result; local cb = spy.new(function (v) result = v; end); p:next(cb); assert.spy(cb).was_called(0); r2("yep"); assert.spy(cb).was_called(0); r1("nope"); assert.spy(cb).was_called(1); assert.same({ [true] = "nope", [false] = "yep" }, result); end); it("passes through non-promise values", function () local r1; local p1 = promise.new(function (resolve) r1 = resolve end); local p = promise.all({ [true] = p1, [false] = "yep" }); local result; local cb = spy.new(function (v) result = v; end); p:next(cb); assert.spy(cb).was_called(0); r1("nope"); assert.spy(cb).was_called(1); assert.same({ [true] = "nope", [false] = "yep" }, result); end); end); describe("all_settled()", function () it("works with fulfilled promises", function () local p1, p2 = promise.resolve("yep"), promise.resolve("nope"); local p = promise.all_settled({ p1, p2 }); local result; p:next(function (v) result = v; end); assert.same({ { status = "fulfilled", value = "yep" }; { status = "fulfilled", value = "nope" }; }, result); end); it("works with pending promises", function () local r1, r2; local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve 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("yep"); assert.spy(cb).was_called(0); r1("nope"); assert.spy(cb).was_called(1); assert.same({ { status = "fulfilled", value = "nope" }; { status = "fulfilled", value = "yep" }; }, result); end); it("works when some promises reject", function () local r1, r2; local p1, p2 = promise.new(function (resolve) r1 = resolve 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 succeeds"); assert.spy(cb).was_called(1); assert.same({ { status = "fulfilled", value = "this succeeds" }; { 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); local p = promise.all_settled({ foo = p1, bar = p2 }); local result; local cb = spy.new(function (v) result = v; end); p:next(cb); assert.spy(cb).was_called(0); r2("yep"); assert.spy(cb).was_called(0); r1("nope"); assert.spy(cb).was_called(1); assert.same({ foo = { status = "fulfilled", value = "nope" }; bar = { status = "fulfilled", value = "yep" }; }, result); end); it("passes through non-promise values", function () local r1; local p1 = promise.new(function (resolve) r1 = resolve end); local p = promise.all_settled({ foo = p1, bar = "yep" }); local result; local cb = spy.new(function (v) result = v; end); p:next(cb); assert.spy(cb).was_called(0); r1("nope"); assert.spy(cb).was_called(1); assert.same({ foo = { status = "fulfilled", value = "nope" }; bar = "yep"; }, result); end); end); describe("catch()", function () it("works", function () local result; local p = promise.new(function (resolve) error({ foo = true }); end); local cb1 = spy.new(function (v) result = v; end); assert.spy(cb1).was_called(0); p:catch(cb1); assert.spy(cb1).was_called(1); assert.same({ foo = true }, result); end); end); describe("join()", function () it("works", function () local r1, r2; local res1, res2; local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end); local p = promise.join(function (_res1, _res2) res1, res2 = _res1, _res2; return promise.resolve("works"); end, p1, p2); local result; local cb = spy.new(function (v) result = v; end); p:next(cb); assert.spy(cb).was_called(0); r2("yep"); assert.spy(cb).was_called(0); r1("nope"); assert.spy(cb).was_called(1); assert.same("works", result); assert.equals("nope", res1); assert.equals("yep", res2); end); end); it("promises may be resolved by other promises", function () local r1, r2; local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end); local result; local cb = spy.new(function (v) result = v; end); p1:next(cb); assert.spy(cb).was_called(0); r1(p2); assert.spy(cb).was_called(0); r2("yep"); assert.spy(cb).was_called(1); assert.equal("yep", result); end); describe("reject()", function () it("returns a rejected promise", function () local p = promise.reject("foo"); local cb = spy.new(function () end); p:catch(cb); assert.spy(cb).was_called(1); assert.spy(cb).was_called_with("foo"); end); it("returns a rejected promise and does not call on_fulfilled", function () local p = promise.reject("foo"); local cb = spy.new(function () end); p:next(cb); assert.spy(cb).was_called(0); end); end); describe("finally()", function () local p, p2, resolve, reject, on_finally; before_each(function () p = promise.new(function (_resolve, _reject) resolve, reject = _resolve, _reject; end); on_finally = spy.new(function () end); p2 = p:finally(on_finally); end); it("runs when a promise is resolved", function () assert.spy(on_finally).was_called(0); resolve("foo"); assert.spy(on_finally).was_called(1); assert.spy(on_finally).was_not_called_with("foo"); end); it("runs when a promise is rejected", function () assert.spy(on_finally).was_called(0); reject("foo"); assert.spy(on_finally).was_called(1); assert.spy(on_finally).was_not_called_with("foo"); end); it("returns a promise that fulfills with the original value", function () local cb2 = spy.new(function () end); p2:next(cb2); assert.spy(on_finally).was_called(0); assert.spy(cb2).was_called(0); resolve("foo"); assert.spy(on_finally).was_called(1); assert.spy(cb2).was_called(1); assert.spy(on_finally).was_not_called_with("foo"); assert.spy(cb2).was_called_with("foo"); end); it("returns a promise that rejects with the original error", function () local on_finally_err = spy.new(function () end); local on_finally_ok = spy.new(function () end); p2:catch(on_finally_err); p2:next(on_finally_ok); assert.spy(on_finally).was_called(0); assert.spy(on_finally_err).was_called(0); reject("foo"); assert.spy(on_finally).was_called(1); -- Since the original promise was rejected, the finally promise should also be assert.spy(on_finally_ok).was_called(0); assert.spy(on_finally_err).was_called(1); assert.spy(on_finally).was_not_called_with("foo"); assert.spy(on_finally_err).was_called_with("foo"); end); it("returns a promise that rejects with an uncaught error inside on_finally", function () p = promise.new(function (_resolve, _reject) resolve, reject = _resolve, _reject; end); local test_error = {}; on_finally = spy.new(function () error(test_error) end); p2 = p:finally(on_finally); local on_finally_err = spy.new(function () end); p2:catch(on_finally_err); assert.spy(on_finally).was_called(0); assert.spy(on_finally_err).was_called(0); reject("foo"); assert.spy(on_finally).was_called(1); assert.spy(on_finally_err).was_called(1); assert.spy(on_finally).was_not_called_with("foo"); assert.spy(on_finally).was_not_called_with(test_error); assert.spy(on_finally_err).was_called_with(test_error); end); end); describe("try()", function () it("works with functions that return a promise", function () local resolve; local p = promise.try(function () return promise.new(function (_resolve) resolve = _resolve; end); end); assert.is_function(resolve); local on_resolved = spy.new(function () end); p:next(on_resolved); assert.spy(on_resolved).was_not_called(); resolve("foo"); assert.spy(on_resolved).was_called_with("foo"); end); it("works with functions that return a value", function () local p = promise.try(function () return "foo"; end); local on_resolved = spy.new(function () end); p:next(on_resolved); assert.spy(on_resolved).was_called_with("foo"); end); it("works with functions that return a promise that rejects", function () local reject; local p = promise.try(function () return promise.new(function (_, _reject) reject = _reject; end); end); assert.is_function(reject); local on_rejected = spy.new(function () end); p:catch(on_rejected); assert.spy(on_rejected).was_not_called(); reject("foo"); assert.spy(on_rejected).was_called_with("foo"); end); it("works with functions that throw errors", function () local test_error = {}; local p = promise.try(function () error(test_error); end); local on_rejected = spy.new(function () end); p:catch(on_rejected); assert.spy(on_rejected).was_called(1); assert.spy(on_rejected).was_called_with(test_error); end); end); describe("set_nexttick()", function () it("works", function () local next_tick = spy.new(function (f) f(); end) local cb = spy.new(function () end); promise.set_nexttick(next_tick); promise.new(function (y, _) y("okay"); end):next(cb); assert.spy(next_tick).was.called(); assert.spy(cb).was.called_with("okay"); end); end) end); prosody-13.0.1/spec/PaxHeaders/util_pubsub_spec.lua0000644000000000000000000000011714773555365017404 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.751711172 prosody-13.0.1/spec/util_pubsub_spec.lua0000644000175000017500000004446114773555365021614 0ustar00prosodyprosody00000000000000local pubsub; setup(function () pubsub = require "util.pubsub"; end); --[[TODO: Retract Purge auto-create/auto-subscribe Item store/node store resize on max_items change service creation config provides alternative node_defaults get subscriptions ]] describe("util.pubsub", function () describe("simple node creation and deletion", function () randomize(false); -- These tests are ordered -- Roughly a port of scansion/scripts/pubsub_createdelete.scs local service = pubsub.new(); describe("#create", function () randomize(false); -- These tests are ordered it("creates a new node", function () assert.truthy(service:create("princely_musings", true)); end); it("fails to create the same node again", function () assert.falsy(service:create("princely_musings", true)); end); end); describe("#delete", function () randomize(false); -- These tests are ordered it("deletes the node", function () assert.truthy(service:delete("princely_musings", true)); end); it("can't delete an already deleted node", function () assert.falsy(service:delete("princely_musings", true)); end); end); end); describe("simple publishing", function () randomize(false); -- These tests are ordered local notified; local broadcaster = spy.new(function (notif_type, node_name, subscribers, item) -- luacheck: ignore 212 notified = subscribers; end); local service = pubsub.new({ broadcaster = broadcaster; }); it("creates a node", function () assert.truthy(service:create("node", true)); end); it("lets someone subscribe", function () assert.truthy(service:add_subscription("node", true, "someone")); end); it("publishes an item", function () assert.truthy(service:publish("node", true, "1", "item 1")); assert.truthy(notified["someone"]); end); it("called the broadcaster", function () assert.spy(broadcaster).was_called(); end); it("should return one item", function () local ok, ret = service:get_items("node", true); assert.truthy(ok); assert.same({ "1", ["1"] = "item 1" }, ret); end); it("lets someone unsubscribe", function () assert.truthy(service:remove_subscription("node", true, "someone")); end); it("does not send notifications after subscription is removed", function () assert.truthy(service:publish("node", true, "1", "item 1")); assert.is_nil(notified["someone"]); end); end); describe("publish with config", function () randomize(false); -- These tests are ordered local broadcaster = spy.new(function (notif_type, node_name, subscribers, item) -- luacheck: ignore 212 end); local service = pubsub.new({ broadcaster = broadcaster; autocreate_on_publish = true; }); it("automatically creates node with requested config", function () assert(service:publish("node", true, "1", "item 1", { myoption = true })); local ok, config = assert(service:get_node_config("node", true)); assert.truthy(ok); assert.equals(true, config.myoption); end); 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); end); it("allows to publish to a node with differing config when only defaults are suggested", function () assert(service:publish("node", true, "1", "item 2", { _defaults_only = true, myoption = false })); end); end); describe("#issue1082", function () randomize(false); -- These tests are ordered local service = pubsub.new(); it("creates a node with max_items = 1", function () assert.truthy(service:create("node", true, { max_items = 1 })); end); it("changes max_items to 2", function () assert.truthy(service:set_node_config("node", true, { max_items = 2 })); end); it("publishes one item", function () assert.truthy(service:publish("node", true, "1", "item 1")); end); it("should return one item", function () local ok, ret = service:get_items("node", true); assert.truthy(ok); assert.same({ "1", ["1"] = "item 1" }, ret); end); it("publishes another item", function () assert.truthy(service:publish("node", true, "2", "item 2")); end); it("should return two items", function () local ok, ret = service:get_items("node", true); assert.truthy(ok); assert.same({ "2", "1", ["1"] = "item 1", ["2"] = "item 2", }, ret); end); it("publishes yet another item", function () assert.truthy(service:publish("node", true, "3", "item 3")); end); it("should still return only two items", function () local ok, ret = service:get_items("node", true); assert.truthy(ok); assert.same({ "3", "2", ["2"] = "item 2", ["3"] = "item 3", }, ret); end); it("has a default max_items", function () assert.truthy(service.config.max_items); end) it("changes max_items to max", function () assert.truthy(service:set_node_config("node", true, { max_items = "max" })); end); it("publishes some more items", function() for i = 4, service.config.max_items + 5 do assert.truthy(service:publish("node", true, tostring(i), "item " .. tostring(i))); end end); it("should still return only two items", function () local ok, ret = service:get_items("node", true); assert.truthy(ok); assert.same(service.config.max_items, #ret); end); end); describe("the thing", function () randomize(false); -- These tests are ordered local service = pubsub.new(); it("creates a node with some items", function () assert.truthy(service:create("node", true, { max_items = 3 })); assert.truthy(service:publish("node", true, "1", "item 1")); assert.truthy(service:publish("node", true, "2", "item 2")); assert.truthy(service:publish("node", true, "3", "item 3")); end); it("should return the requested item", function () local ok, ret = service:get_items("node", true, "1"); assert.truthy(ok); assert.same({ "1", ["1"] = "item 1" }, ret); end); it("should return multiple requested items", function () local ok, ret = service:get_items("node", true, { "1", "2" }); assert.truthy(ok); assert.same({ "1", "2", ["1"] = "item 1", ["2"] = "item 2", }, ret); end); end); describe("node config", function () local service; before_each(function () service = pubsub.new(); service:create("test", true); end); it("access is forbidden for unaffiliated entities", function () local ok, err = service:get_node_config("test", "stranger"); assert.is_falsy(ok); assert.equals("forbidden", err); end); it("returns an error for nodes that do not exist", function () local ok, err = service:get_node_config("nonexistent", true); assert.is_falsy(ok); assert.equals("item-not-found", err); end); end); describe("access model", function () describe("open", function () local service; before_each(function () service = pubsub.new(); -- Do not supply any config, 'open' should be default service:create("test", true); end); it("should be the default", function () local ok, config = service:get_node_config("test", true); assert.truthy(ok); assert.equal("open", config.access_model); end); it("should allow anyone to subscribe", function () local ok = service:add_subscription("test", "stranger", "stranger"); assert.is_true(ok); end); it("should still reject outcast-affiliated entities", function () assert(service:set_affiliation("test", true, "enemy", "outcast")); local ok, err = service:add_subscription("test", "enemy", "enemy"); assert.is_falsy(ok); assert.equal("forbidden", err); end); end); describe("whitelist", function () local service; before_each(function () service = assert(pubsub.new()); assert.is_true(service:create("test", true, { access_model = "whitelist" })); end); it("should be present in the configuration", function () local ok, config = service:get_node_config("test", true); assert.truthy(ok); assert.equal("whitelist", config.access_model); end); it("should not allow anyone to subscribe", function () local ok, err = service:add_subscription("test", "stranger", "stranger"); assert.is_false(ok); assert.equals("forbidden", err); end); end); describe("change", function () local service; before_each(function () service = pubsub.new(); service:create("test", true, { access_model = "open" }); end); it("affects existing subscriptions", function () do local ok = service:add_subscription("test", "stranger", "stranger"); assert.is_true(ok); end do local ok, sub = service:get_subscription("test", "stranger", "stranger"); assert.is_true(ok); assert.is_true(sub); end assert(service:set_node_config("test", true, { access_model = "whitelist" })); do local ok, sub = service:get_subscription("test", "stranger", "stranger"); assert.is_true(ok); assert.is_nil(sub); end end); end); end); describe("publish model", function () describe("publishers", function () local service; before_each(function () service = pubsub.new(); -- Do not supply any config, 'publishers' should be default service:create("test", true); end); it("should be the default", function () local ok, config = service:get_node_config("test", true); assert.truthy(ok); assert.equal("publishers", config.publish_model); end); it("should not allow anyone to publish", function () assert.is_true(service:add_subscription("test", "stranger", "stranger")); local ok, err = service:publish("test", "stranger", "item1", "foo"); assert.is_falsy(ok); assert.equals("forbidden", err); end); it("should allow publishers to publish", function () assert(service:set_affiliation("test", true, "mypublisher", "publisher")); -- luacheck: ignore 211/err local ok, err = service:publish("test", "mypublisher", "item1", "foo"); assert.is_true(ok); end); it("should allow owners to publish", function () assert(service:set_affiliation("test", true, "myowner", "owner")); local ok = service:publish("test", "myowner", "item1", "foo"); assert.is_true(ok); end); end); describe("open", function () local service; before_each(function () service = pubsub.new(); service:create("test", true, { publish_model = "open" }); end); it("should allow anyone to publish", function () local ok = service:publish("test", "stranger", "item1", "foo"); assert.is_true(ok); end); end); describe("subscribers", function () local service; before_each(function () service = pubsub.new(); service:create("test", true, { publish_model = "subscribers" }); end); it("should not allow non-subscribers to publish", function () local ok, err = service:publish("test", "stranger", "item1", "foo"); assert.is_falsy(ok); assert.equals("forbidden", err); end); it("should allow subscribers to publish without an affiliation", function () assert.is_true(service:add_subscription("test", "stranger", "stranger")); local ok = service:publish("test", "stranger", "item1", "foo"); assert.is_true(ok); end); it("should allow publishers to publish without a subscription", function () assert(service:set_affiliation("test", true, "mypublisher", "publisher")); -- luacheck: ignore 211/err local ok, err = service:publish("test", "mypublisher", "item1", "foo"); assert.is_true(ok); end); it("should allow owners to publish without a subscription", function () assert(service:set_affiliation("test", true, "myowner", "owner")); local ok = service:publish("test", "myowner", "item1", "foo"); assert.is_true(ok); end); end); end); describe("item API", function () local service; before_each(function () service = pubsub.new(); service:create("test", true, { publish_model = "subscribers" }); end); describe("get_last_item()", function () it("succeeds with nil on empty nodes", function () local ok, id, item = service:get_last_item("test", true); assert.is_true(ok); assert.is_nil(id); assert.is_nil(item); end); it("succeeds and returns the last item", function () service:publish("test", true, "one", "hello world"); service:publish("test", true, "two", "hello again"); service:publish("test", true, "three", "hey"); service:publish("test", true, "one", "bye"); local ok, id, item = service:get_last_item("test", true); assert.is_true(ok); assert.equal("one", id); assert.equal("bye", item); end); end); describe("get_items()", function () it("fails on non-existent nodes", function () local ok, err = service:get_items("no-node", true); assert.is_falsy(ok); assert.equal("item-not-found", err); end); it("returns no items on an empty node", function () local ok, items = service:get_items("test", true); assert.is_true(ok); assert.equal(0, #items); assert.is_nil(next(items)); end); it("returns no items on an empty node", function () local ok, items = service:get_items("test", true); assert.is_true(ok); assert.equal(0, #items); assert.is_nil((next(items))); end); it("returns all published items", function () service:publish("test", true, "one", "hello world"); service:publish("test", true, "two", "hello again"); service:publish("test", true, "three", "hey"); service:publish("test", true, "one", "bye"); local ok, items = service:get_items("test", true); assert.is_true(ok); assert.same({ "one", "three", "two", two = "hello again", three = "hey", one = "bye" }, items); end); end); end); describe("restoring data from nodestore", function () local nodestore = { data = { test = { name = "test"; config = {}; affiliations = {}; subscribers = { ["someone"] = true; }; } } }; function nodestore:users() return pairs(self.data) end function nodestore:get(key) return self.data[key]; end local service = pubsub.new({ nodestore = nodestore; }); it("subscriptions", function () local ok, ret = service:get_subscriptions(nil, true, nil) assert.is_true(ok); assert.same({ { node = "test", jid = "someone", subscription = true, } }, ret); end); end); describe("node config checking", function () local service; before_each(function () service = pubsub.new({ check_node_config = function (node, actor, config) -- luacheck: ignore 212 return config["max_items"] <= 20; end; }); end); it("defaults, then configure", function () local ok, err = service:create("node", true); assert.is_true(ok, err); local ok, err = service:set_node_config("node", true, { max_items = 10 }); assert.is_true(ok, err); local ok, err = service:set_node_config("node", true, { max_items = 100 }); assert.falsy(ok, err); assert.equals(err, "not-acceptable"); end); it("create with ok config, then configure", function () local ok, err = service:create("node", true, { max_items = 10 }); assert.is_true(ok, err); local ok, err = service:set_node_config("node", true, { max_items = 100 }); assert.falsy(ok, err); local ok, err = service:set_node_config("node", true, { max_items = 10 }); assert.is_true(ok, err); end); it("create with unacceptable config", function () local ok, err = service:create("node", true, { max_items = 100 }); assert.falsy(ok, err); end); end); describe("subscriber filter", function () it("works", function () local filter = spy.new(function (subs) -- luacheck: ignore 212/subs return {["modified"] = true}; end); local broadcaster = spy.new(function (notif_type, node_name, subscribers, item) -- luacheck: ignore 212 end); local service = pubsub.new({ subscriber_filter = filter; broadcaster = broadcaster; }); local ok = service:create("node", true); assert.truthy(ok); local ok = service:add_subscription("node", true, "someone"); assert.truthy(ok); local ok = service:publish("node", true, "1", "item"); assert.truthy(ok); -- TODO how to match table arguments? assert.spy(filter).was_called(); assert.spy(broadcaster).was_called(); end); end); describe("persist_items", function() it("can be disabled", function() local broadcaster = spy.new(function(notif_type, node_name, subscribers, item) -- luacheck: ignore 212 end); local service = pubsub.new { node_defaults = { persist_items = false }, broadcaster = broadcaster } local ok = service:create("node", true) assert.truthy(ok); local ok = service:publish("node", true, "1", "item"); assert.truthy(ok); assert.spy(broadcaster).was_called(); local ok, items = service:get_items("node", true); assert.not_truthy(ok); assert.equal(items, "persistent-items-unsupported"); end); end) describe("max_items", function () it("works", function () local service = pubsub.new { }; local ok = service:create("node", true) assert.truthy(ok); for i = 1, 20 do assert.truthy(service:publish("node", true, "item"..tostring(i), "data"..tostring(i))); end do local ok, items = service:get_items("node", true, nil, { max = 3 }); assert.truthy(ok, items); assert.equal(3, #items); assert.same({ "item20", "item19", "item18", item20 = "data20", item19 = "data19", item18 = "data18", }, items, "items should be ordered by oldest first"); end do local ok, items = service:get_items("node", true, nil, { max = 10 }); assert.truthy(ok, items); assert.equal(10, #items); assert.same({ "item20", "item19", "item18", "item17", "item16", "item15", "item14", "item13", "item12", "item11", item20 = "data20", item19 = "data19", item18 = "data18", item17 = "data17", item16 = "data16", item15 = "data15", item14 = "data14", item13 = "data13", item12 = "data12", item11 = "data11", }, items, "items should be ordered by oldest first"); end 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); prosody-13.0.1/spec/PaxHeaders/util_queue_spec.lua0000644000000000000000000000011714773555365017230 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.755711188 prosody-13.0.1/spec/util_queue_spec.lua0000644000175000017500000000737714773555365021445 0ustar00prosodyprosody00000000000000 local queue = require "util.queue"; describe("util.queue", function() describe("#new()", function() it("should work", function() do local q = queue.new(10); assert.are.equal(q.size, 10); assert.are.equal(q:count(), 0); assert.is_true(q:push("one")); assert.is_true(q:push("two")); assert.is_true(q:push("three")); for i = 4, 10 do assert.is_true(q:push("hello")); assert.are.equal(q:count(), i, "count is not "..i.."("..q:count()..")"); end assert.are.equal(q:push("hello"), nil, "queue overfull!"); assert.are.equal(q:push("hello"), nil, "queue overfull!"); assert.are.equal(q:pop(), "one", "queue item incorrect"); assert.are.equal(q:pop(), "two", "queue item incorrect"); assert.is_true(q:push("hello")); assert.is_true(q:push("hello")); assert.are.equal(q:pop(), "three", "queue item incorrect"); assert.is_true(q:push("hello")); assert.are.equal(q:push("hello"), nil, "queue overfull!"); assert.are.equal(q:push("hello"), nil, "queue overfull!"); assert.are.equal(q:count(), 10, "queue count incorrect"); for _ = 1, 10 do assert.are.equal(q:pop(), "hello", "queue item incorrect"); end assert.are.equal(q:count(), 0, "queue count incorrect"); assert.are.equal(q:pop(), nil, "empty queue pops non-nil result"); assert.are.equal(q:count(), 0, "popping empty queue affects count"); assert.are.equal(q:peek(), nil, "empty queue peeks non-nil result"); assert.are.equal(q:count(), 0, "peeking empty queue affects count"); assert.is_true(q:push(1)); for i = 1, 1001 do assert.are.equal(q:pop(), i); assert.are.equal(q:count(), 0); assert.is_true(q:push(i+1)); assert.are.equal(q:count(), 1); end assert.are.equal(q:pop(), 1002); assert.is_true(q:push(1)); for i = 1, 1000 do assert.are.equal(q:pop(), i); assert.is_true(q:push(i+1)); end assert.are.equal(q:pop(), 1001); assert.are.equal(q:count(), 0); end do -- Test queues that purge old items when pushing to a full queue local q = queue.new(10, true); for i = 1, 10 do q:push(i); end assert.are.equal(q:count(), 10); assert.is_true(q:push(11)); assert.are.equal(q:count(), 10); assert.are.equal(q:pop(), 2); -- First item should have been purged assert.are.equal(q:peek(), 3); for i = 12, 32 do assert.is_true(q:push(i)); end assert.are.equal(q:count(), 10); assert.are.equal(q:pop(), 23); end do -- Test iterator local q = queue.new(10, true); for i = 1, 10 do q:push(i); end local i = 0; for item in q:items() do i = i + 1; assert.are.equal(item, i, "unexpected item returned by iterator") end end end); end); describe("consume()", function () it("should work", function () local q = queue.new(10); for i = 1, 5 do q:push(i); end local c = 0; for i in q:consume() do assert(i == c + 1); assert(q:count() == (5-i)); c = i; end end); it("should work even if items are pushed in the loop", function () local q = queue.new(10); for i = 1, 5 do q:push(i); end local c = 0; for i in q:consume() do assert(i == c + 1); if c < 3 then assert(q:count() == (5-i)); else assert(q:count() == (6-i)); end c = i; if c == 3 then q:push(6); end end 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); prosody-13.0.1/spec/PaxHeaders/util_random_spec.lua0000644000000000000000000000011714773555365017364 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.755711188 prosody-13.0.1/spec/util_random_spec.lua0000644000175000017500000000073214773555365021565 0ustar00prosodyprosody00000000000000 local random = require "util.random"; describe("util.random", function() describe("#bytes()", function() it("should return a string", function() assert.is_string(random.bytes(16)); end); it("should return the requested number of bytes", function() -- Makes no attempt at testing how random the bytes are, -- just that it returns the number of bytes requested for i = 1, 20 do assert.are.equal(2^i, #random.bytes(2^i)); end end); end); end); prosody-13.0.1/spec/PaxHeaders/util_ringbuffer_spec.lua0000644000000000000000000000011714773555365020235 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.755711188 prosody-13.0.1/spec/util_ringbuffer_spec.lua0000644000175000017500000000523214773555365022436 0ustar00prosodyprosody00000000000000local rb = require "util.ringbuffer"; describe("util.ringbuffer", function () describe("#new", function () it("has a constructor", function () assert.Function(rb.new); end); it("can be created", function () assert.truthy(rb.new()); end); it("won't create an empty buffer", function () assert.has_error(function () rb.new(0); end); end); it("won't create a negatively sized buffer", function () assert.has_error(function () rb.new(-1); end); end); end); describe(":write", function () local b = rb.new(); it("works", function () assert.truthy(b:write("hi")); end); end); describe(":discard", function () local b = rb.new(); it("works", function () assert.truthy(b:write("hello world")); assert.truthy(b:discard(6)); assert.equal(5, #b); assert.equal("world", b:read(5)); end); end); describe(":sub", function () -- Helper function to compare buffer:sub() with string:sub() local function test_sub(b, x, y) local s = b:read(#b, true); local string_result, buffer_result = s:sub(x, y), b:sub(x, y); assert.equals(string_result, buffer_result, ("buffer:sub(%d, %s) does not match string:sub()"):format(x, y and ("%d"):format(y) or "nil")); end it("works", function () local b = rb.new(); assert.truthy(b:write("hello world")); assert.equals("hello", b:sub(1, 5)); end); it("supports optional end parameter", function () local b = rb.new(); assert.truthy(b:write("hello world")); assert.equals("hello world", b:sub(1)); assert.equals("world", b:sub(-5)); end); it("is equivalent to string:sub", function () local b = rb.new(6); assert.truthy(b:write("foobar")); b:read(3); b:write("foo"); for i = -13, 13 do for j = -13, 13 do test_sub(b, i, j); end end end); end); describe(":byte", function () -- Helper function to compare buffer:byte() with string:byte() local function test_byte(b, x, y) local s = b:read(#b, true); 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")); end it("is equivalent to string:byte", function () local b = rb.new(6); assert.truthy(b:write("foobar")); b:read(3); b:write("foo"); test_byte(b, 1); test_byte(b, 3); test_byte(b, -1); test_byte(b, -3); for i = -13, 13 do for j = -13, 13 do test_byte(b, i, j); end end end); it("works with characters > 127", function () local b = rb.new(); b:write(string.char(0, 140)); local r = { b:byte(1, 2) }; assert.same({ 0, 140 }, r); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_roles_spec.lua0000644000000000000000000000011714773555365017230 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.755711188 prosody-13.0.1/spec/util_roles_spec.lua0000644000175000017500000001147314773555365021435 0ustar00prosodyprosody00000000000000describe("util.roles", function () randomize(false); local roles; it("can be loaded", function () roles = require "util.roles"; end); local test_role; it("can create a new role", function () test_role = roles.new(); assert.is_not_nil(test_role); assert.is_truthy(roles.is_role(test_role)); end); describe("role object", function () it("can be initialized with permissions", function () local test_role_2 = roles.new({ permissions = { perm1 = true; perm2 = false; }; }); assert.truthy(test_role_2:may("perm1")); assert.falsy(test_role_2:may("perm2")); end); it("has a sensible tostring", function () local test_role_2 = roles.new({ id = "test-role-2"; name = "Test Role 2"; }); assert.truthy(tostring(test_role_2):find(test_role_2.id, 1, true)); assert.truthy(tostring(test_role_2):find("Test Role 2", 1, true)); end); it("is restrictive by default", function () assert.falsy(test_role:may("my-permission")); end); it("allows you to set permissions", function () test_role:set_permission("my-permission", true); assert.truthy(test_role:may("my-permission")); end); it("allows you to set negative permissions", function () test_role:set_permission("my-other-permission", false); assert.falsy(test_role:may("my-other-permission")); end); it("does not allows you to override previously set permissions by default", function () local ok, err = test_role:set_permission("my-permission", false); assert.falsy(ok); assert.is_equal("policy-already-exists", err); -- Confirm old permission still in place assert.truthy(test_role:may("my-permission")); end); it("allows you to explicitly override previously set permissions", function () assert.truthy(test_role:set_permission("my-permission", false, true)); assert.falsy(test_role:may("my-permission")); end); describe("inheritance", function () local child_role; it("works", function () test_role:set_permission("inherited-permission", true); child_role = roles.new({ inherits = { test_role }; }); assert.truthy(child_role:may("inherited-permission")); assert.falsy(child_role:may("my-permission")); end); it("allows listing policies", function () local expected = { ["my-permission"] = false; ["my-other-permission"] = false; ["inherited-permission"] = true; }; local received = {}; for permission_name, permission_policy in child_role:policies() do received[permission_name] = permission_policy; end assert.same(expected, received); end); it("supports multiple depths of inheritance", function () local grandchild_role = roles.new({ inherits = { child_role }; }); assert.truthy(grandchild_role:may("inherited-permission")); end); describe("supports ordered inheritance from multiple roles", function () local parent_role = roles.new(); local final_role = roles.new({ -- Yes, the names are getting confusing. -- btw, test_role is inherited through child_role. inherits = { parent_role, child_role }; }); local test_cases = { -- { , , } { true, nil, false, result = true }; { nil, false, true, result = false }; { nil, true, false, result = true }; { nil, nil, false, result = false }; { nil, nil, true, result = true }; }; for n, test_case in ipairs(test_cases) do it("(case "..n..")", function () local perm_name = ("multi-inheritance-perm-%d"):format(n); assert.truthy(final_role:set_permission(perm_name, test_case[1])); assert.truthy(parent_role:set_permission(perm_name, test_case[2])); assert.truthy(test_role:set_permission(perm_name, test_case[3])); assert.equal(test_case.result, final_role:may(perm_name)); end); end end); it("updates child roles when parent roles change", function () assert.truthy(child_role:may("inherited-permission")); assert.truthy(test_role:set_permission("inherited-permission", false, true)); assert.falsy(child_role:may("inherited-permission")); end); end); describe("cloning", function () local cloned_role; it("works", function () assert.truthy(test_role:set_permission("perm-1", true)); cloned_role = test_role:clone(); assert.truthy(cloned_role:may("perm-1")); end); it("isolates changes", function () -- After cloning, changes in either the original or the clone -- should not appear in the other. assert.truthy(test_role:set_permission("perm-1", false, true)); assert.truthy(test_role:set_permission("perm-2", true)); assert.truthy(cloned_role:set_permission("perm-3", true)); assert.truthy(cloned_role:may("perm-1")); assert.falsy(cloned_role:may("perm-2")); assert.falsy(test_role:may("perm-3")); end); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_rsm_spec.lua0000644000000000000000000000011714773555365016705 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.755711188 prosody-13.0.1/spec/util_rsm_spec.lua0000644000175000017500000000747514773555365021121 0ustar00prosodyprosody00000000000000local rsm = require "util.rsm"; local xml = require "util.xml"; local function strip(s) return (s:gsub(">%s+<", "><")); end describe("util.rsm", function () describe("parse", function () it("works", function () local test = xml.parse(strip([[ 10 ]])); assert.same({ max = 10 }, rsm.parse(test)); end); it("works", function () local test = xml.parse(strip([[ saint@example.org peterpan@neverland.lit 800 ]])); assert.same({ first = { index = 0, "saint@example.org" }, last = "peterpan@neverland.lit", count = 800 }, rsm.parse(test)); end); it("works", function () local test = xml.parse(strip([[ 10 peter@pixyland.org ]])); assert.same({ max = 10, before = "peter@pixyland.org" }, rsm.parse(test)); end); it("all fields works", function() local test = assert(xml.parse(strip([[ a b 10 f 5 z 100 ]]))); assert.same({ after = "a"; before = "b"; count = 10; first = {index = 1; "f"}; index = 5; last = "z"; max = 100; }, rsm.parse(test)); end); end); describe("generate", function () it("works", function () local test = xml.parse(strip([[ 10 ]])); local res = rsm.generate({ max = 10 }); assert.same(test:get_child_text("max"), res:get_child_text("max")); end); it("works", function () local test = xml.parse(strip([[ saint@example.org peterpan@neverland.lit 800 ]])); local res = rsm.generate({ first = { index = 0, "saint@example.org" }, last = "peterpan@neverland.lit", count = 800 }); assert.same(test:get_child("first").attr.index, res:get_child("first").attr.index); assert.same(test:get_child_text("first"), res:get_child_text("first")); assert.same(test:get_child_text("last"), res:get_child_text("last")); assert.same(test:get_child_text("count"), res:get_child_text("count")); end); it("works", function () local test = xml.parse(strip([[ 10 peter@pixyland.org ]])); local res = rsm.generate({ max = 10, before = "peter@pixyland.org" }); assert.same(test:get_child_text("max"), res:get_child_text("max")); assert.same(test:get_child_text("before"), res:get_child_text("before")); end); it("handles floats", function () local r1 = rsm.generate({ max = 10.0, count = 100.0, first = { index = 1.0, "foo" } }); assert.equal("10", r1:get_child_text("max")); assert.equal("100", r1:get_child_text("count")); assert.equal("1", r1:get_child("first").attr.index); end); it("all fields works", function () local res = rsm.generate({ after = "a"; before = "b"; count = 10; first = {index = 1; "f"}; index = 5; last = "z"; max = 100; }); assert.equal("a", res:get_child_text("after")); assert.equal("b", res:get_child_text("before")); assert.equal("10", res:get_child_text("count")); assert.equal("f", res:get_child_text("first")); assert.equal("1", res:get_child("first").attr.index); assert.equal("5", res:get_child_text("index")); assert.equal("z", res:get_child_text("last")); assert.equal("100", res:get_child_text("max")); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_sasl_spec.lua0000644000000000000000000000011714773555365017046 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.755711188 prosody-13.0.1/spec/util_sasl_spec.lua0000644000175000017500000000376114773555365021254 0ustar00prosodyprosody00000000000000local sasl = require "util.sasl"; -- profile * mechanism -- callbacks could use spies instead describe("util.sasl", function () describe("plain_test profile", function () local profile = { plain_test = function (_, username, password, realm) assert.equals("user", username) assert.equals("pencil", password) assert.equals("sasl.test", realm) return true, true; end; }; it("works with PLAIN", function () local plain = sasl.new("sasl.test", profile); assert.truthy(plain:select("PLAIN")); assert.truthy(plain:process("\000user\000pencil")); assert.equals("user", plain.username); end); end); describe("plain profile", function () local profile = { plain = function (_, username, realm) assert.equals("user", username) assert.equals("sasl.test", realm) return "pencil", true; end; }; it("works with PLAIN", function () local plain = sasl.new("sasl.test", profile); assert.truthy(plain:select("PLAIN")); assert.truthy(plain:process("\000user\000pencil")); assert.equals("user", plain.username); end); -- 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); prosody-13.0.1/spec/PaxHeaders/util_serialization_spec.lua0000644000000000000000000000011714773555365020761 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.759711204 prosody-13.0.1/spec/util_serialization_spec.lua0000644000175000017500000000421414773555365023161 0ustar00prosodyprosody00000000000000local serialization = require "util.serialization"; describe("util.serialization", function () describe("serialize", function () it("makes a string", function () assert.is_string(serialization.serialize({})); assert.is_string(serialization.serialize(nil)); assert.is_string(serialization.serialize(1)); assert.is_string(serialization.serialize(true)); end); it("rejects function by default", function () assert.has_error(function () serialization.serialize(function () end) end); end); it("makes a string in debug mode", function () assert.is_string(serialization.serialize(function () end, "debug")); end); it("rejects cycles", function () assert.has_error(function () local t = {} t[t] = { t }; serialization.serialize(t) end); -- also with multirefs allowed assert.has_error(function () local t = {} t[t] = { t }; serialization.serialize(t, { multirefs = true }) end); end); it("rejects multiple references to same table", function () assert.has_error(function () local t1 = {}; local t2 = { t1, t1 }; serialization.serialize(t2, { multirefs = false }); end); end); it("optionally allows multiple references to same table", function () assert.has_error(function () local t1 = {}; local t2 = { t1, t1 }; serialization.serialize(t2, { multirefs = true }); end); end); it("roundtrips", function () local function test(data) local serialized = serialization.serialize(data); assert.is_string(serialized); local deserialized, err = serialization.deserialize(serialized); assert.same(data, deserialized, err); end test({}); test({hello="world"}); test("foobar") test("\0\1\2\3"); test("nödåtgärd"); test({1,2,3,4}); test({foo={[100]={{"bar"},{baz=1}}}}); test({["goto"] = {["function"]={["do"]="keywords"}}}); end); it("can serialize with metatables", function () local s = serialization.new({ freeze = true }); local t = setmetatable({ a = "hi" }, { __freeze = function (t) return { t.a } end }); local rt = serialization.deserialize(s(t)); assert.same({"hi"}, rt); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_smqueue_spec.lua0000644000000000000000000000011714773555365017570 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.759711204 prosody-13.0.1/spec/util_smqueue_spec.lua0000644000175000017500000000431014773555365021765 0ustar00prosodyprosody00000000000000describe("util.smqueue", function() local smqueue setup(function() smqueue = require "util.smqueue"; end) 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) end) describe("#push()", function() it("should allow pushing many items", function() local q = smqueue.new(10); for i = 1, 20 do q:push(i); end assert.equal(20, q:count_unacked()); end) end) describe("#resumable()", function() it("returns true while the queue is small", function() local q = smqueue.new(10); for i = 1, 10 do q:push(i); end assert.truthy(q:resumable()); q:push(11); assert.falsy(q:resumable()); end) end) describe("#ack", function() it("allows removing items", function() local q = smqueue.new(10); for i = 1, 10 do q:push(i); end assert.same({ 1; 2; 3 }, q:ack(3)); assert.same({ 4; 5; 6 }, q:ack(6)); assert.falsy(q:ack(3), "can't go backwards") assert.falsy(q:ack(100), "can't ack too many") for i = 11, 20 do q:push(i); end assert.same({ 11; 12 }, q:ack(12), "items are dropped"); end) end) describe("#resume", function() it("iterates over current items", function() local q = smqueue.new(10); for i = 1, 12 do q:push(i); end assert.same({ 3; 4; 5; 6 }, q:ack(6)); assert.truthy(q:resumable()); local resume = {} for _, i in q:resume() do resume[i] = true end assert.same({ [7] = true; [8] = true; [9] = true; [10] = true; [11] = true; [12] = true }, resume); end) end) describe("#table", function () it("produces a compat layer", function () local q = smqueue.new(10); for i = 1,10 do q:push(i); end do local t = q:table(); assert.same({ 1; 2; 3; 4; 5; 6; 7; 8; 9; 10 }, t); end do for i = 11,20 do q:push(i); end local t = q:table(); assert.same({ 11; 12; 13; 14; 15; 16; 17; 18; 19; 20 }, t); end do q:ack(15); local t = q:table(); assert.same({ 16; 17; 18; 19; 20 }, t); end do q:ack(20); local t = q:table(); assert.same({}, t); end end) end) end); prosody-13.0.1/spec/PaxHeaders/util_stanza_spec.lua0000644000000000000000000000011714773555365017404 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.759711204 prosody-13.0.1/spec/util_stanza_spec.lua0000644000175000017500000005042214773555365021606 0ustar00prosodyprosody00000000000000 local st = require "util.stanza"; local errors = require "util.error"; describe("util.stanza", function() describe("#preserialize()", function() it("should work", function() local stanza = st.stanza("message", { type = "chat" }):text_tag("body", "Hello"); local stanza2 = st.preserialize(stanza); assert.is_table(stanza2, "Preserialized stanza is a table"); assert.is_nil(getmetatable(stanza2), "Preserialized stanza has no metatable"); assert.is_string(stanza2.name, "Preserialized stanza has a name field"); assert.equal(stanza.name, stanza2.name, "Preserialized stanza has same name as the input stanza"); assert.same(stanza.attr, stanza2.attr, "Preserialized stanza same attr table as input stanza"); assert.is_nil(stanza2.tags, "Preserialized stanza has no tag list"); assert.is_nil(stanza2.last_add, "Preserialized stanza has no last_add marker"); assert.is_table(stanza2[1], "Preserialized child element preserved"); assert.equal("body", stanza2[1].name, "Preserialized child element name preserved"); end); end); describe("#deserialize()", function() it("should work", function() local stanza = { name = "message", attr = { type = "chat" }, { name = "body", attr = { }, "Hello" } }; local stanza2 = st.deserialize(st.preserialize(stanza)); assert.is_table(stanza2, "Deserialized stanza is a table"); assert.equal(st.stanza_mt, getmetatable(stanza2), "Deserialized stanza has stanza metatable"); assert.is_string(stanza2.name, "Deserialized stanza has a name field"); assert.equal(stanza.name, stanza2.name, "Deserialized stanza has same name as the input table"); assert.same(stanza.attr, stanza2.attr, "Deserialized stanza same attr table as input table"); assert.is_table(stanza2.tags, "Deserialized stanza has tag list"); assert.is_table(stanza2[1], "Deserialized child element preserved"); assert.equal("body", stanza2[1].name, "Deserialized child element name preserved"); end); end); describe("#stanza()", function() it("should work", function() local s = st.stanza("foo", { xmlns = "myxmlns", a = "attr-a" }); assert.are.equal(s.name, "foo"); assert.are.equal(s.attr.xmlns, "myxmlns"); assert.are.equal(s.attr.a, "attr-a"); local s1 = st.stanza("s1"); assert.are.equal(s1.name, "s1"); assert.are.equal(s1.attr.xmlns, nil); assert.are.equal(#s1, 0); assert.are.equal(#s1.tags, 0); s1:tag("child1"); assert.are.equal(#s1.tags, 1); assert.are.equal(s1.tags[1].name, "child1"); s1:tag("grandchild1"):up(); assert.are.equal(#s1.tags, 1); assert.are.equal(s1.tags[1].name, "child1"); assert.are.equal(#s1.tags[1], 1); assert.are.equal(s1.tags[1][1].name, "grandchild1"); s1:up():tag("child2"); assert.are.equal(#s1.tags, 2, tostring(s1)); assert.are.equal(s1.tags[1].name, "child1"); assert.are.equal(s1.tags[2].name, "child2"); assert.are.equal(#s1.tags[1], 1); assert.are.equal(s1.tags[1][1].name, "grandchild1"); s1:up():text("Hello world"); assert.are.equal(#s1.tags, 2); assert.are.equal(#s1, 3); assert.are.equal(s1.tags[1].name, "child1"); assert.are.equal(s1.tags[2].name, "child2"); assert.are.equal(#s1.tags[1], 1); assert.are.equal(s1.tags[1][1].name, "grandchild1"); end); it("should work with unicode values", function () local s = st.stanza("Объект", { xmlns = "myxmlns", ["Объект"] = "&" }); assert.are.equal(s.name, "Объект"); assert.are.equal(s.attr.xmlns, "myxmlns"); assert.are.equal(s.attr["Объект"], "&"); end); it("should allow :text() with nil and empty strings", function () local s_control = st.stanza("foo"); assert.same(st.stanza("foo"):text(), s_control); assert.same(st.stanza("foo"):text(nil), s_control); assert.same(st.stanza("foo"):text(""), s_control); end); it("validates names", function () assert.has_error_match(function () st.stanza("invalid\0name"); end, "invalid tag name:") assert.has_error_match(function () st.stanza("name", { ["foo\1\2\3bar"] = "baz" }); end, "invalid attribute name: contains control characters") assert.has_error_match(function () st.stanza("name", { ["foo"] = "baz\1\2\3\255moo" }); end, "invalid attribute value: contains control characters") end) it("validates types", function () assert.has_error_match(function () st.stanza(1); end, "invalid tag name: expected string, got number") assert.has_error_match(function () st.stanza("name", "string"); end, "invalid attributes: expected table, got string") assert.has_error_match(function () st.stanza("name",{1}); end, "invalid attribute name: expected string, got number") assert.has_error_match(function () st.stanza("name",{foo=1}); end, "invalid attribute value: expected string, got number") end) end); describe("#message()", function() it("should work", function() local m = st.message(); assert.are.equal(m.name, "message"); end); end); describe("#iq()", function() it("should create an iq stanza", function() local i = st.iq({ type = "get", id = "foo" }); assert.are.equal("iq", i.name); assert.are.equal("foo", i.attr.id); assert.are.equal("get", i.attr.type); end); it("should reject stanzas with no attributes", function () assert.has.error_match(function () st.iq(); end, "attributes"); end); it("should reject stanzas with no id", function () assert.has.error_match(function () st.iq({ type = "get" }); end, "id attribute"); end); it("should reject stanzas with no type", function () assert.has.error_match(function () st.iq({ id = "foo" }); end, "type attribute"); end); end); describe("#presence()", function () it("should work", function() local p = st.presence(); assert.are.equal(p.name, "presence"); end); end); describe("#reply()", function() it("should work for ", function() -- Test stanza local s = st.stanza("s", { to = "touser", from = "fromuser", id = "123" }) :tag("child1"); -- Make reply stanza local r = st.reply(s); assert.are.equal(r.name, s.name); assert.are.equal(r.id, s.id); assert.are.equal(r.attr.to, s.attr.from); assert.are.equal(r.attr.from, s.attr.to); assert.are.equal(#r.tags, 0, "A reply should not include children of the original stanza"); end); it("should work for ", function() -- Test stanza local s = st.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "get" }) :tag("child1"); -- Make reply stanza local r = st.reply(s); assert.are.equal(r.name, s.name); assert.are.equal(r.id, s.id); assert.are.equal(r.attr.to, s.attr.from); assert.are.equal(r.attr.from, s.attr.to); assert.are.equal(r.attr.type, "result"); assert.are.equal(#r.tags, 0, "A reply should not include children of the original stanza"); end); it("should work for ", function() -- Test stanza local s = st.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "set" }) :tag("child1"); -- Make reply stanza local r = st.reply(s); assert.are.equal(r.name, s.name); assert.are.equal(r.id, s.id); assert.are.equal(r.attr.to, s.attr.from); assert.are.equal(r.attr.from, s.attr.to); assert.are.equal(r.attr.type, "result"); assert.are.equal(#r.tags, 0, "A reply should not include children of the original stanza"); end); it("should reject not-stanzas", function () assert.has.error_match(function () st.reply(not "a stanza"); end, "expected stanza"); end); it("should reject not-stanzas", function () assert.has.error_match(function () st.reply({name="x"}); end, "expected stanza"); end); end); describe("#error_reply()", function() it("should work for ", function() -- Test stanza local s = st.stanza("s", { to = "touser", from = "fromuser", id = "123" }) :tag("child1"); -- Make reply stanza local r = st.error_reply(s, "cancel", "service-unavailable", nil, "host"); assert.are.equal(r.name, s.name); assert.are.equal(r.id, s.id); assert.are.equal(r.attr.to, s.attr.from); assert.are.equal(r.attr.from, s.attr.to); assert.are.equal(#r.tags, 1); assert.are.equal(r.tags[1].tags[1].name, "service-unavailable"); assert.are.equal(r.tags[1].attr.by, "host"); end); it("should work for ", function() -- Test stanza local s = st.stanza("iq", { to = "touser", from = "fromuser", id = "123", type = "get" }) :tag("child1"); -- Make reply stanza local r = st.error_reply(s, "cancel", "service-unavailable"); assert.are.equal(r.name, s.name); assert.are.equal(r.id, s.id); assert.are.equal(r.attr.to, s.attr.from); assert.are.equal(r.attr.from, s.attr.to); assert.are.equal(r.attr.type, "error"); assert.are.equal(#r.tags, 1); assert.are.equal(r.tags[1].tags[1].name, "service-unavailable"); end); it("should reject not-stanzas", function () assert.has.error_match(function () st.error_reply(not "a stanza", "modify", "bad-request"); end, "expected stanza"); end); it("should reject stanzas of type error", function () assert.has.error_match(function () st.error_reply(st.message({type="error"}), "cancel", "conflict"); end, "got stanza of type error"); assert.has.error_match(function () st.error_reply(st.error_reply(st.message({type="chat"}), "modify", "forbidden"), "cancel", "service-unavailable"); end, "got stanza of type error"); end); describe("util.error integration", function () it("should accept util.error objects", function () local s = st.message({ to = "touser", from = "fromuser", id = "123", type = "chat" }, "Hello"); local e = errors.new({ type = "modify", condition = "not-acceptable", text = "Bork bork bork" }, { by = "this.test" }); local r = st.error_reply(s, e); assert.are.equal(r.name, s.name); assert.are.equal(r.id, s.id); assert.are.equal(r.attr.to, s.attr.from); assert.are.equal(r.attr.from, s.attr.to); assert.are.equal(r.attr.type, "error"); assert.are.equal(r.tags[1].name, "error"); assert.are.equal(r.tags[1].attr.type, e.type); assert.are.equal(r.tags[1].tags[1].name, e.condition); assert.are.equal(r.tags[1].tags[2]:get_text(), e.text); assert.are.equal("this.test", r.tags[1].attr.by); end); it("should accept util.error objects with an URI", function () local s = st.message({ to = "touser", from = "fromuser", id = "123", type = "chat" }, "Hello"); local gone = errors.new({ condition = "gone", extra = { uri = "file:///dev/null" } }) local gonner = st.error_reply(s, gone); assert.are.equal("gone", gonner.tags[1].tags[1].name); assert.are.equal("file:///dev/null", gonner.tags[1].tags[1][1]); end); it("should accept util.error objects with application specific error", function () local s = st.message({ to = "touser", from = "fromuser", id = "123", type = "chat" }, "Hello"); local e = errors.new({ condition = "internal-server-error", text = "Namespaced thing happened", extra = {namespace="xmpp:example.test", condition="this-happened"} }) local r = st.error_reply(s, e); assert.are.equal("xmpp:example.test", r.tags[1].tags[3].attr.xmlns); assert.are.equal("this-happened", r.tags[1].tags[3].name); local e2 = errors.new({ condition = "internal-server-error", text = "Namespaced thing happened", extra = {tag=st.stanza("that-happened", { xmlns = "xmpp:example.test", ["another-attribute"] = "here" })} }) local r2 = st.error_reply(s, e2); assert.are.equal("xmpp:example.test", r2.tags[1].tags[3].attr.xmlns); assert.are.equal("that-happened", r2.tags[1].tags[3].name); assert.are.equal("here", r2.tags[1].tags[3].attr["another-attribute"]); end); end); end); describe("#get_error()", function () describe("basics", function () local s = st.message(); local e = st.error_reply(s, "cancel", "not-acceptable", "UNACCEPTABLE!!!! ONE MILLION YEARS DUNGEON!") :tag("dungeon", { xmlns = "urn:uuid:c9026187-5b05-4e70-b265-c3b6338a7d0f", period="1000000years"}); 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.not_nil(extra) 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"] = "<>"; } local invalid_data = { ["number"] = 1234, ["table"] = {}; ["utf8"] = string.char(0xF4, 0x90, 0x80, 0x80); ["nil"] = "nil"; ["boolean"] = true; ["control characters"] = "\0\1\2\3"; }; for value_type, value in pairs(invalid_names) do it(value_type.." in tag names", function () assert.error_matches(function () st.stanza(value); end, value_type); end); it(value_type.." in attribute names", function () assert.error_matches(function () st.stanza("valid", { [value] = "valid" }); end, value_type); end); end for value_type, value in pairs(invalid_data) do if value == "nil" then value = nil; end it(value_type.." in tag names", function () assert.error_matches(function () st.stanza(value); end, value_type); end); it(value_type.." in attribute names", function () assert.error_matches(function () st.stanza("valid", { [value] = "valid" }); end, value_type); end); if value ~= nil then it(value_type.." in attribute values", function () assert.error_matches(function () st.stanza("valid", { valid = value }); end, value_type); end); it(value_type.." in text node", function () assert.error_matches(function () st.stanza("valid"):text(value); end, value_type); end); end end end); describe("#is_stanza", function () -- is_stanza(any) -> boolean it("identifies stanzas as stanzas", function () assert.truthy(st.is_stanza(st.stanza("x"))); end); it("identifies strings as not stanzas", function () assert.falsy(st.is_stanza("")); end); it("identifies numbers as not stanzas", function () assert.falsy(st.is_stanza(1)); end); it("identifies tables as not stanzas", function () assert.falsy(st.is_stanza({})); end); end); describe("#remove_children", function () it("should work", function () local s = st.stanza("x", {xmlns="test"}) :tag("y", {xmlns="test"}):up() :tag("z", {xmlns="test2"}):up() :tag("x", {xmlns="test2"}):up() s:remove_children("x"); assert.falsy(s:get_child("x")) assert.truthy(s:get_child("z","test2")); assert.truthy(s:get_child("x","test2")); s:remove_children(nil, "test2"); assert.truthy(s:get_child("y")) assert.falsy(s:get_child(nil,"test2")); s:remove_children(); assert.falsy(s.tags[1]); end); end); describe("#maptags", function () it("should work", function () local s = st.stanza("test") :tag("one"):up() :tag("two"):up() :tag("one"):up() :tag("three"):up(); local function one_filter(tag) if tag.name == "one" then return nil; end return tag; end assert.equal(4, #s.tags); s:maptags(one_filter); assert.equal(2, #s.tags); end); it("should work with multiple consecutive text nodes", function () local s = st.deserialize({ "\n"; { "away"; name = "show"; attr = {}; }; "\n"; { "I am away"; name = "status"; attr = {}; }; "\n"; { "0"; name = "priority"; attr = {}; }; "\n"; { name = "c"; attr = { xmlns = "http://jabber.org/protocol/caps"; node = "http://psi-im.org"; hash = "sha-1"; }; }; "\n"; "\n"; name = "presence"; attr = { to = "user@example.com/jflsjfld"; from = "room@chat.example.org/nick"; }; }); assert.equal(4, #s.tags); s:maptags(function (tag) return tag; end); assert.equal(4, #s.tags); s:maptags(function (tag) if tag.name == "c" then return nil; end return tag; end); assert.equal(3, #s.tags); end); it("errors on invalid data - #981", function () local s = st.message({}, "Hello"); s.tags[1] = st.clone(s.tags[1]); assert.has_error_match(function () s:maptags(function () end); end, "Invalid stanza"); end); end); describe("get_child_with_attr", function () local s = st.message({ type = "chat" }) :text_tag("body", "Hello world", { ["xml:lang"] = "en" }) :text_tag("body", "Bonjour le monde", { ["xml:lang"] = "fr" }) :text_tag("body", "Hallo Welt", { ["xml:lang"] = "de" }) it("works", function () assert.equal(s:get_child_with_attr("body", nil, "xml:lang", "en"):get_text(), "Hello world"); assert.equal(s:get_child_with_attr("body", nil, "xml:lang", "de"):get_text(), "Hallo Welt"); assert.equal(s:get_child_with_attr("body", nil, "xml:lang", "fr"):get_text(), "Bonjour le monde"); assert.is_nil(s:get_child_with_attr("body", nil, "xml:lang", "FR")); assert.is_nil(s:get_child_with_attr("body", nil, "xml:lang", "es")); end); it("supports normalization", function () assert.equal(s:get_child_with_attr("body", nil, "xml:lang", "EN", string.upper):get_text(), "Hello world"); assert.is_nil(s:get_child_with_attr("body", nil, "xml:lang", "ES", string.upper)); end); end); describe("#clone", function () it("works", function () local s = st.message({type="chat"}, "Hello"):reset(); local c = st.clone(s); assert.same(s, c); end); it("works", function () assert.has_error(function () st.clone("this is not a stanza"); end); end); end); describe("top_tag", function () local xml_parse = require "util.xml".parse; it("works", function () local s = st.message({type="chat"}, "Hello"); local top_tag = s:top_tag(); assert.is_string(top_tag); assert.not_equal("/>", top_tag:sub(-2, -1)); assert.equal(">", top_tag:sub(-1, -1)); local s2 = xml_parse(top_tag.."
"); assert(st.is_stanza(s2)); assert.equal("message", s2.name); assert.equal(0, #s2); assert.equal(0, #s2.tags); assert.equal("chat", s2.attr.type); end); it("works with namespaced attributes", function () local s = xml_parse[[]]; local top_tag = s:top_tag(); assert.is_string(top_tag); assert.not_equal("/>", top_tag:sub(-2, -1)); assert.equal(">", top_tag:sub(-1, -1)); local s2 = xml_parse(top_tag..""); assert(st.is_stanza(s2)); assert.equal("message", s2.name); assert.equal(0, #s2); assert.equal(0, #s2.tags); assert.equal("true", s2.attr["my-awesome-ns\1bar"]); end); end); describe("indent", function () local s = st.stanza("foo"):text("\n"):tag("bar"):tag("baz"):up():text_tag("cow", "moo"); assert.equal("\n\t\n\t\t\n\t\tmoo\n\t\n", tostring(s:indent())); assert.equal("\n \n \n moo\n \n", tostring(s:indent(1, " "))); assert.equal("\n\t\t\n\t\t\t\n\t\t\tmoo\n\t\t\n\t", tostring(s:indent(2, "\t"))); end); describe("find", function() it("works", function() local s = st.stanza("root", { attr = "value" }):tag("child", { xmlns = "urn:example:not:same"; childattr = "thisvalue" }):text_tag("nested", "text"):reset(); assert.equal("value", s:find("@attr"), "finds attr") assert.equal(s:get_child("child", "urn:example:not:same"), s:find("{urn:example:not:same}child"), "equivalent to get_child") assert.equal("thisvalue", s:find("{urn:example:not:same}child@childattr"), "finds child attr") 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); prosody-13.0.1/spec/PaxHeaders/util_strbitop_spec.lua0000644000000000000000000000011714773555365017752 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.759711204 prosody-13.0.1/spec/util_strbitop_spec.lua0000644000175000017500000000537614773555365022164 0ustar00prosodyprosody00000000000000local strbitop = require "util.strbitop"; describe("util.strbitop", function () describe("sand()", function () it("works", function () assert.equal(string.rep("Aa", 100), strbitop.sand(string.rep("a", 200), "Aa")); end); it("returns empty string if first argument is empty", function () assert.equal("", strbitop.sand("", "")); assert.equal("", strbitop.sand("", "key")); end); it("returns initial string if key is empty", function () assert.equal("hello", strbitop.sand("hello", "")); end); end); describe("sor()", function () it("works", function () assert.equal(string.rep("a", 200), strbitop.sor(string.rep("Aa", 100), "a")); end); it("returns empty string if first argument is empty", function () assert.equal("", strbitop.sor("", "")); assert.equal("", strbitop.sor("", "key")); end); it("returns initial string if key is empty", function () assert.equal("hello", strbitop.sor("hello", "")); end); end); describe("sxor()", function () it("works", function () assert.equal(string.rep("Aa", 100), strbitop.sxor(string.rep("a", 200), " \0")); end); it("returns empty string if first argument is empty", function () assert.equal("", strbitop.sxor("", "")); assert.equal("", strbitop.sxor("", "key")); end); it("returns initial string if key is empty", function () assert.equal("hello", strbitop.sxor("hello", "")); end); end); describe("common_prefix_bits()", function () local function B(s) assert(#s%8==0, "Invalid test input: B(s): s should be a multiple of 8 bits in length"); local byte = 0; local out_str = {}; for i = 1, #s do local bit_ascii = s:byte(i); if bit_ascii == 49 then -- '1' byte = byte + 2^((7-(i-1))%8); elseif bit_ascii ~= 48 then error("Invalid test input: B(s): s should contain only '0' or '1' characters"); end if (i-1)%8 == 7 then table.insert(out_str, string.char(byte)); byte = 0; end end return table.concat(out_str); end local _cpb = strbitop.common_prefix_bits; local function test(a, b) local Ba, Bb = B(a), B(b); local ret1 = _cpb(Ba, Bb); local ret2 = _cpb(Bb, Ba); assert(ret1 == ret2, ("parameter order should not make a difference to the result (%s, %s) = %d, reversed = %d"):format(a, b, ret1, ret2)); return ret1; end it("works on single bytes", function () assert.equal(0, test("00000000", "11111111")); assert.equal(1, test("10000000", "11111111")); assert.equal(0, test("01000000", "11111111")); assert.equal(0, test("01000000", "11111111")); assert.equal(8, test("11111111", "11111111")); end); it("works on multiple bytes", function () for i = 0, 16 do assert.equal(i, test(string.rep("1", i)..string.rep("0", 16-i), "1111111111111111")); end end); end); end); prosody-13.0.1/spec/PaxHeaders/util_table_spec.lua0000644000000000000000000000011714773555365017173 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.759711204 prosody-13.0.1/spec/util_table_spec.lua0000644000175000017500000000370114773555365021373 0ustar00prosodyprosody00000000000000local u_table = require "util.table"; describe("util.table", function () describe("create()", function () it("works", function () -- Can't test the allocated sizes of the table, so what you gonna do? assert.is.table(u_table.create(1,1)); end); end); describe("pack()", function () it("works", function () assert.same({ "lorem", "ipsum", "dolor", "sit", "amet", n = 5 }, u_table.pack("lorem", "ipsum", "dolor", "sit", "amet")); end); end); 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); prosody-13.0.1/spec/PaxHeaders/util_throttle_spec.lua0000644000000000000000000000011714773555365017751 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.763711219 prosody-13.0.1/spec/util_throttle_spec.lua0000644000175000017500000001022714773555365022152 0ustar00prosodyprosody00000000000000 -- luacheck: ignore 411/a -- Mock util.time local now = 0; -- wibbly-wobbly... timey-wimey... stuff local function later(n) now = now + n; -- time passes at a different rate end package.loaded["util.time"] = { now = function() return now; end } local throttle = require "util.throttle"; describe("util.throttle", function() describe("#create()", function() it("should be created with correct values", function() now = 5; local a = throttle.create(3, 10); assert.same(a, { balance = 3, max = 3, rate = 0.3, t = 5 }); local a = throttle.create(3, 5); assert.same(a, { balance = 3, max = 3, rate = 0.6, t = 5 }); local a = throttle.create(1, 1); assert.same(a, { balance = 1, max = 1, rate = 1, t = 5 }); local a = throttle.create(10, 10); assert.same(a, { balance = 10, max = 10, rate = 1, t = 5 }); local a = throttle.create(10, 1); assert.same(a, { balance = 10, max = 10, rate = 10, t = 5 }); end); end); describe("#update()", function() it("does nothing when no time has passed, even if balance is not full", function() now = 5; local a = throttle.create(10, 10); for i=1,5 do a:update(); assert.same(a, { balance = 10, max = 10, rate = 1, t = 5 }); end a.balance = 0; for i=1,5 do a:update(); assert.same(a, { balance = 0, max = 10, rate = 1, t = 5 }); end end); it("updates only time when time passes but balance is full", function() now = 5; local a = throttle.create(10, 10); for i=1,5 do later(5); a:update(); assert.same(a, { balance = 10, max = 10, rate = 1, t = 5 + i*5 }); end end); it("updates balance when balance has room to grow as time passes", function() now = 5; local a = throttle.create(10, 10); a.balance = 0; assert.same(a, { balance = 0, max = 10, rate = 1, t = 5 }); later(1); a:update(); assert.same(a, { balance = 1, max = 10, rate = 1, t = 6 }); later(3); a:update(); assert.same(a, { balance = 4, max = 10, rate = 1, t = 9 }); later(10); a:update(); assert.same(a, { balance = 10, max = 10, rate = 1, t = 19 }); end); it("handles 10 x 0.1s updates the same as 1 x 1s update ", function() now = 5; local a = throttle.create(1, 1); a.balance = 0; later(1); a:update(); assert.same(a, { balance = 1, max = 1, rate = 1, t = now }); a.balance = 0; for i=1,10 do later(0.1); a:update(); end assert(math.abs(a.balance - 1) < 0.0001); -- incremental updates cause rounding errors end); end); -- describe("po") describe("#poll()", function() it("should only allow successful polls until cost is hit", function() now = 5; local a = throttle.create(3, 10); assert.same(a, { balance = 3, max = 3, rate = 0.3, t = 5 }); assert.is_true(a:poll(1)); -- 3 -> 2 assert.same(a, { balance = 2, max = 3, rate = 0.3, t = 5 }); assert.is_true(a:poll(2)); -- 2 -> 1 assert.same(a, { balance = 0, max = 3, rate = 0.3, t = 5 }); assert.is_false(a:poll(1)); -- MEEP, out of credits! assert.is_false(a:poll(1)); -- MEEP, out of credits! assert.same(a, { balance = 0, max = 3, rate = 0.3, t = 5 }); end); it("should not allow polls more than the cost", function() now = 0; local a = throttle.create(10, 10); assert.same(a, { balance = 10, max = 10, rate = 1, t = 0 }); assert.is_false(a:poll(11)); assert.same(a, { balance = 10, max = 10, rate = 1, t = 0 }); assert.is_true(a:poll(6)); assert.same(a, { balance = 4, max = 10, rate = 1, t = 0 }); assert.is_false(a:poll(5)); assert.same(a, { balance = 4, max = 10, rate = 1, t = 0 }); -- fractional assert.is_true(a:poll(3.5)); assert.same(a, { balance = 0.5, max = 10, rate = 1, t = 0 }); assert.is_true(a:poll(0.25)); assert.same(a, { balance = 0.25, max = 10, rate = 1, t = 0 }); assert.is_false(a:poll(0.3)); assert.same(a, { balance = 0.25, max = 10, rate = 1, t = 0 }); assert.is_true(a:poll(0.25)); assert.same(a, { balance = 0, max = 10, rate = 1, t = 0 }); assert.is_false(a:poll(0.1)); assert.same(a, { balance = 0, max = 10, rate = 1, t = 0 }); assert.is_true(a:poll(0)); assert.same(a, { balance = 0, max = 10, rate = 1, t = 0 }); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_time_spec.lua0000644000000000000000000000011714773555365017042 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.763711219 prosody-13.0.1/spec/util_time_spec.lua0000644000175000017500000000120214773555365021234 0ustar00prosodyprosody00000000000000describe("util.time", function () local time; setup(function () time = require "util.time"; end); describe("now()", function () it("exists", function () assert.is_function(time.now); end); it("returns a number", function () assert.is_number(time.now()); end); end); describe("monotonic()", function () it("exists", function () assert.is_function(time.monotonic); end); it("returns a number", function () assert.is_number(time.monotonic()); end); it("time goes in one direction", function () local a = time.monotonic(); local b = time.monotonic(); assert.truthy(a <= b); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_uuid_spec.lua0000644000000000000000000000011714773555365017052 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.763711219 prosody-13.0.1/spec/util_uuid_spec.lua0000644000175000017500000000250514773555365021253 0ustar00prosodyprosody00000000000000-- This tests the format, not the randomness local uuid = require "util.uuid"; describe("util.uuid", function() describe("#generate()", function() it("should work follow the UUID pattern", function() -- https://www.rfc-editor.org/rfc/rfc4122.html#section-4.4 local pattern = "^" .. table.concat({ string.rep("%x", 8), string.rep("%x", 4), "4" .. -- version string.rep("%x", 3), "[89ab]" .. -- reserved bits of 1 and 0 string.rep("%x", 3), string.rep("%x", 12), }, "%-") .. "$"; 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); prosody-13.0.1/spec/PaxHeaders/util_xml_spec.lua0000644000000000000000000000011714773555365016704 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.763711219 prosody-13.0.1/spec/util_xml_spec.lua0000644000175000017500000000316514773555365021110 0ustar00prosodyprosody00000000000000 local xml = require "util.xml"; describe("util.xml", function() describe("#parse()", function() it("should work", function() local x = [[ ]] local stanza = xml.parse(x, {allow_comments = true}); assert.are.equal(stanza.tags[2].attr.xmlns, "b"); assert.are.equal(stanza.tags[2].namespaces["a"], "b"); end); it("should reject doctypes", function() local x = ""; local ok = xml.parse(x); assert.falsy(ok); end); it("should reject comments by default", function() local x = ""; local ok = xml.parse(x); assert.falsy(ok); end); it("should allow comments if asked nicely", function() local x = ""; local stanza = xml.parse(x, {allow_comments = true}); assert.are.equal(stanza.name, "foo"); assert.are.equal(#stanza, 0); end); it("should reject processing instructions", function() local x = ""; local ok = xml.parse(x); assert.falsy(ok); end); it("should allow processing instructions if asked nicely", function() local x = ""; local stanza = xml.parse(x, {allow_processing_instructions = true}); assert.truthy(stanza); assert.are.equal(stanza.name, "foo"); end); it("should allow an xml declaration", function() local x = ""; local stanza = xml.parse(x); assert.truthy(stanza); assert.are.equal(stanza.name, "foo"); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_xmppstream_spec.lua0000644000000000000000000000011714773555365020304 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.763711219 prosody-13.0.1/spec/util_xmppstream_spec.lua0000644000175000017500000001116214773555365022504 0ustar00prosodyprosody00000000000000 local xmppstream = require "util.xmppstream"; describe("util.xmppstream", function() local function test(xml, expect_success, ex) local stanzas = {}; local session = { notopen = true }; local callbacks = { stream_ns = "streamns"; stream_tag = "stream"; default_ns = "stanzans"; streamopened = function (_session) assert.are.equal(session, _session); assert.are.equal(session.notopen, true); _session.notopen = nil; return true; end; handlestanza = function (_session, stanza) assert.are.equal(session, _session); assert.are.equal(_session.notopen, nil); table.insert(stanzas, stanza); end; streamclosed = function (_session) assert.are.equal(session, _session); assert.are.equal(_session.notopen, nil); _session.notopen = nil; end; } if type(ex) == "table" then for k, v in pairs(ex) do if k ~= "_size_limit" then callbacks[k] = v; end end end local stream = xmppstream.new(session, callbacks, ex and ex._size_limit or nil); local ok, err = pcall(function () assert(stream:feed(xml)); end); if ok and type(expect_success) == "function" then expect_success(stanzas); end assert.are.equal(not not ok, not not expect_success, "Expected "..(expect_success and ("success ("..tostring(err)..")") or "failure")); end local function test_stanza(stanza, expect_success, ex) return test([[]]..stanza, expect_success, ex); end describe("#new()", function() it("should work", function() test([[]], true); test([[]], true); -- Incorrect stream tag name should be rejected test([[]], false); -- Incorrect stream namespace should be rejected test([[]], false); -- Invalid XML should be rejected test("<>", false); test_stanza("", function (stanzas) assert.are.equal(#stanzas, 1); assert.are.equal(stanzas[1].name, "message"); end); test_stanza("< message>>>>/>\n", false); test_stanza([[ ]], function (stanzas) assert.are.equal(#stanzas, 1); local s = stanzas[1]; assert.are.equal(s.name, "x"); assert.are.equal(#s.tags, 2); assert.are.equal(s.tags[1].name, "y"); assert.are.equal(s.tags[1].attr.xmlns, nil); assert.are.equal(s.tags[1].tags[1].name, "z"); assert.are.equal(s.tags[1].tags[1].attr.xmlns, "c"); assert.are.equal(s.tags[2].name, "z"); assert.are.equal(s.tags[2].attr.xmlns, "b"); assert.are.equal(s.namespaces, nil); end); end); end); it("should allow an XML declaration", function () test([[]], true); test([[]], true); test([[]], true); end); it("should not accept XML versions other than 1.0", function () test([[]], false); end); it("should not allow a misplaced XML declaration", function () test([[]], false); end); describe("should forbid restricted XML:", function () it("comments", function () test_stanza("", false); end); it("DOCTYPE", function () test([[]], false); end); it("incorrect encoding specification", function () -- This is actually caught by the underlying XML parser test([[]], false); end); it("non-UTF8 encodings: ISO-8859-1", function () test([[]], false); end); it("non-UTF8 encodings: UTF-16", function () -- -- encoded into UTF-16 local hx = ([[fffe3c003f0078006d006c002000760065007200730069006f006e003d00 220031002e0030002200200065006e0063006f00640069006e0067003d00 22005500540046002d003100360022003f003e003c007300740072006500 61006d00200078006d006c006e0073003d00220073007400720065006100 6d006e00730022002f003e00]]):gsub("%x%x", function (c) return string.char(tonumber(c, 16)); end); test(hx, false); end); it("processing instructions", function () test([[]], false); end); end); end); prosody-13.0.1/spec/PaxHeaders/util_xtemplate_spec.lua0000644000000000000000000000011714773555365020107 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.763711219 prosody-13.0.1/spec/util_xtemplate_spec.lua0000644000175000017500000000450114773555365022306 0ustar00prosodyprosody00000000000000local 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) prosody-13.0.1/PaxHeaders/teal-src0000644000000000000000000000013114773555365014031 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.763711219 30 ctime=1743706869.767711236 prosody-13.0.1/teal-src/0000755000175000017500000000000014773555365016311 5ustar00prosodyprosody00000000000000prosody-13.0.1/teal-src/PaxHeaders/README.md0000644000000000000000000000011714773555365015371 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.767711236 prosody-13.0.1/teal-src/README.md0000644000175000017500000000244114773555365017571 0ustar00prosodyprosody00000000000000# 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. prosody-13.0.1/teal-src/PaxHeaders/module.d.tl0000644000000000000000000000011714773555365016162 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.767711236 prosody-13.0.1/teal-src/module.d.tl0000644000175000017500000001202714773555365020363 0ustar00prosodyprosody00000000000000local st = require "prosody.util.stanza" global record moduleapi get_name : function (moduleapi) : string get_host : function (moduleapi) : string enum host_type "global" "local" "component" end get_host_type : function (moduleapi) : host_type set_global : function (moduleapi) add_feature : function (moduleapi, string) add_identity : function (moduleapi, string, string, string) -- TODO enum? add_extension : function (moduleapi, st.stanza_t) fire_event : function (moduleapi, string, any) : any type handler = function (any) : any record util_events -- TODO import def end hook_object_event : function (moduleapi, util_events, string, handler, number) unhook_object_event : function (moduleapi, util_events, string, handler) hook : function (moduleapi, string, handler, number) hook_global : function (moduleapi, string, handler, number) hook_tag : function (moduleapi, string, string, handler, number) unhook : function (moduleapi, string, handler) wrap_object_event : function (moduleapi, util_events, string, handler) wrap_event : function (moduleapi, string, handler) wrap_global : function (moduleapi, string, handler) require : function (moduleapi, string) : table depends : function (moduleapi, string) : table shared : function (moduleapi, string) : table type config_getter = function
(moduleapi, string, A) : A get_option : config_getter get_option_scalar : config_getter get_option_string : config_getter get_option_number : function (moduleapi, string, number, number, number) : number get_option_integer : function (moduleapi, string, integer, integer, integer) : integer get_option_boolean : config_getter get_option_enum : function (moduleapi, string, ... : A) : A get_option_period : function (moduleapi, string|number, string|number, string|number, string|number) : number record util_array -- TODO import def { any } end get_option_array : config_getter record util_set -- TODO import def _items : { any : boolean } end get_option_set : function (moduleapi, string, { any }) : util_set get_option_inherited_set : function (moduleapi, string, { any }) : util_set get_option_path : function (moduleapi, string, string, string) : string context : function (moduleapi, string) : moduleapi add_item : function (moduleapi, string, any) remove_item : function (moduleapi, string, any) get_host_items : function (moduleapi, string) : { any } handle_items : function (moduleapi, string, handler, handler, boolean) provides : function (moduleapi, string, table) record util_session -- TODO import def send : function ( st.stanza_t | string ) end send : function (moduleapi, st.stanza_t, util_session) 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 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+" end load_resource : function (moduleapi, string, file_mode) : FILE enum store_type "keyval" "keyval+" "map" "archive" end open_store : function (moduleapi, string, store_type) enum stat_type "amount" "counter" "rate" "distribution" "sizes" "times" end record stats_conf initial : number units : string type : string end measure : function (moduleapi, string, stat_type, stats_conf) measure_object_event : function (moduleapi, util_events, string, string) measure_event : function (moduleapi, string, string) measure_global_event : function (moduleapi, string, string) enum status_type "error" "warn" "info" "core" end set_status : function (moduleapi, status_type, string, boolean) enum log_level "debug" "info" "warn" "error" end log_status : function (moduleapi, log_level, string, ... : any) get_status : function (moduleapi) : status_type, string, number -- added by modulemanager name : string host : string _log : function (log_level, string, ... : any) log : function (moduleapi, log_level, string, ... : any) reloading : boolean saved_state : any record module_environment module : moduleapi end environment : module_environment 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 global record common_event stanza : st.stanza_t record origin send : function (st.stanza_t) end end global record prosody version : string end return module prosody-13.0.1/teal-src/PaxHeaders/prosody0000644000000000000000000000013114773555365015530 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.767711236 30 ctime=1743706869.775711268 prosody-13.0.1/teal-src/prosody/0000755000175000017500000000000014773555365020010 5ustar00prosodyprosody00000000000000prosody-13.0.1/teal-src/prosody/PaxHeaders/core0000644000000000000000000000013114773555365016460 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.767711236 30 ctime=1743706869.767711236 prosody-13.0.1/teal-src/prosody/core/0000755000175000017500000000000014773555365020740 5ustar00prosodyprosody00000000000000prosody-13.0.1/teal-src/prosody/core/PaxHeaders/storagemanager.d.tl0000644000000000000000000000011714773555365022323 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.767711236 prosody-13.0.1/teal-src/prosody/core/storagemanager.d.tl0000644000175000017500000000454614773555365024533 0ustar00prosodyprosody00000000000000-- 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 prosody-13.0.1/teal-src/prosody/core/PaxHeaders/usermanager.d.tl0000644000000000000000000000011714773555365021635 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.767711236 prosody-13.0.1/teal-src/prosody/core/usermanager.d.tl0000644000175000017500000000435014773555365024036 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/PaxHeaders/net0000644000000000000000000000013114773555365016316 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.767711236 30 ctime=1743706869.771711252 prosody-13.0.1/teal-src/prosody/net/0000755000175000017500000000000014773555365020576 5ustar00prosodyprosody00000000000000prosody-13.0.1/teal-src/prosody/net/PaxHeaders/http0000644000000000000000000000013114773555365017275 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.767711236 30 ctime=1743706869.771711252 prosody-13.0.1/teal-src/prosody/net/http/0000755000175000017500000000000014773555365021555 5ustar00prosodyprosody00000000000000prosody-13.0.1/teal-src/prosody/net/http/PaxHeaders/codes.d.tl0000644000000000000000000000011714773555365021236 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.767711236 prosody-13.0.1/teal-src/prosody/net/http/codes.d.tl0000644000175000017500000000010714773555365023433 0ustar00prosodyprosody00000000000000local type response_codes = { integer : string } return response_codes prosody-13.0.1/teal-src/prosody/net/http/PaxHeaders/errors.d.tl0000644000000000000000000000011714773555365021455 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.771711252 prosody-13.0.1/teal-src/prosody/net/http/errors.d.tl0000644000175000017500000000077414773555365023664 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/net/http/PaxHeaders/files.d.tl0000644000000000000000000000011714773555365021243 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.771711252 prosody-13.0.1/teal-src/prosody/net/http/files.d.tl0000644000175000017500000000043314773555365023442 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/net/http/PaxHeaders/parser.d.tl0000644000000000000000000000011714773555365021435 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.771711252 prosody-13.0.1/teal-src/prosody/net/http/parser.d.tl0000644000175000017500000000166414773555365023643 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/net/http/PaxHeaders/server.d.tl0000644000000000000000000000011714773555365021447 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.771711252 prosody-13.0.1/teal-src/prosody/net/http/server.d.tl0000644000175000017500000000007314773555365023646 0ustar00prosodyprosody00000000000000 local record http_server -- TODO end return http_server prosody-13.0.1/teal-src/prosody/net/PaxHeaders/http.d.tl0000644000000000000000000000011714773555365020141 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.767711236 prosody-13.0.1/teal-src/prosody/net/http.d.tl0000644000175000017500000000315614773555365022345 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/net/PaxHeaders/server.d.tl0000644000000000000000000000011714773555365020470 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.771711252 prosody-13.0.1/teal-src/prosody/net/server.d.tl0000644000175000017500000000313014773555365022664 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/PaxHeaders/plugins0000644000000000000000000000013114773555365017211 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.771711252 30 ctime=1743706869.775711268 prosody-13.0.1/teal-src/prosody/plugins/0000755000175000017500000000000014773555365021471 5ustar00prosodyprosody00000000000000prosody-13.0.1/teal-src/prosody/plugins/PaxHeaders/mod_cron.tl0000644000000000000000000000011714773555365021433 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.771711252 prosody-13.0.1/teal-src/prosody/plugins/mod_cron.tl0000644000175000017500000001005614773555365023634 0ustar00prosodyprosody00000000000000module:set_global(); local async = require "prosody.util.async"; local cron_initial_delay = module:get_option_number("cron_initial_delay", 1); local cron_check_delay = module:get_option_number("cron_check_delay", 3600); local cron_spread_factor = module:get_option_number("cron_spread_factor", 0); local record map_store -- TODO move to somewhere sensible get : function (map_store, string, K) : V set : function (map_store, string, K, V) end local enum frequency "hourly" "daily" "weekly" end local record task_spec id : string -- unique id name : string -- name or short description when : frequency period : number last : integer run : function (task_spec, integer) save : function (task_spec, integer) restore : function (task_spec, integer) end local record task_event source : module item : task_spec end local active_hosts : { string : boolean } = { } function module.add_host(host_module : moduleapi) local last_run_times = host_module:open_store("cron", "map") as map_store; active_hosts[host_module.host] = true; local function save_task(task : task_spec, started_at : integer) last_run_times:set(nil, task.id, started_at); end local function restore_task(task : task_spec) if task.last == nil then task.last = last_run_times:get(nil, task.id); end end local function task_added(event : task_event) : boolean local task = event.item; if task.name == nil then task.name = task.when; end if task.id == nil then task.id = event.source.name .. "/" .. task.name:gsub("%W", "_"):lower(); end task.period = host_module:get_option_period(task.id:gsub("/", "_") .. "_period", "1" .. task.when, 60, 86400*7*53); task.restore = restore_task; task.save = save_task; module:log("debug", "%s task %s added", task.when, task.id); return true; end local function task_removed(event : task_event) : boolean local task = event.item; host_module:log("debug", "Task %s removed", task.id); return true; end host_module:handle_items("task", task_added, task_removed, true); function host_module.unload() active_hosts[host_module.host]=nil; end end local function should_run(task : task_spec, last : integer) : boolean return not last or last + task.period * 0.995 <= os.time(); end local function run_task(task : task_spec) task:restore(); if not should_run(task, task.last) then return; end local started_at = os.time(); task:run(started_at); task.last = started_at; task:save(started_at); end local function spread(t : number, factor : number) : number return t * (1 - factor + 2*factor*math.random()); end local task_runner : async.runner_t = async.runner(run_task); scheduled = module:add_timer(cron_initial_delay, function() : number module:log("info", "Running periodic tasks"); local delay = spread(cron_check_delay, cron_spread_factor); for host in pairs(active_hosts) do module:log("debug", "Running periodic tasks for host %s", host); for _, task in ipairs(module:context(host):get_host_items("task") as { task_spec } ) do task_runner:run(task); end end module:log("debug", "Wait %gs", delay); return delay; end); -- TODO measure load, pick a good time to do stuff module:add_item("shell-command", { section = "cron"; section_desc = "View and manage recurring tasks"; name = "tasks"; desc = "View registered tasks"; args = {}; handler = function (self, filter_host : string) local format_table = require "prosody.util.human.io".table; local it = require "util.iterators"; local row = format_table({ { title = "Host", width = "2p" }; { title = "Task", width = "3p" }; { title = "Desc", width = "3p" }; { title = "When", width = "1p" }; { title = "Last run", width = "20" }; }, self.session.width); local print = self.session.print; print(row()); for host in it.sorted_pairs(filter_host and { [filter_host]=true } or active_hosts) do for _, task in ipairs(module:context(host):get_host_items("task")) do print(row { host, task.id, task.name, task.when, task.last and os.date("%Y-%m-%d %R:%S", task.last) or "never" }); end end end; }); prosody-13.0.1/teal-src/prosody/plugins/PaxHeaders/muc0000644000000000000000000000013114773555365017775 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.775711268 30 ctime=1743706869.775711268 prosody-13.0.1/teal-src/prosody/plugins/muc/0000755000175000017500000000000014773555365022255 5ustar00prosodyprosody00000000000000prosody-13.0.1/teal-src/prosody/plugins/muc/PaxHeaders/muc.lib.d.tl0000644000000000000000000000011714773555365022172 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.775711268 prosody-13.0.1/teal-src/prosody/plugins/muc/muc.lib.d.tl0000644000175000017500000001642014773555365024374 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/PaxHeaders/util0000644000000000000000000000013114773555365016505 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.775711268 30 ctime=1743706869.795711348 prosody-13.0.1/teal-src/prosody/util/0000755000175000017500000000000014773555365020765 5ustar00prosodyprosody00000000000000prosody-13.0.1/teal-src/prosody/util/PaxHeaders/array.d.tl0000644000000000000000000000011714773555365020467 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.775711268 prosody-13.0.1/teal-src/prosody/util/array.d.tl0000644000175000017500000000016014773555365022663 0ustar00prosodyprosody00000000000000local record array_t { T } end local record lib metamethod __call : function () : array_t end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/async.d.tl0000644000000000000000000000011714773555365020466 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.775711268 prosody-13.0.1/teal-src/prosody/util/async.d.tl0000644000175000017500000000250014773555365022662 0ustar00prosodyprosody00000000000000local record lib ready : function () : boolean waiter : function (num : integer, allow_many : boolean) : function (), function () guarder : function () : function (id : function ()) : function () | nil record runner_t func : function (T) thread : thread enum state_e -- from Lua manual "running" "suspended" "normal" "dead" -- from util.async "ready" "error" end state : state_e notified_state : state_e queue : { T } type watcher_t = function (runner_t, ... : any) type watchers_t = { state_e : watcher_t } data : any id : string run : function (runner_t, T) : boolean, state_e, integer enqueue : function (runner_t, T) : runner_t log : function (runner_t, string, string, ... : any) onready : function (runner_t, function) : runner_t onready : function (runner_t, function) : runner_t onwaiting : function (runner_t, function) : runner_t onerror : function (runner_t, function) : runner_t end runner : function (function (T), runner_t.watchers_t, any) : runner_t wait_for : function (any) : any, any sleep : function (t:number) -- set_nexttick = function(new_next_tick) next_tick = new_next_tick; end; -- set_schedule_function = function (new_schedule_function) schedule_task = new_schedule_function; end; end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/bitcompat.d.tl0000644000000000000000000000011714773555365021333 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.775711268 prosody-13.0.1/teal-src/prosody/util/bitcompat.d.tl0000644000175000017500000000046614773555365023540 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/compat.d.tl0000644000000000000000000000011714773555365020634 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.775711268 prosody-13.0.1/teal-src/prosody/util/compat.d.tl0000644000175000017500000000013614773555365023033 0ustar00prosodyprosody00000000000000local record lib xpcall : function (function, function, ...:any):boolean, any end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/crand.d.tl0000644000000000000000000000011714773555365020440 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.775711268 prosody-13.0.1/teal-src/prosody/util/crand.d.tl0000644000175000017500000000022114773555365022632 0ustar00prosodyprosody00000000000000local record lib bytes : function (n : integer) : string enum sourceid "OpenSSL" "arc4random()" "Linux" end _source : sourceid end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/crypto.d.tl0000644000000000000000000000011714773555365020671 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.775711268 prosody-13.0.1/teal-src/prosody/util/crypto.d.tl0000644000175000017500000000347214773555365023076 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/dataforms.d.tl0000644000000000000000000000011714773555365021331 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.775711268 prosody-13.0.1/teal-src/prosody/util/dataforms.d.tl0000644000175000017500000000156114773555365023533 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/datamapper.tl0000644000000000000000000000011714773555365021245 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.779711284 prosody-13.0.1/teal-src/prosody/util/datamapper.tl0000644000175000017500000002755414773555365023461 0ustar00prosodyprosody00000000000000-- Copyright (C) 2021 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- Based on -- https://json-schema.org/draft/2020-12/json-schema-core.html -- https://json-schema.org/draft/2020-12/json-schema-validation.html -- http://spec.openapis.org/oas/v3.0.1#xmlObject -- https://github.com/OAI/OpenAPI-Specification/issues/630 (text:true) -- -- XML Object Extensions: -- text to refer to the text content at the same time as attributes -- x_name_is_value for enum fields where the is the value -- x_single_attribute for -- -- TODO pointers -- TODO cleanup / refactor -- TODO s/number/integer/ once we have appropriate math.type() compat -- if not math.type then require "prosody.util.mathcompat" end local st = require "prosody.util.stanza"; local json = require "prosody.util.json" local pointer = require "prosody.util.jsonpointer"; local json_type_name = json.json_type_name; local json_schema_object = require "prosody.util.jsonschema" local type schema_t = boolean | json_schema_object local function toboolean ( s : string ) : boolean if s == "true" or s == "1" then return true elseif s == "false" or s == "0" then return false elseif s then return true end end local function totype(t : json_type_name, s : string) : any if not s then return nil end if t == "string" then return s; elseif t == "boolean" then return toboolean(s) elseif t == "number" or t == "integer" then return tonumber(s) end end local enum value_goes "in_tag_name" "in_text" "in_text_tag" "in_attribute" "in_single_attribute" "in_children" "in_wrapper" end local function resolve_schema(schema : schema_t, root : json_schema_object) : schema_t if schema is json_schema_object then if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then return pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t; end end return schema; end local function guess_schema_type(schema : json_schema_object) : json_type_name local schema_types = schema.type if schema_types is json_type_name then return schema_types elseif schema_types ~= nil then error "schema has unsupported 'type' property" elseif schema.properties then return "object" elseif schema.items then return "array" end return "string" -- default assumption end local function unpack_propschema( propschema : schema_t, propname : string, current_ns : string ) : json_type_name, value_goes, string, string, string, string, { any } local proptype : json_type_name = "string" local value_where : value_goes = propname and "in_text_tag" or "in_text" local name = propname local namespace : string local prefix : string local single_attribute : string local enums : { any } if propschema is json_schema_object then proptype = guess_schema_type(propschema); elseif propschema is string then -- Teal says this can never be a string, but it could before so best be sure error("schema as string is not supported: "..propschema.." {"..current_ns.."}"..propname) end if proptype == "object" or proptype == "array" then value_where = "in_children" end if propschema is json_schema_object then local xml = propschema.xml if xml then if xml.name then name = xml.name end if xml.namespace and xml.namespace ~= current_ns then namespace = xml.namespace end if xml.prefix then prefix = xml.prefix end if proptype == "array" and xml.wrapped then value_where = "in_wrapper" elseif xml.attribute then value_where = "in_attribute" elseif xml.text then value_where = "in_text" elseif xml.x_name_is_value then value_where = "in_tag_name" elseif xml.x_single_attribute then single_attribute = xml.x_single_attribute value_where = "in_single_attribute" end end if propschema["const"] then enums = { propschema["const"] } elseif propschema["enum"] then enums = propschema["enum"] end end return proptype, value_where, name, namespace, prefix, single_attribute, enums end local parse_object : function (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { string : any } local parse_array : function (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { any } local function extract_value (s : st.stanza_t, value_where : value_goes, proptype : json.json_type_name, name : string, namespace : string, prefix : string, single_attribute : string, enums : { any }) : string if value_where == "in_tag_name" then local c : st.stanza_t if proptype == "boolean" then c = s:get_child(name, namespace); elseif enums and proptype == "string" then -- XXX O(n²) ? -- Probably better to flip the table and loop over :childtags(nil, ns), should be 2xO(n) -- BUT works first, optimize later for i = 1, #enums do c = s:get_child(enums[i] as string, namespace); if c then break end end else c = s:get_child(nil, namespace); end if c then return c.name; end elseif value_where == "in_attribute" then local attr = name if prefix then attr = prefix .. ':' .. name elseif namespace and namespace ~= s.attr.xmlns then attr = namespace .. "\1" .. name end return s.attr[attr] elseif value_where == "in_text" then return s:get_text() elseif value_where == "in_single_attribute" then local c = s:get_child(name, namespace) return c and c.attr[single_attribute] elseif value_where == "in_text_tag" then return s:get_child_text(name, namespace) end end function parse_object (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { string : any } local out : { string : any } = {} schema = resolve_schema(schema, root) if schema is json_schema_object and schema.properties then for prop, propschema in pairs(schema.properties) do propschema = resolve_schema(propschema, root) local proptype, value_where, name, namespace, prefix, single_attribute, enums = unpack_propschema(propschema, prop, s.attr.xmlns) if value_where == "in_children" and propschema is json_schema_object then if proptype == "object" then local c = s:get_child(name, namespace) if c then out[prop] = parse_object(propschema, c, root); end elseif proptype == "array" then local a = parse_array(propschema, s, root); if a and a[1] ~= nil then out[prop] = a; end else error "unreachable" end elseif value_where == "in_wrapper" and propschema is json_schema_object and proptype == "array" then local wrapper = s:get_child(name, namespace); if wrapper then out[prop] = parse_array(propschema, wrapper, root); end else local value : string = extract_value (s, value_where, proptype, name, namespace, prefix, single_attribute, enums) out[prop] = totype(proptype, value) end end end return out end function parse_array (schema : json_schema_object, s : st.stanza_t, root : json_schema_object) : { any } local itemschema : schema_t = resolve_schema(schema.items, root); local proptype, value_where, child_name, namespace, prefix, single_attribute, enums = unpack_propschema(itemschema, nil, s.attr.xmlns) local attr_name : string if value_where == "in_single_attribute" then -- FIXME this shouldn't be needed value_where = "in_attribute"; attr_name = single_attribute; end local out : { any } = {} if proptype == "object" then if itemschema is json_schema_object then for c in s:childtags(child_name, namespace) do table.insert(out, parse_object(itemschema, c, root)); end else error "array items must be schema object" end elseif proptype == "array" then if itemschema is json_schema_object then for c in s:childtags(child_name, namespace) do table.insert(out, parse_array(itemschema, c, root)); end end else for c in s:childtags(child_name, namespace) do local value : string = extract_value (c, value_where, proptype, attr_name or child_name, namespace, prefix, single_attribute, enums) table.insert(out, totype(proptype, value)); end end return out; end local function parse (schema : json_schema_object, s : st.stanza_t) : table local s_type = guess_schema_type(schema) if s_type == "object" then return parse_object(schema, s, schema) elseif s_type == "array" then return parse_array(schema, s, schema) else error "top-level scalars unsupported" end end local function toxmlstring(proptype : json_type_name, v : any) : string if proptype == "string" and v is string then return v elseif proptype == "number" and v is number then return string.format("%g", v) elseif proptype == "integer" and v is number then -- TODO is integer return string.format("%d", v) elseif proptype == "boolean" then return v and "1" or "0" end end local unparse : function (json_schema_object, table, string, string, st.stanza_t, json_schema_object) : st.stanza_t local function unparse_property(out : st.stanza_t, v : any, proptype : json_type_name, propschema : schema_t, value_where : value_goes, name : string, namespace : string, current_ns : string, prefix : string, single_attribute : string, root : json_schema_object) if value_where == "in_attribute" then local attr = name if prefix then attr = prefix .. ':' .. name elseif namespace and namespace ~= current_ns then attr = namespace .. "\1" .. name end out.attr[attr] = toxmlstring(proptype, v) elseif value_where == "in_text" then out:text(toxmlstring(proptype, v)) elseif value_where == "in_single_attribute" then assert(single_attribute) local propattr : { string : string } = {} if namespace and namespace ~= current_ns then propattr.xmlns = namespace end propattr[single_attribute] = toxmlstring(proptype, v) out:tag(name, propattr):up(); else local propattr : { string : string } if namespace ~= current_ns then propattr = { xmlns = namespace } end if value_where == "in_tag_name" then if proptype == "string" and v is string then out:tag(v, propattr):up(); elseif proptype == "boolean" and v == true then out:tag(name, propattr):up(); end elseif proptype == "object" and propschema is json_schema_object and v is table then local c = unparse(propschema, v, name, namespace, nil, root); if c then out:add_direct_child(c); end elseif proptype == "array" and propschema is json_schema_object and v is table then if value_where == "in_wrapper" then local c = unparse(propschema, v, name, namespace, nil, root); if c then out:add_direct_child(c); end else unparse(propschema, v, name, namespace, out, root); end else out:text_tag(name, toxmlstring(proptype, v), propattr) end end end function unparse ( schema : json_schema_object, t : table, current_name : string, current_ns : string, ctx : st.stanza_t, root : json_schema_object ) : st.stanza_t if root == nil then root = schema end if schema.xml then if schema.xml.name then current_name = schema.xml.name end if schema.xml.namespace then current_ns = schema.xml.namespace end -- TODO prefix? end local out = ctx or st.stanza(current_name, { xmlns = current_ns }) local s_type = guess_schema_type(schema) if s_type == "object" then for prop, propschema in pairs(schema.properties) do propschema = resolve_schema(propschema, root) local v = t[prop] if v ~= nil then local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(propschema, prop, current_ns) unparse_property(out, v, proptype, propschema, value_where, name, namespace, current_ns, prefix, single_attribute, root) end end return out; elseif s_type == "array" then local itemschema = resolve_schema(schema.items, root) local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns) for _, item in ipairs(t as { string }) do unparse_property(out, item, proptype, itemschema, value_where, name, namespace, current_ns, prefix, single_attribute, root) end return out; end end return { parse = parse, unparse = unparse, } prosody-13.0.1/teal-src/prosody/util/PaxHeaders/datetime.d.tl0000644000000000000000000000011714773555365021145 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.779711284 prosody-13.0.1/teal-src/prosody/util/datetime.d.tl0000644000175000017500000000037014773555365023344 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/encodings.d.tl0000644000000000000000000000011714773555365021322 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.779711284 prosody-13.0.1/teal-src/prosody/util/encodings.d.tl0000644000175000017500000000136114773555365023522 0ustar00prosodyprosody00000000000000-- TODO many actually return Maybe(String) local record lib record base64 encode : function (s : string) : string decode : function (s : string) : string end record stringprep nameprep : function (s : string, strict : boolean) : string nodeprep : function (s : string, strict : boolean) : string resourceprep : function (s : string, strict : boolean) : string saslprep : function (s : string, strict : boolean) : string end record idna to_ascii : function (s : string) : string to_unicode : function (s : string) : string end record utf8 valid : function (s : string) : boolean length : function (s : string) : integer end record confusable skeleton : function (s : string) : string end version : string end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/error.d.tl0000644000000000000000000000011714773555365020502 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.779711284 prosody-13.0.1/teal-src/prosody/util/error.d.tl0000644000175000017500000000337614773555365022712 0ustar00prosodyprosody00000000000000local enum error_type "auth" "cancel" "continue" "modify" "wait" end local enum error_condition "bad-request" "conflict" "feature-not-implemented" "forbidden" "gone" "internal-server-error" "item-not-found" "jid-malformed" "not-acceptable" "not-allowed" "not-authorized" "policy-violation" "recipient-unavailable" "redirect" "registration-required" "remote-server-not-found" "remote-server-timeout" "resource-constraint" "service-unavailable" "subscription-required" "undefined-condition" "unexpected-request" end local record protoerror type : error_type condition : error_condition text : string code : integer end local record Error type : error_type condition : error_condition text : string code : integer context : { any : any } source : string end local type compact_registry_item = { string, string, string, string } local type compact_registry = { compact_registry_item } local type registry = { string : protoerror } local type context = { string : any } local record error_registry_wrapper source : string registry : registry new : function (string, context) : Error coerce : function (any, string) : any, Error wrap : function (Error) : Error wrap : function (string, context) : Error is_error : function (any) : boolean end local record lib record configure_opt auto_inject_traceback : boolean end new : function (protoerror, context, { string : protoerror }, string) : Error init : function (string, string, registry | compact_registry) : error_registry_wrapper init : function (string, registry | compact_registry) : error_registry_wrapper is_error : function (any) : boolean coerce : function (any, string) : any, Error from_stanza : function (table, context, string) : Error configure : function end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/format.d.tl0000644000000000000000000000011714773555365020641 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.779711284 prosody-13.0.1/teal-src/prosody/util/format.d.tl0000644000175000017500000000012014773555365023031 0ustar00prosodyprosody00000000000000local record lib format : function (string, ... : any) : string end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/hashes.d.tl0000644000000000000000000000011714773555365020624 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.779711284 prosody-13.0.1/teal-src/prosody/util/hashes.d.tl0000644000175000017500000000150214773555365023021 0ustar00prosodyprosody00000000000000local type hash = function (msg : string, hex : boolean) : string local type hmac = function (key : string, msg : string, hex : boolean) : string local type kdf = function (pass : string, salt : string, i : integer) : string local record lib sha1 : hash sha224 : hash sha256 : hash sha384 : hash sha512 : hash md5 : hash sha3_256 : hash sha3_512 : hash blake2s256 : hash blake2b512 : hash hmac_sha1 : hmac hmac_sha224 : hmac hmac_sha256 : hmac hmac_sha384 :hmac hmac_sha512 : hmac hmac_md5 : hmac hmac_sha3_256 : hmac hmac_sha3_512 : hmac hmac_blake2s256 : hmac hmac_blake2b512 : hmac scram_Hi_sha1 : kdf pbkdf2_hmac_sha1 : kdf pbkdf2_hmac_sha256 : kdf hkdf_hmac_sha256 : kdf hkdf_hmac_sha384 : kdf equals : function (string, string) : boolean version : string _LIBCRYPTO_VERSION : string end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/hex.d.tl0000644000000000000000000000011714773555365020135 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.779711284 prosody-13.0.1/teal-src/prosody/util/hex.d.tl0000644000175000017500000000020214773555365022326 0ustar00prosodyprosody00000000000000local type s2s = function (s : string) : string local record lib to : s2s from : s2s encode : s2s decode : s2s end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/http.d.tl0000644000000000000000000000011714773555365020330 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.779711284 prosody-13.0.1/teal-src/prosody/util/http.d.tl0000644000175000017500000000055414773555365022533 0ustar00prosodyprosody00000000000000local record lib urlencode : function (s : string) : string urldecode : function (s : string) : string formencode : function (f : { string : string }) : string formdecode : function (s : string) : { string : string } contains_token : function (field : string, token : string) : boolean normalize_path : function (path : string) : string end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/human0000644000000000000000000000013114773555365017615 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.783711299 30 ctime=1743706869.783711299 prosody-13.0.1/teal-src/prosody/util/human/0000755000175000017500000000000014773555365022075 5ustar00prosodyprosody00000000000000prosody-13.0.1/teal-src/prosody/util/human/PaxHeaders/io.d.tl0000644000000000000000000000011714773555365021070 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.783711299 prosody-13.0.1/teal-src/prosody/util/human/io.d.tl0000644000175000017500000000144414773555365023272 0ustar00prosodyprosody00000000000000local record lib getchar : function (n : integer) : string getline : function () : string getpass : function () : string show_yesno : function (prompt : string) : boolean read_password : function () : string show_prompt : function (prompt : string) : boolean printf : function (fmt : string, ... : any) padleft : function (s : string, width : integer) : string padright : function (s : string, width : integer) : string -- {K:V} vs T ? record tablerow width : integer | string -- generate an 1..100 % enum? title : string mapper : function (V, {K:V}) : string key : K enum alignments "left" "right" end align : alignments end type getrow = function ({ K : V }) : string table : function ({ tablerow }, width : integer) : getrow end return lib prosody-13.0.1/teal-src/prosody/util/human/PaxHeaders/units.d.tl0000644000000000000000000000011714773555365021623 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.783711299 prosody-13.0.1/teal-src/prosody/util/human/units.d.tl0000644000175000017500000000025614773555365024025 0ustar00prosodyprosody00000000000000local lib = record enum logbase "b" -- 1024 end adjust : function (number, string) : number, string format : function (number, string, logbase) : string end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/id.d.tl0000644000000000000000000000011714773555365017745 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.783711299 prosody-13.0.1/teal-src/prosody/util/id.d.tl0000644000175000017500000000026014773555365022142 0ustar00prosodyprosody00000000000000local record lib short : function () : string medium : function () : string long : function () : string custom : function (integer) : function () : string end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/interpolation.d.tl0000644000000000000000000000011714773555365022240 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.783711299 prosody-13.0.1/teal-src/prosody/util/interpolation.d.tl0000644000175000017500000000034014773555365024434 0ustar00prosodyprosody00000000000000local type renderer = function (string, { string : any }) : string local type filter = function (string, any) : string local record lib new : function (string, string, funcs : { string : filter }) : renderer end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/ip.d.tl0000644000000000000000000000011714773555365017761 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.783711299 prosody-13.0.1/teal-src/prosody/util/ip.d.tl0000644000175000017500000000066514773555365022167 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/jid.d.tl0000644000000000000000000000011714773555365020117 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.783711299 prosody-13.0.1/teal-src/prosody/util/jid.d.tl0000644000175000017500000000100414773555365022311 0ustar00prosodyprosody00000000000000local record lib split : function (string) : string, string, string bare : function (string) : string prepped_split : function (string, boolean) : string, string, string join : function (string, string, string) : string prep : function (string, boolean) : string compare : function (string, string) : boolean node : function (string) : string host : function (string) : string resource : function (string) : string escape : function (string) : string unescape : function (string) : string end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/json.d.tl0000644000000000000000000000011714773555365020322 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.783711299 prosody-13.0.1/teal-src/prosody/util/json.d.tl0000644000175000017500000000037614773555365022527 0ustar00prosodyprosody00000000000000local record lib encode : function (any) : string decode : function (string) : any, string enum json_type_name "null" "boolean" "object" "array" "number" "string" "integer" end type null_type = (nil) null : null_type end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/jsonpointer.tl0000644000000000000000000000011714773555365021501 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.783711299 prosody-13.0.1/teal-src/prosody/util/jsonpointer.tl0000644000175000017500000000167214773555365023706 0ustar00prosodyprosody00000000000000 local enum ptr_error "invalid-table" "invalid-path" end local function unescape_token(escaped_token : string) : string local unescaped = escaped_token:gsub("~1", "/"):gsub("~0", "~") return unescaped end local function resolve_json_pointer(ref : any, path : string) : any, ptr_error local ptr_len = #path+1 for part, pos in path:gmatch("/([^/]*)()") do local token = unescape_token(part) if not ref is table then return nil end local idx = next(ref) local new_ref : any if idx is string then new_ref = ref[token] elseif idx is integer then local i = tonumber(token) if token == "-" then i = #(ref as {any}) + 1 end new_ref = ref[i+1] else return nil, "invalid-table" end if pos as integer == ptr_len then return new_ref elseif new_ref is table then ref = new_ref elseif not ref is table then return nil, "invalid-path" end end return ref end return { resolve = resolve_json_pointer, } prosody-13.0.1/teal-src/prosody/util/PaxHeaders/jsonschema.tl0000644000000000000000000000011714773555365021261 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.787711316 prosody-13.0.1/teal-src/prosody/util/jsonschema.tl0000644000175000017500000003443714773555365023473 0ustar00prosodyprosody00000000000000-- 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; prosody-13.0.1/teal-src/prosody/util/PaxHeaders/jwt.d.tl0000644000000000000000000000011714773555365020155 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.787711316 prosody-13.0.1/teal-src/prosody/util/jwt.d.tl0000644000175000017500000000162214773555365022355 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/logger.d.tl0000644000000000000000000000011714773555365020630 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.787711316 prosody-13.0.1/teal-src/prosody/util/logger.d.tl0000644000175000017500000000076714773555365023041 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/mathcompat.tl0000644000000000000000000000011714773555365021264 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.787711316 prosody-13.0.1/teal-src/prosody/util/mathcompat.tl0000644000175000017500000000044214773555365023463 0ustar00prosodyprosody00000000000000if 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/net.d.tl0000644000000000000000000000011714773555365020137 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.787711316 prosody-13.0.1/teal-src/prosody/util/net.d.tl0000644000175000017500000000033114773555365022333 0ustar00prosodyprosody00000000000000 local enum type_strings "both" "ipv4" "ipv6" end local record lib local_addresses : function (type_strings, boolean) : { string } pton : function (string):string ntop : function (string):string end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/poll.d.tl0000644000000000000000000000011714773555365020317 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.787711316 prosody-13.0.1/teal-src/prosody/util/poll.d.tl0000644000175000017500000000147114773555365022521 0ustar00prosodyprosody00000000000000local record state enum waiterr "timeout" "signal" end add : function (state, integer, boolean, boolean) : boolean add : function (state, integer, boolean, boolean) : nil, string, integer set : function (state, integer, boolean, boolean) : boolean set : function (state, integer, boolean, boolean) : nil, string, integer del : function (state, integer) : boolean del : function (state, integer) : nil, string, integer wait : function (state, integer) : integer, boolean, boolean wait : function (state, integer) : nil, string, integer wait : function (state, integer) : nil, waiterr getfd : function (state) : integer end local record lib new : function () : state EEXIST : integer EMFILE : integer ENOENT : integer enum api_backend "epoll" "poll" "select" end api : api_backend end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/pposix.d.tl0000644000000000000000000000011714773555365020673 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.787711316 prosody-13.0.1/teal-src/prosody/util/pposix.d.tl0000644000175000017500000000427014773555365023075 0ustar00prosodyprosody00000000000000local record pposix enum syslog_facility "auth" "authpriv" "cron" "daemon" "ftp" "kern" "local0" "local1" "local2" "local3" "local4" "local5" "local6" "local7" "lpr" "mail" "syslog" "user" "uucp" end enum syslog_level "debug" "info" "notice" "warn" "error" end enum ulimit_resource "CORE" "CPU" "DATA" "FSIZE" "NOFILE" "STACK" "MEMLOCK" "NPROC" "RSS" "NICE" end enum ulimit_unlimited "unlimited" end type ulimit_limit = integer | ulimit_unlimited record utsname sysname : string nodename : string release : string version : string machine : string domainname : string end record memoryinfo allocated : integer allocated_mmap : integer used : integer unused : integer returnable : integer end abort : function () daemonize : function () : boolean, string syslog_open : function (ident : string, facility : syslog_facility) syslog_close : function () syslog_log : function (level : syslog_level, src : string, msg : string) syslog_setminlevel : function (level : syslog_level) getpid : function () : integer getuid : function () : integer getgid : function () : integer setuid : function (uid : integer | string) : boolean, string -- string|integer setgid : function (uid : integer | string) : boolean, string initgroups : function (user : string, gid : integer) : boolean, string umask : function (umask : string) : string mkdir : function (dir : string) : boolean, string setrlimit : function (resource : ulimit_resource, soft : ulimit_limit, hard : ulimit_limit) : boolean, string getrlimit : function (resource : ulimit_resource) : boolean, ulimit_limit, ulimit_limit getrlimit : function (resource : ulimit_resource) : boolean, string uname : function () : utsname setenv : function (key : string, value : string) : boolean meminfo : function () : memoryinfo atomic_append : function (f : FILE, s : string) : boolean, string, integer remove_blocks : function (f : FILE, integer, integer) isatty : function(FILE) : boolean ENOENT : integer _NAME : string _VESRION : string end return pposix prosody-13.0.1/teal-src/prosody/util/PaxHeaders/promise.d.tl0000644000000000000000000000011714773555365021027 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.791711332 prosody-13.0.1/teal-src/prosody/util/promise.d.tl0000644000175000017500000000113714773555365023230 0ustar00prosodyprosody00000000000000 local record lib type resolve_func = function (any) type promise_body = function (resolve_func, resolve_func) record Promise type on_resolved = function (A) : any type on_rejected = function (B) : any next : function (Promise, on_resolved, on_rejected) : Promise end new : function (promise_body) : Promise resolve : function (any) : Promise reject : function (any) : Promise all : function ({ Promise }) : Promise all_settled : function ({ Promise }) : Promise race : function ({ Promise }) : Promise try : function is_promise : function(any) : boolean end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/queue.d.tl0000644000000000000000000000011714773555365020475 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.791711332 prosody-13.0.1/teal-src/prosody/util/queue.d.tl0000644000175000017500000000113114773555365022670 0ustar00prosodyprosody00000000000000local record lib record queue size : integer count : function (queue) : integer enum push_errors "queue full" end push : function (queue, T) : boolean, push_errors pop : function (queue) : T peek : function (queue) : T replace : function (queue, T) : boolean, push_errors type iterator = function (T, integer) : integer, T items : function (queue) : iterator, T, integer type consume_iter = function (queue) : T consume : function (queue) : consume_iter end new : function (size:integer, allow_wrapping:boolean) : queue end return lib; prosody-13.0.1/teal-src/prosody/util/PaxHeaders/random.d.tl0000644000000000000000000000011714773555365020631 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.791711332 prosody-13.0.1/teal-src/prosody/util/random.d.tl0000644000175000017500000000010514773555365023024 0ustar00prosodyprosody00000000000000local record lib bytes : function (n:integer):string end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/ringbuffer.d.tl0000644000000000000000000000011714773555365021502 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.791711332 prosody-13.0.1/teal-src/prosody/util/ringbuffer.d.tl0000644000175000017500000000113614773555365023702 0ustar00prosodyprosody00000000000000local record lib record ringbuffer find : function (ringbuffer, string) : integer discard : function (ringbuffer, integer) : boolean read : function (ringbuffer, integer, boolean) : string readuntil : function (ringbuffer, string) : string write : function (ringbuffer, string) : integer size : function (ringbuffer) : integer length : function (ringbuffer) : integer sub : function (ringbuffer, integer, integer) : string byte : function (ringbuffer, integer, integer) : integer... free : function (ringbuffer) : integer end new : function (integer) : ringbuffer end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/roles.d.tl0000644000000000000000000000011714773555365020475 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.791711332 prosody-13.0.1/teal-src/prosody/util/roles.d.tl0000644000175000017500000000117214773555365022675 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/serialization.d.tl0000644000000000000000000000011714773555365022226 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.791711332 prosody-13.0.1/teal-src/prosody/util/serialization.d.tl0000644000175000017500000000126714773555365024433 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/set.d.tl0000644000000000000000000000011714773555365020144 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.791711332 prosody-13.0.1/teal-src/prosody/util/set.d.tl0000644000175000017500000000132314773555365022342 0ustar00prosodyprosody00000000000000local record lib record Set add : function (Set, T) contains : function (Set, T) : boolean contains_set : function (Set, Set) : boolean items : function (Set) : function (Set, T) : T remove : function (Set, T) add_list : function (Set, { T }) include : function (Set, Set) exclude : function (Set, Set) empty : function (Set) : boolean end new : function ({ T }) : Set is_set : function (any) : boolean union : function (Set, Set) : Set difference : function (Set, Set) : Set intersection : function (Set, Set) : Set xor : function (Set, Set) : Set end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/signal.d.tl0000644000000000000000000000011714773555365020626 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.791711332 prosody-13.0.1/teal-src/prosody/util/signal.d.tl0000644000175000017500000000112414773555365023023 0ustar00prosodyprosody00000000000000local record lib enum Signal "SIGABRT" "SIGALRM" "SIGBUS" "SIGCHLD" "SIGCLD" "SIGCONT" "SIGFPE" "SIGHUP" "SIGILL" "SIGINT" "SIGIO" "SIGIOT" "SIGKILL" "SIGPIPE" "SIGPOLL" "SIGPROF" "SIGQUIT" "SIGSEGV" "SIGSTKFLT" "SIGSTOP" "SIGSYS" "SIGTERM" "SIGTRAP" "SIGTTIN" "SIGTTOU" "SIGURG" "SIGUSR1" "SIGUSR2" "SIGVTALRM" "SIGWINCH" "SIGXCPU" "SIGXFSZ" end signal : function (integer | Signal, function, boolean) : boolean raise : function (integer | Signal) kill : function (integer, integer | Signal) -- enum : integer end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/smqueue.tl0000644000000000000000000000011714773555365020613 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.795711348 prosody-13.0.1/teal-src/prosody/util/smqueue.tl0000644000175000017500000000426014773555365023014 0ustar00prosodyprosody00000000000000local queue = require "prosody.util.queue"; local record lib -- T would typically be util.stanza record smqueue _queue : queue.queue _head : integer _tail : integer enum ack_errors "tail" "head" "pop" end push : function (smqueue, T) ack : function (smqueue, integer) : { T }, ack_errors resumable : function (smqueue) : boolean resume : function (smqueue) : queue.queue.iterator, any, integer type consume_iter = function (smqueue) : T consume : function (smqueue) : consume_iter table : function (smqueue) : { T } end new : function (integer) : smqueue end local type smqueue = lib.smqueue; function smqueue:push(v) self._head = self._head + 1; -- Wraps instead of errors assert(self._queue:push(v)); end function smqueue:ack(h : integer) : { any }, smqueue.ack_errors if h < self._tail then return nil, "tail"; elseif h > self._head then return nil, "head"; end -- TODO optimize? cache table fields local acked = {}; self._tail = h; local expect = self._head - self._tail; while expect < self._queue:count() do local v = self._queue:pop(); if not v then return nil, "pop"; end table.insert(acked, v); end return acked; end function smqueue:count_unacked() : integer return self._head - self._tail; end function smqueue:count_acked() : integer return self._tail; end function smqueue:resumable() : boolean return self._queue:count() >= (self._head - self._tail); end function smqueue:resume() : queue.queue.iterator, any, integer return self._queue:items(); end function smqueue:consume() : queue.queue.consume_iter return self._queue:consume() end -- Compatibility layer, plain ol' table function smqueue:table() : { any } local t : { any } = {}; for i, v in self:resume() do t[i] = v; end return t; end local function freeze(q : smqueue) : { string:integer } return { head = q._head, tail = q._tail } end local queue_mt = { -- __name = "smqueue"; __index = smqueue; __len = smqueue.count_unacked; __freeze = freeze; } function lib.new(size : integer) : queue.queue assert(size>0); return setmetatable({ _head = 0; _tail = 0; _queue = queue.new(size, true) }, queue_mt); end return lib; prosody-13.0.1/teal-src/prosody/util/PaxHeaders/stanza.d.tl0000644000000000000000000000011714773555365020651 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.795711348 prosody-13.0.1/teal-src/prosody/util/stanza.d.tl0000644000175000017500000000772114773555365023057 0ustar00prosodyprosody00000000000000local record lib type children_iter = function ( stanza_t ) : stanza_t type childtags_iter = function () : stanza_t type maptags_cb = function ( stanza_t ) : stanza_t enum stanza_error_type "auth" "cancel" "continue" "modify" "wait" end enum stanza_error_condition "bad-request" "conflict" "feature-not-implemented" "forbidden" "gone" "internal-server-error" "item-not-found" "jid-malformed" "not-acceptable" "not-allowed" "not-authorized" "policy-violation" "recipient-unavailable" "redirect" "registration-required" "remote-server-not-found" "remote-server-timeout" "resource-constraint" "service-unavailable" "subscription-required" "undefined-condition" "unexpected-request" end record stanza_t name : string attr : { string : string } { stanza_t | string } tags : { stanza_t } query : function ( stanza_t, string ) : stanza_t body : function ( stanza_t, string, { string : string } ) : stanza_t text_tag : function ( stanza_t, string, string, { string : string } ) : stanza_t tag : function ( stanza_t, string, { string : string } ) : stanza_t text : function ( stanza_t, string ) : stanza_t up : function ( stanza_t ) : stanza_t at_top : function ( stanza_t ) : boolean reset : function ( stanza_t ) : stanza_t add_direct_child : function ( stanza_t, stanza_t ) add_child : function ( stanza_t, stanza_t ) remove_children : function ( stanza_t, string, string ) : stanza_t get_child : function ( stanza_t, string, string ) : stanza_t get_text : function ( stanza_t ) : string get_child_text : function ( stanza_t, string, string ) : string get_child_attr : function ( stanza_t, string, string ) : string get_child_with_attr : function ( stanza_t, string, string, string, function (string) : boolean ) : string child_with_name : function ( stanza_t, string, string ) : stanza_t child_with_ns : function ( stanza_t, string, string ) : stanza_t children : function ( stanza_t ) : children_iter, stanza_t, integer childtags : function ( stanza_t, string, string ) : childtags_iter maptags : function ( stanza_t, maptags_cb ) : stanza_t find : function ( stanza_t, string ) : stanza_t | string top_tag : function ( stanza_t ) : string pretty_print : function ( stanza_t ) : string pretty_top_tag : function ( stanza_t ) : string -- FIXME Represent util.error support get_error : function ( stanza_t ) : stanza_error_type, stanza_error_condition, string, stanza_t add_error : function ( stanza_t, stanza_error_type, stanza_error_condition, string, string ) indent : function ( stanza_t, integer, string ) : stanza_t end record serialized_stanza_t name : string attr : { string : string } { serialized_stanza_t | string } end record message_attr ["xml:lang"] : string from : string id : string to : string type : message_type enum message_type "chat" "error" "groupchat" "headline" "normal" end end record presence_attr ["xml:lang"] : string from : string id : string to : string type : presence_type enum presence_type "error" "probe" "subscribe" "subscribed" "unsubscribe" "unsubscribed" end end record iq_attr ["xml:lang"] : string from : string id : string to : string type : iq_type enum iq_type "error" "get" "result" "set" end end stanza : function ( string, { string : string } ) : stanza_t is_stanza : function ( any ) : boolean preserialize : function ( stanza_t ) : serialized_stanza_t deserialize : function ( serialized_stanza_t ) : stanza_t clone : function ( stanza_t, boolean ) : stanza_t message : function ( message_attr, string ) : stanza_t iq : function ( iq_attr ) : stanza_t reply : function ( stanza_t ) : stanza_t error_reply : function ( stanza_t, stanza_error_type, stanza_error_condition, string, string ) : stanza_t presence : function ( presence_attr ) : stanza_t xml_escape : function ( string ) : string pretty_print : function ( string ) : string end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/strbitop.d.tl0000644000000000000000000000011714773555365021217 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.795711348 prosody-13.0.1/teal-src/prosody/util/strbitop.d.tl0000644000175000017500000000033214773555365023414 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/struct.d.tl0000644000000000000000000000011714773555365020675 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.795711348 prosody-13.0.1/teal-src/prosody/util/struct.d.tl0000644000175000017500000000024414773555365023074 0ustar00prosodyprosody00000000000000local record lib pack : function (string, ...:any) : string unpack : function(string, string, integer) : any... size : function(string) : integer end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/table.d.tl0000644000000000000000000000011714773555365020440 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.795711348 prosody-13.0.1/teal-src/prosody/util/table.d.tl0000644000175000017500000000027314773555365022641 0ustar00prosodyprosody00000000000000local record lib create : function (narr:integer, nrec:integer):table pack : function (...:any):{any} move : function (table, integer, integer, integer, table) : table end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/termcolours.d.tl0000644000000000000000000000011714773555365021727 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.795711348 prosody-13.0.1/teal-src/prosody/util/termcolours.d.tl0000644000175000017500000000031014773555365024120 0ustar00prosodyprosody00000000000000local record lib getstring : function (string, string) : string getstyle : function (...:string) : string setstyle : function (string) : string tohtml : function (string) : string end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/time.d.tl0000644000000000000000000000011714773555365020307 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.795711348 prosody-13.0.1/teal-src/prosody/util/time.d.tl0000644000175000017500000000013714773555365022507 0ustar00prosodyprosody00000000000000 local record lib now : function () : number monotonic : function () : number end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/timer.d.tl0000644000000000000000000000011714773555365020471 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.795711348 prosody-13.0.1/teal-src/prosody/util/timer.d.tl0000644000175000017500000000036614773555365022675 0ustar00prosodyprosody00000000000000local 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 prosody-13.0.1/teal-src/prosody/util/PaxHeaders/uuid.d.tl0000644000000000000000000000011714773555365020317 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.795711348 prosody-13.0.1/teal-src/prosody/util/uuid.d.tl0000644000175000017500000000020714773555365022515 0ustar00prosodyprosody00000000000000local record lib get_nibbles : function (number) : string generate : function () : string seed : function (string) end return lib prosody-13.0.1/teal-src/prosody/util/PaxHeaders/xtemplate.tl0000644000000000000000000000011714773555365021132 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.795711348 prosody-13.0.1/teal-src/prosody/util/xtemplate.tl0000644000175000017500000000670014773555365023334 0ustar00prosodyprosody00000000000000-- render(template, stanza) --> string -- {path} --> stanza:find(path) -- {{ns}name/child|each({ns}name){sub-template}} --[[ template ::= "{" path ("|" name ("(" args ")")? (template)? )* "}" path ::= defined by util.stanza name ::= %w+ args ::= anything with balanced ( ) pairs ]] local s_gsub = string.gsub; local s_match = string.match; local s_sub = string.sub; local t_concat = table.concat; local st = require "prosody.util.stanza"; local type escape_t = function (string) : string local type filter_t = function (string | 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, "(%s*)(%b{})(%s*)", function(pre_blank : string, block : string, post_blank : string) : string local inner = s_sub(block, 2, -2); if inner:sub(1, 1) == "-" then pre_blank = ""; inner = inner:sub(2); end if inner:sub(-1, -1) == "-" then post_blank = ""; inner = inner:sub(1, -2); end local path, pipe, pos = s_match(inner, "^([^|]+)(|?)()"); if not path is string then return end local value : string | st.stanza_t if path == "." then value = root; elseif path == "#" then value = root:get_text(); else value = root:find(path); end local is_escaped = false; while pipe == "|" do local func, args, tmpl, p = s_match(inner, "^(%w+)(%b())(%b{})()", pos as integer); if not func then func, args, p = s_match(inner, "^(%w+)(%b())()", pos as integer); end if not func then func, tmpl, p = s_match(inner, "^(%w+)(%b{})()", pos as integer); end if not func then func, p = s_match(inner, "^(%w+)()", pos as integer); end if not func then break end if tmpl then tmpl = s_sub(tmpl, 2, -2); end if args then args = s_sub(args, 2, -2); end if func == "each" and tmpl then 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 ns = s_sub(ns, 2, -2); else name, ns = args, nil; end if ns == "" then ns = nil; end if name == "" then name = nil; end local out, i = {}, 1; for c in (value as st.stanza_t):childtags(name, ns) do out[i], i = render(tmpl, c, escape, filters), i + 1; end value = t_concat(out); is_escaped = true; elseif func == "and" and tmpl then local condition = value; if args then condition = root:find(args); end if condition then value = render(tmpl, root, escape, filters); is_escaped = true; end elseif func == "or" and tmpl then local condition = value; if args then condition = root:find(args); end if not condition then value = render(tmpl, root, escape, filters); is_escaped = true; end elseif filters and filters[func] then local f = filters[func]; value, is_escaped = f(value, args, tmpl); else error("No such filter function: " .. func); end pipe, pos = s_match(inner, "^(|?)()", p as integer); end if value is string then if not is_escaped then value = escape(value); end return pre_blank .. value .. post_blank; elseif st.is_stanza(value) then value = value:get_text(); if value then return pre_blank .. escape(value) .. post_blank; end end return pre_blank .. post_blank; end)); end return { render = render }; prosody-13.0.1/PaxHeaders/tools0000644000000000000000000000013114773555365013457 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.799711364 30 ctime=1743706869.815711427 prosody-13.0.1/tools/0000755000175000017500000000000014773555365015737 5ustar00prosodyprosody00000000000000prosody-13.0.1/tools/PaxHeaders/build-env0000644000000000000000000000013114773555365015344 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.799711364 30 ctime=1743706869.799711364 prosody-13.0.1/tools/build-env/0000755000175000017500000000000014773555365017624 5ustar00prosodyprosody00000000000000prosody-13.0.1/tools/build-env/PaxHeaders/Containerfile0000644000000000000000000000011714773555365020132 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.799711364 prosody-13.0.1/tools/build-env/Containerfile0000644000175000017500000000233014773555365022327 0ustar00prosodyprosody00000000000000ARG 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! prosody-13.0.1/tools/build-env/PaxHeaders/build.sh0000644000000000000000000000011714773555365017060 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.799711364 prosody-13.0.1/tools/build-env/build.sh0000755000175000017500000000052314773555365021262 0ustar00prosodyprosody00000000000000#!/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}" prosody-13.0.1/tools/build-env/PaxHeaders/here.sh0000644000000000000000000000011714773555365016704 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.799711364 prosody-13.0.1/tools/build-env/here.sh0000755000175000017500000000046714773555365021115 0ustar00prosodyprosody00000000000000#!/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" "$@" prosody-13.0.1/tools/PaxHeaders/cfgdump.lua0000644000000000000000000000011714773555365015670 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.799711364 prosody-13.0.1/tools/cfgdump.lua0000755000175000017500000000675414773555365020106 0ustar00prosodyprosody00000000000000#!/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 "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 "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"prosody.core.configmanager"; local startup = require "prosody.util.startup"; startup.set_function_metatable(); local config_filename, onlyhost, onlyoption = ...; local ok, _, err = configmanager.load(config_filename or "./prosody.cfg.lua", "lua"); assert(ok, err); if onlyhost then if not onlyoption then onlyhost, onlyoption = "*", onlyhost; end if onlyhost ~= "*" then local component_module = configmanager.get(onlyhost, "component_module"); if component_module == "component" then printf("Component %q", onlyhost); elseif component_module then printf("Component %q %q", onlyhost, component_module); else printf("VirtualHost %q", onlyhost); end end printf("%s = %s", onlyoption or "?", serialize(configmanager.get(onlyhost, onlyoption))); return; end local config = configmanager.getconfig(); for host, hostcfg in it.sorted_pairs(config) do local fixed = {}; for option, value in it.sorted_pairs(hostcfg) do fixed[option] = value; if option:match("ports?$") or option:match("interfaces?$") then if option:match("s$") then if type(value) ~= "table" then fixed[option] = { value }; end else if type(value) == "table" and #value > 1 then fixed[option] = nil; fixed[option.."s"] = value; end end end end config[host] = fixed; end local globals = config["*"]; config["*"] = nil; local function printsection(section) local out, n = {}, 1; for k,v in it.sorted_pairs(section) do out[n], n = s_format("%s = %s", k, serialize(v)), n + 1; end table.sort(out); print(table.concat(out, "\n")); end print("-------------- Prosody Exported Configuration File -------------"); print(); print("------------------------ Global section ------------------------"); print(); printsection(globals); print(); local has_components = nil; print("------------------------ Virtual hosts -------------------------"); for host, hostcfg in it.sorted_pairs(config) do setmetatable(hostcfg, nil); hostcfg.defined = nil; if hostcfg.component_module == nil then print(); printf("VirtualHost %q", host); printsection(hostcfg); else has_components = true end end print(); if has_components then print("------------------------- Components ---------------------------"); for host, hostcfg in it.sorted_pairs(config) do local component_module = hostcfg.component_module; hostcfg.component_module = nil; if component_module then print(); if component_module == "component" then printf("Component %q", host); else printf("Component %q %q", host, component_module); hostcfg.component_module = nil; hostcfg.load_global_modules = nil; end printsection(hostcfg); end end end print() print("------------------------- End of File --------------------------"); prosody-13.0.1/tools/PaxHeaders/dnsregistry.lua0000644000000000000000000000011714773555365016620 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.799711364 prosody-13.0.1/tools/dnsregistry.lua0000644000175000017500000000362414773555365021024 0ustar00prosodyprosody00000000000000-- Generate util/dnsregistry.lua from IANA HTTP status code registry 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"); print(os.date("-- Generated on %Y-%m-%d")) local registry_mapping = { ["dns-parameters-2"] = "classes"; ["dns-parameters-4"] = "types"; ["dns-parameters-6"] = "errors"; }; print("return {"); for registry in registries:childtags("registry") do local registry_name = registry_mapping[registry.attr.id]; if registry_name then print("\t" .. registry_name .. " = {"); local duplicates = {}; for record in registry:childtags("record") do local record_name = record:get_child_text("name"); local record_type = record:get_child_text("type"); 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("unassigned") then record_code = nil; end if registry_name == "classes" and record_code then record_type = record_desc and record_desc:match("%((%w+)%)$") if record_type then print(("\t\t[%q] = %d; [%d] = %q;"):format(record_type, record_code, record_code, record_type)) end elseif registry_name == "types" and record_type and record_code then print(("\t\t[%q] = %d; [%d] = %q;"):format(record_type, record_code, record_code, record_type)) elseif registry_name == "errors" and record_code and record_name then local dup = duplicates[record_code] and "-- " or ""; print(("\t\t%s[%d] = %q; [%q] = %q;"):format(dup, record_code, record_name, record_name, record_desc or record_name)); duplicates[record_code] = true; end end print("\t};"); end end print("};"); prosody-13.0.1/tools/PaxHeaders/ejabberd2prosody.lua0000644000000000000000000000011714773555365017503 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.799711364 prosody-13.0.1/tools/ejabberd2prosody.lua0000755000175000017500000003031614773555365021710 0ustar00prosodyprosody00000000000000#!/usr/bin/env lua -- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- package.path = package.path ..";../?.lua"; local my_name = arg[0]; if my_name:match("[/\\]") then package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua"); package.path = package.path..";"..my_name:gsub("[^/\\]+$", "?.lua"); package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so"); end local erlparse = require "erlparse"; prosody = {}; package.loaded["util.logger"] = {init = function() return function() end; end} local serialize = require "util.serialization".serialize; local st = require "util.stanza"; local dm = require "util.datamanager" dm.set_data_path("data"); function build_stanza(tuple, stanza) assert(type(tuple) == "table", "XML node is of unexpected type: "..type(tuple)); if tuple[1] == "xmlelement" or tuple[1] == "xmlel" then assert(type(tuple[2]) == "string", "element name has type: "..type(tuple[2])); assert(type(tuple[3]) == "table", "element attribute array has type: "..type(tuple[3])); assert(type(tuple[4]) == "table", "element children array has type: "..type(tuple[4])); local name = tuple[2]; local attr = {}; for _, a in ipairs(tuple[3]) do if type(a[1]) == "string" and type(a[2]) == "string" then attr[a[1]] = a[2]; end end local up; if stanza then stanza:tag(name, attr); up = true; else stanza = st.stanza(name, attr); end for _, a in ipairs(tuple[4]) do build_stanza(a, stanza); end if up then stanza:up(); else return stanza end elseif tuple[1] == "xmlcdata" then if type(tuple[2]) ~= "table" then assert(type(tuple[2]) == "string", "XML CDATA has unexpected type: "..type(tuple[2])); stanza:text(tuple[2]); end -- else it's [], i.e., the null value, used for the empty string else error("unknown element type: "..serialize(tuple)); end end function build_time(tuple) local Megaseconds,Seconds,Microseconds = unpack(tuple); return Megaseconds * 1000000 + Seconds; end function build_jid(tuple, full) local node, jid, resource = tuple[1], tuple[2], tuple[3] if type(node) == "string" and node ~= "" then jid = tuple[1] .. "@" .. jid; end if full and type(resource) == "string" and resource ~= "" then jid = jid .. "/" .. resource; end return jid; end function vcard(node, host, stanza) local ret, err = dm.store(node, host, "vcard", st.preserialize(stanza)); print("["..(err or "success").."] vCard: "..node.."@"..host); end function password(node, host, password) local data = {}; if type(password) == "string" then data.password = password; elseif type(password) == "table" and password[1] == "scram" then local unb64 = require"mime".unb64; local function hex(s) return s:gsub(".", function (c) return ("%02x"):format(c:byte()); end); end data.stored_key = hex(unb64(password[2])); data.server_key = hex(unb64(password[3])); data.salt = unb64(password[4]); if type(password[6]) == "number" then assert(password[5] == "sha", "unexpected passwd entry hash: "..tostring(password[5])); data.iteration_count = password[6]; else assert(type(password[5]) == "number", "unexpected passwd entry in source data"); data.iteration_count = password[5]; end end local ret, err = dm.store(node, host, "accounts", data); print("["..(err or "success").."] accounts: "..node.."@"..host); end function roster(node, host, jid, item) local roster = dm.load(node, host, "roster") or {}; roster[jid] = item; local ret, err = dm.store(node, host, "roster", roster); print("["..(err or "success").."] roster: " ..node.."@"..host.." - "..jid); end function roster_pending(node, host, jid) local roster = dm.load(node, host, "roster") or {}; roster.pending = roster.pending or {}; roster.pending[jid] = true; local ret, err = dm.store(node, host, "roster", roster); print("["..(err or "success").."] roster: " ..node.."@"..host.." - "..jid); end function private_storage(node, host, xmlns, stanza) local private = dm.load(node, host, "private") or {}; private[stanza.name..":"..xmlns] = st.preserialize(stanza); local ret, err = dm.store(node, host, "private", private); print("["..(err or "success").."] private: " ..node.."@"..host.." - "..xmlns); end function offline_msg(node, host, t, stanza) stanza.attr.stamp = os.date("!%Y-%m-%dT%H:%M:%SZ", t); stanza.attr.stamp_legacy = os.date("!%Y%m%dT%H:%M:%S", t); local ret, err = dm.list_append(node, host, "offline", st.preserialize(stanza)); print("["..(err or "success").."] offline: " ..node.."@"..host.." - "..os.date("!%Y-%m-%dT%H:%M:%SZ", t)); end function privacy(node, host, default, lists) local privacy = { lists = {} }; local count = 0; if default then privacy.default = default; end for _, inlist in ipairs(lists) do local name, items = inlist[1], inlist[2]; local list = { name = name; items = {}; }; local orders = {}; for _, item in pairs(items) do repeat if item[1] ~= "listitem" then print("[error] privacy: unhandled item: "..tostring(item[1])); break; end local _type, value = item[2], item[3]; if _type == "jid" then if type(value) ~= "table" then print("[error] privacy: jid value is not valid: "..tostring(value)); break; end local _node, _host, _resource = value[1], value[2], value[3]; value = build_jid(value, true) elseif _type == "none" then _type = nil; value = nil; elseif _type == "group" then if type(value) ~= "string" then print("[error] privacy: group value is not string: "..tostring(value)); break; end elseif _type == "subscription" then if value~="both" and value~="from" and value~="to" and value~="none" then print("[error] privacy: subscription value is invalid: "..tostring(value)); break; end else print("[error] privacy: invalid item type: "..tostring(_type)); break; end local action = item[4]; if action ~= "allow" and action ~= "deny" then print("[error] privacy: unhandled action: "..tostring(action)); break; end local order = item[5]; if type(order) ~= "number" or order<0 then print("[error] privacy: order is not numeric: "..tostring(order)); break; end if orders[order] then print("[error] privacy: duplicate order value: "..tostring(order)); break; end orders[order] = true; local match_all = item[6]; local match_iq = item[7]; local match_message = item[8]; local match_presence_in = item[9]; local match_presence_out = item[10]; list.items[#list.items+1] = { type = _type; value = value; action = action; order = order; message = match_message == "true"; iq = match_iq == "true"; ["presence-in"] = match_presence_in == "true"; ["presence-out"] = match_presence_out == "true"; }; until true; end table.sort(list.items, function(a, b) return a.order < b.order; end); if privacy.lists[list.name] then print("[warn] duplicate privacy list: "..tostring(list.name)); end privacy.lists[list.name] = list; count = count + 1; end if default and not privacy.lists[default] then if default == "none" then privacy.default = nil; else print("[warn] default privacy list doesn't exist: "..tostring(default)); end end local ret, err = dm.store(node, host, "privacy", privacy); print("["..(err or "success").."] privacy: " ..node.."@"..host.." - "..count.." list(s)"); end function muc_room(node, host, properties) local store = { jid = node.."@"..host, _data = {}, _affiliations = {} }; for _,aff in ipairs(properties.affiliations) do store._affiliations[build_jid(aff[1])] = aff[2][1] or aff[2]; end -- destructure ejabberd's subject datum (e.g. [{text,<<>>,<<"my room subject">>}] ) store._data.subject = properties.subject[1][3]; if properties.subject_author then store._data.subject_from = store.jid .. "/" .. properties.subject_author; end store._data.name = properties.title; store._data.description = properties.description; if properties.password_protected ~= false and properties.password ~= "" then store._data.password = properties.password; end store._data.moderated = (properties.moderated == "true") or nil; store._data.members_only = (properties.members_only == "true") or nil; store._data.persistent = (properties.persistent == "true") or nil; store._data.changesubject = (properties.allow_change_subj == "true") or nil; store._data.whois = properties.anonymous == "true" and "moderators" or "anyone"; store._data.hidden = (properties.public_list == "false") or nil; if not store._data.persistent then return print("[error] muc_room: skipping non-persistent room: "..node.."@"..host); end local ret, err = dm.store(node, host, "config", store); if ret then ret, err = dm.load(nil, host, "persistent"); if ret or not err then ret = ret or {}; ret[store.jid] = true; ret, err = dm.store(nil, host, "persistent", ret); end end print("["..(err or "success").."] muc_room: " ..node.."@"..host); end local filters = { passwd = function(tuple) password(tuple[2][1], tuple[2][2], tuple[3]); end; vcard = function(tuple) vcard(tuple[2][1], tuple[2][2], build_stanza(tuple[3])); end; roster = function(tuple) local node = tuple[3][1]; local host = tuple[3][2]; local contact = build_jid(tuple[4]); local name = tuple[5]; local subscription = tuple[6]; local ask = tuple[7]; local groups = tuple[8]; if type(name) ~= type("") then name = nil; end if ask == "none" then ask = nil; elseif ask == "out" then ask = "subscribe" elseif ask == "in" then roster_pending(node, host, contact); ask = nil; elseif ask == "both" then roster_pending(node, host, contact); ask = "subscribe"; else error("Unknown ask type: "..ask); end if subscription ~= "both" and subscription ~= "from" and subscription ~= "to" and subscription ~= "none" then error(subscription) end local item = {name = name, ask = ask, subscription = subscription, groups = {}}; for _, g in ipairs(groups) do if type(g) == "string" then item.groups[g] = true; end end roster(node, host, contact, item); end; private_storage = function(tuple) private_storage(tuple[2][1], tuple[2][2], tuple[2][3], build_stanza(tuple[3])); end; offline_msg = function(tuple) offline_msg(tuple[2][1], tuple[2][2], build_time(tuple[3]), build_stanza(tuple[7])); end; privacy = function(tuple) privacy(tuple[2][1], tuple[2][2], tuple[3], tuple[4]); end; muc_room = function(tuple) local properties = {}; for _,pair in ipairs(tuple[3]) do if not(type(pair[2]) == "table" and #pair[2] == 0) then -- skip nil values properties[pair[1]] = pair[2]; end end muc_room(tuple[2][1], tuple[2][2], properties); end; --[=[config = function(tuple) if tuple[2] == "hosts" then local output = io.output(); io.output("prosody.cfg.lua"); io.write("-- Configuration imported from ejabberd --\n"); io.write([[Host "*" modules_enabled = { "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in. "legacyauth"; -- Legacy authentication. Only used by some old clients and bots. "roster"; -- Allow users to have a roster. Recommended ;) "register"; -- Allow users to register on this server using a client "tls"; -- Add support for secure TLS on c2s/s2s connections "vcard"; -- Allow users to set vCards "private"; -- Private XML storage (for room bookmarks, etc.) "version"; -- Replies to server version requests "dialback"; -- s2s dialback support "uptime"; "disco"; "time"; "ping"; --"selftests"; }; ]]); for _, h in ipairs(tuple[3]) do io.write("Host \"" .. h .. "\"\n"); end io.output(output); print("prosody.cfg.lua created"); end end;]=] }; local arg = ...; local help = "/? -? ? /h -h /help -help --help"; if not arg or help:find(arg, 1, true) then print([[ejabberd db dump importer for Prosody Usage: ]]..my_name..[[ filename.txt The file can be generated from ejabberd using: sudo ejabberdctl dump filename.txt Note: The path of ejabberdctl depends on your ejabberd installation, and ejabberd needs to be running for ejabberdctl to work.]]); os.exit(1); end local count = 0; local t = {}; for item in erlparse.parseFile(arg) do count = count + 1; local name = item[1]; t[name] = (t[name] or 0) + 1; --print(count, serialize(item)); if filters[name] then filters[name](item); end end --print(serialize(t)); prosody-13.0.1/tools/PaxHeaders/ejabberdsql2prosody.lua0000644000000000000000000000011714773555365020223 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.803711379 prosody-13.0.1/tools/ejabberdsql2prosody.lua0000644000175000017500000002327714773555365022435 0ustar00prosodyprosody00000000000000#!/usr/bin/env lua -- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- prosody = {}; package.path = package.path ..";../?.lua"; local my_name = arg[0]; 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 "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) ------ local file = nil; local last = nil; local line = 1; local function read(expected) local ch; if last then ch = last; last = nil; else ch = file:read(1); if ch == "\n" then line = line + 1; end end if expected and ch ~= expected then error("expected: "..expected.."; got: "..(ch or "nil").." on line "..line); end return ch; end local function peek() if not last then last = read(); end return last; end local escapes = { ["\\0"] = "\0"; ["\\'"] = "'"; ["\\\""] = "\""; ["\\b"] = "\b"; ["\\n"] = "\n"; ["\\r"] = "\r"; ["\\t"] = "\t"; ["\\Z"] = "\26"; ["\\\\"] = "\\"; ["\\%"] = "%"; ["\\_"] = "_"; } local function unescape(s) return escapes[s] or error("Unknown escape sequence: "..s); end local function readString() read("'"); local s = ""; while true do local ch = peek(); if ch == "\\" then s = s..unescape(read()..read()); elseif ch == "'" then break; else s = s..read(); end end read("'"); return s; end local function readNonString() local s = ""; while true do if peek() == "," or peek() == ")" then break; else s = s..read(); end end return tonumber(s); end local function readItem() if peek() == "'" then return readString(); else return readNonString(); end end local function readTuple() local items = {} read("("); while peek() ~= ")" do table.insert(items, readItem()); if peek() == ")" then break; end read(","); end read(")"); return items; end local function readTuples() if peek() ~= "(" then read("("); end local tuples = {}; while true do table.insert(tuples, readTuple()); if peek() == "," then read() end if peek() == ";" then break; end end return tuples; end local function readTableName() local tname = ""; while peek() ~= "`" do tname = tname..read(); end return tname; end local function readInsert() if peek() == nil then return nil; end for ch in ("INSERT INTO `"):gmatch(".") do -- find line starting with this if peek() == ch then read(); -- found else -- match failed, skip line while peek() and read() ~= "\n" do end return nil; end end local tname = readTableName(); read("`"); read(" ") -- expect this if peek() == "(" then -- skip column list repeat until read() == ")"; read(" "); end for ch in ("VALUES "):gmatch(".") do read(ch); end -- expect this local tuples = readTuples(); read(";"); read("\n"); return tname, tuples; end local function readFile(filename) file = io.open(filename); if not file then error("File not found: "..filename); os.exit(0); end local t = {}; while true do local tname, tuples = readInsert(); if tname then if t[tname] then local t_name = t[tname]; for i=1,#tuples do table.insert(t_name, tuples[i]); end else t[tname] = tuples; end elseif peek() == nil then break; end end return t; end return readFile(filename); ------ end local arg, hostname = ...; local help = "/? -? ? /h -h /help -help --help"; if not(arg and hostname) or help:find(arg, 1, true) then print([[ejabberd SQL DB dump importer for Prosody Usage: ejabberdsql2prosody.lua filename.txt hostname The file can be generated using mysqldump: mysqldump db_name > filename.txt]]); os.exit(1); end local map = { ["last"] = {"username", "seconds", "state"}; ["privacy_default_list"] = {"username", "name"}; ["privacy_list"] = {"username", "name", "id"}; ["privacy_list_data"] = {"id", "t", "value", "action", "ord", "match_all", "match_iq", "match_message", "match_presence_in", "match_presence_out"}; ["private_storage"] = {"username", "namespace", "data"}; ["rostergroups"] = {"username", "jid", "grp"}; ["rosterusers"] = {"username", "jid", "nick", "subscription", "ask", "askmessage", "server", "subscribe", "type"}; ["spool"] = {"username", "xml", "seq"}; ["users"] = {"username", "password"}; ["vcard"] = {"username", "vcard"}; --["vcard_search"] = {}; } local NULL = {}; local parsed = parseFile(arg); for name, data in pairs(parsed) do local m = map[name]; if m then if #data > 0 and #data[1] ~= #m then print("[warning] expected "..#m.." columns for table `"..name.."`, found "..#data[1]); end for i=1,#data do local row = data[i]; for j=1,#m do row[m[j]] = row[j]; row[j] = nil; end end end end --print(serialize(t)); for _, row in ipairs(parsed["users"] or NULL) do local node, password = row.username, row.password; local ret, err = dm.store(node, hostname, "accounts", {password = password}); print("["..(err or "success").."] accounts: "..node.."@"..hostname); end function roster(node, host, jid, item) local roster = dm.load(node, host, "roster") or {}; roster[jid] = item; local ret, err = dm.store(node, host, "roster", roster); print("["..(err or "success").."] roster: " ..node.."@"..host.." - "..jid); end function roster_pending(node, host, jid) local roster = dm.load(node, host, "roster") or {}; roster.pending = roster.pending or {}; roster.pending[jid] = true; local ret, err = dm.store(node, host, "roster", roster); print("["..(err or "success").."] roster-pending: " ..node.."@"..host.." - "..jid); end function roster_group(node, host, jid, group) local roster = dm.load(node, host, "roster") or {}; local item = roster[jid]; if not item then print("Warning: No roster item "..jid.." for user "..node..", can't put in group "..group); return; end item.groups[group] = true; local ret, err = dm.store(node, host, "roster", roster); print("["..(err or "success").."] roster-group: " ..node.."@"..host.." - "..jid.." - "..group); end function private_storage(node, host, xmlns, stanza) local private = dm.load(node, host, "private") or {}; private[stanza.name..":"..xmlns] = st.preserialize(stanza); local ret, err = dm.store(node, host, "private", private); print("["..(err or "success").."] private: " ..node.."@"..host.." - "..xmlns); end function offline_msg(node, host, t, stanza) stanza.attr.stamp = os.date("!%Y-%m-%dT%H:%M:%SZ", t); stanza.attr.stamp_legacy = os.date("!%Y%m%dT%H:%M:%S", t); local ret, err = dm.list_append(node, host, "offline", st.preserialize(stanza)); print("["..(err or "success").."] offline: " ..node.."@"..host.." - "..os.date("!%Y-%m-%dT%H:%M:%SZ", t)); end for _, row in ipairs(parsed["rosterusers"] or NULL) do local node, contact = row.username, row.jid; local name = row.nick; if name == "" then name = nil; end local subscription = row.subscription; if subscription == "N" then subscription = "none" elseif subscription == "B" then subscription = "both" elseif subscription == "F" then subscription = "from" elseif subscription == "T" then subscription = "to" else error("Unknown subscription type: "..subscription) end; local ask = row.ask; if ask == "N" then ask = nil; elseif ask == "O" then ask = "subscribe"; elseif ask == "I" then roster_pending(node, hostname, contact); ask = nil; elseif ask == "B" then roster_pending(node, hostname, contact); ask = "subscribe"; else error("Unknown ask type: "..ask); end local item = {name = name, ask = ask, subscription = subscription, groups = {}}; roster(node, hostname, contact, item); end for _, row in ipairs(parsed["rostergroups"] or NULL) do roster_group(row.username, hostname, row.jid, row.grp); end for _, row in ipairs(parsed["vcard"] or NULL) do local stanza, err = parse_xml(row.vcard); if stanza then local ret, err = dm.store(row.username, hostname, "vcard", st.preserialize(stanza)); print("["..(err or "success").."] vCard: "..row.username.."@"..hostname); else print("[error] vCard XML parse failed: "..row.username.."@"..hostname); end end for _, row in ipairs(parsed["private_storage"] or NULL) do local stanza, err = parse_xml(row.data); if stanza then private_storage(row.username, hostname, row.namespace, stanza); else print("[error] Private XML parse failed: "..row.username.."@"..hostname); end end table.sort(parsed["spool"] or NULL, function(a,b) return a.seq < b.seq; end); -- sort by sequence number, just in case local time_offset = os.difftime(os.time(os.date("!*t")), os.time(os.date("*t"))) -- to deal with timezones local date_parse = function(s) local year, month, day, hour, min, sec = s:match("(....)-?(..)-?(..)T(..):(..):(..)"); return os.time({year=year, month=month, day=day, hour=hour, min=min, sec=sec-time_offset}); end for _, row in ipairs(parsed["spool"] or NULL) do local stanza, err = parse_xml(row.xml); if stanza then local last_child = stanza.tags[#stanza.tags]; if not last_child or last_child ~= stanza[#stanza] then error("Last child of offline message is not a tag"); end if last_child.name ~= "x" and last_child.attr.xmlns ~= "jabber:x:delay" then error("Last child of offline message is not a timestamp"); end stanza[#stanza], stanza.tags[#stanza.tags] = nil, nil; local t = date_parse(last_child.attr.stamp); offline_msg(row.username, hostname, t, stanza); else print("[error] Offline message XML parsing failed: "..row.username.."@"..hostname); end end prosody-13.0.1/tools/PaxHeaders/erlparse.lua0000644000000000000000000000011714773555365016060 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.803711379 prosody-13.0.1/tools/erlparse.lua0000644000175000017500000001064714773555365020267 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local string_byte, string_char = string.byte, string.char; local t_concat, t_insert = table.concat, table.insert; local type, tonumber, tostring = type, tonumber, tostring; local file = nil; local last = nil; local line = 1; local function read(expected) local ch; if last then ch = last; last = nil; else ch = file:read(1); if ch == "\n" then line = line + 1; end end if expected and ch ~= expected then error("expected: "..expected.."; got: "..(ch or "nil").." on line "..line); end return ch; end local function pushback(ch) if last then error(); end last = ch; end local function peek() if not last then last = read(); end return last; end local _A, _a, _Z, _z, _0, _9, __, _at, _space, _minus = string_byte("AaZz09@_ -", 1, 10); local function isLowerAlpha(ch) ch = string_byte(ch) or 0; return (ch >= _a and ch <= _z); end local function isNumeric(ch) ch = string_byte(ch) or 0; return (ch >= _0 and ch <= _9) or ch == _minus; end local function isAtom(ch) ch = string_byte(ch) or 0; return (ch >= _A and ch <= _Z) or (ch >= _a and ch <= _z) or (ch >= _0 and ch <= _9) or ch == __ or ch == _at; end local function isSpace(ch) ch = string_byte(ch) or "x"; return ch <= _space; end local escapes = {["\\b"]="\b", ["\\d"]="\127", ["\\e"]="\27", ["\\f"]="\f", ["\\n"]="\n", ["\\r"]="\r", ["\\s"]=" ", ["\\t"]="\t", ["\\v"]="\v", ["\\\""]="\"", ["\\'"]="'", ["\\\\"]="\\"}; local function readString() read("\""); -- skip quote local slash = nil; local str = {}; while true do local ch = read(); if slash then slash = slash..ch; if not escapes[slash] then error("Unknown escape sequence: "..slash); end str[#str+1] = escapes[slash]; slash = nil; elseif ch == "\"" then break; elseif ch == "\\" then slash = ch; else str[#str+1] = ch; end end return t_concat(str); end local function readAtom1() local var = { read() }; while isAtom(peek()) do var[#var+1] = read(); end return t_concat(var); end local function readAtom2() local str = { read("'") }; local slash = nil; while true do local ch = read(); str[#str+1] = ch; if ch == "'" and not slash then break; end end return t_concat(str); end local function readNumber() local num = { read() }; while isNumeric(peek()) do num[#num+1] = read(); end if peek() == "." then num[#num+1] = read(); while isNumeric(peek()) do num[#num+1] = read(); end end return tonumber(t_concat(num)); end local readItem = nil; local function readTuple() local t = {}; local s = {}; -- string representation read(); -- read {, or [, or < while true do local item = readItem(); if not item then break; end if type(item) ~= "number" or item > 255 then s = nil; elseif s then s[#s+1] = string_char(item); end t_insert(t, item); end read(); -- read }, or ], or > if s and #s > 0 then return t_concat(s) else return t end; end local function readBinary() read("<"); -- read < -- Discard PIDs if isNumeric(peek()) then while peek() ~= ">" do read(); end read(">"); return {}; end local t = readTuple(); read(">") -- read > local ch = peek(); if type(t) == "string" then -- binary is a list of integers return t; elseif type(t) == "table" then if t[1] then -- binary contains string return t[1]; else -- binary is empty return ""; end; else error(); end end readItem = function() local ch = peek(); if ch == nil then return nil end if ch == "{" or ch == "[" then return readTuple(); elseif isLowerAlpha(ch) then return readAtom1(); elseif ch == "'" then return readAtom2(); elseif isNumeric(ch) then return readNumber(); elseif ch == "\"" then return readString(); elseif ch == "<" then return readBinary(); elseif isSpace(ch) or ch == "," or ch == "|" then read(); return readItem(); else --print("Unknown char: "..ch); return nil; end end local function readChunk() local x = readItem(); if x then read("."); end return x; end local function readFile(filename) file = io.open(filename); if not file then error("File not found: "..filename); os.exit(0); end return function() local x = readChunk(); if not x and peek() then error("Invalid char: "..peek()); end return x; end; end local _M = {}; function _M.parseFile(file) return readFile(file); end return _M; prosody-13.0.1/tools/PaxHeaders/form2table.lua0000644000000000000000000000011714773555365016300 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.803711379 prosody-13.0.1/tools/form2table.lua0000644000175000017500000000274414773555365020506 0ustar00prosodyprosody00000000000000-- 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 = { title = stanza:get_child_text("title"); instructions = stanza:get_child_text("instructions"); }; for tag in stanza:childtags("field") do local field = { name = tag.attr.var; type = tag.attr.type; label = tag.attr.label; desc = tag:get_child_text("desc"); required = tag:get_child("required") and true or nil; value = tag:get_child_text("value"); options = nil; }; if field.type == "list-single" or field.type == "list-multi" then local options = {}; for option in tag:childtags("option") do options[#options+1] = { label = option.attr.label, value = option:get_child_text("value") }; end field.options = options; end if field.type == "jid-multi" or field.type == "list-multi" or field.type == "text-multi" then local values = {}; for value in tag:childtags("value") do values[#values+1] = value:get_text(); end if field.type == "text-multi" then values = table.concat(values, "\n"); end field.value = values; end if field.type == "boolean" then field.value = field.value == "true" or field.value == "1"; end layout[#layout+1] = field; end return layout; end print("dataforms.new " .. require"prosody.util.serialization".serialize(from_stanza(require"prosody.util.xml".parse(io.read("*a"))), { unquoted = true })) prosody-13.0.1/tools/PaxHeaders/generate_format_spec.lua0000644000000000000000000000011714773555365020417 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.803711379 prosody-13.0.1/tools/generate_format_spec.lua0000644000175000017500000000364414773555365022625 0ustar00prosodyprosody00000000000000local format = require"util.format".format; local dump = require"util.serialization".new("oneline") local types = { "nil"; "boolean"; "number"; "string"; "function"; -- "userdata"; "thread"; "table"; }; local example_values = { ["nil"] = { n = 1; nil }; ["boolean"] = { true; false }; ["number"] = { 97; -12345; 1.5; 73786976294838206464; math.huge; 2147483647 }; ["string"] = { "hello"; "foo \1\2\3 bar"; "nödåtgärd"; string.sub("nödåtgärd", 1, -4) }; ["function"] = { function() end }; -- ["userdata"] = {}; ["thread"] = { coroutine.create(function() end) }; ["table"] = { {}, setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}) }; }; local example_strings = setmetatable({ ["nil"] = { "nil" }; ["function"] = { "function() end" }; ["number"] = { "97"; "-12345"; "1.5"; "73786976294838206464"; "math.huge"; "2147483647" }; ["thread"] = { "coroutine.create(function() end)" }; ["table"] = { "{ }", "setmetatable({},{__tostring=function ()return \"foo \\1\\2\\3 bar\"end})" } }, { __index = function() return {} end }); for _, lua_type in ipairs(types) do print(string.format("\t\tdescribe(\"%s\", function ()", lua_type)); local examples = example_values[lua_type]; for fmt in ("cdiouxXaAeEfgGqs"):gmatch(".") do print(string.format("\t\t\tdescribe(\"to %%%s\", function ()", fmt)); print("\t\t\t\tit(\"works\", function ()"); for i = 1, examples.n or #examples do local example = examples[i]; if not tostring(example):match("%w+: 0[xX]%x+") then print(string.format("\t\t\t\t\tassert.equal(%q, format(%q, %s))", format("%" .. fmt, example), "%" .. fmt, example_strings[lua_type][i] or dump(example))); else print(string.format("\t\t\t\t\tassert.matches(\"[%s: 0[xX]%%x+]\", format(%q, %s))", lua_type, "%" .. fmt, example_strings[lua_type][i] or dump(example))); end end print("\t\t\t\tend);"); print("\t\t\tend);"); print() end print("\t\tend);"); print() end prosody-13.0.1/tools/PaxHeaders/http-status-codes.lua0000644000000000000000000000011714773555365017636 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.803711379 prosody-13.0.1/tools/http-status-codes.lua0000644000175000017500000000246114773555365022040 0ustar00prosodyprosody00000000000000-- 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 "prosody.util.xml"; local registry = xml.parse(io.read("*a"), { allow_processing_instructions = true }); io.write([[ local response_codes = { -- Source: http://www.iana.org/assignments/http-status-codes ]]); for record in registry:get_child("registry"):childtags("record") do -- Extract values local value = record:get_child_text("value"); local description = record:get_child_text("description"); local ref = record:get_child_text("xref"); local code = tonumber(value); -- Space between major groups if code and code % 100 == 0 then io.write("\n"); end -- Reserved and Unassigned entries should be not be included if description == "Reserved" or description == "Unassigned" or description == "(Unused)" then code = nil; end -- Non-empty references become comments if ref and ref:find("%S") then ref = " -- " .. ref; else ref = ""; end io.write((code and "\t[%d] = %q;%s\n" or "\t-- [%s] = %q;%s\n"):format(code or value, description, ref)); end io.write([[}; for k,v in pairs(response_codes) do response_codes[k] = k.." "..v; end return setmetatable(response_codes, { __index = function(_, k) return k.." Unassigned"; end }) ]]); prosody-13.0.1/tools/PaxHeaders/jabberd14sql2prosody.lua0000644000000000000000000000011714773555365020223 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.807711396 prosody-13.0.1/tools/jabberd14sql2prosody.lua0000644000175000017500000007526014773555365022434 0ustar00prosodyprosody00000000000000#!/usr/bin/env lua if not pcall(require, "prosody.loader") then pcall(require, "loader"); end do local _parse_sql_actions = { [0] = 0, 1, 0, 1, 1, 2, 0, 2, 2, 0, 9, 2, 0, 10, 2, 0, 11, 2, 0, 13, 2, 1, 2, 2, 1, 6, 3, 0, 3, 4, 3, 0, 3, 5, 3, 0, 3, 7, 3, 0, 3, 8, 3, 0, 3, 12, 4, 0, 2, 3, 7, 4, 0, 3, 8, 11 }; local _parse_sql_trans_keys = { [0] = 0, 0, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 82, 82, 69, 69, 65, 65, 84, 84, 69, 69, 32, 32, 68, 84, 65, 65, 84, 84, 65, 65, 66, 66, 65, 65, 83, 83, 69, 69, 9, 47, 9, 96, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 10, 96, 10, 96, 9, 47, 9, 59, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 65, 65, 66, 66, 76, 76, 69, 69, 32, 32, 73, 96, 70, 70, 32, 32, 78, 78, 79, 79, 84, 84, 32, 32, 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, 40, 10, 10, 32, 41, 32, 32, 75, 96, 69, 69, 89, 89, 32, 32, 96, 96, 10, 96, 10, 96, 10, 10, 82, 82, 73, 73, 77, 77, 65, 65, 82, 82, 89, 89, 32, 32, 75, 75, 69, 69, 89, 89, 32, 32, 78, 78, 73, 73, 81, 81, 85, 85, 69, 69, 32, 32, 75, 75, 10, 96, 10, 96, 10, 10, 10, 59, 10, 59, 82, 82, 79, 79, 80, 80, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 69, 32, 32, 73, 73, 70, 70, 32, 32, 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, 83, 32, 32, 96, 96, 10, 96, 10, 96, 59, 59, 78, 78, 83, 83, 69, 69, 82, 82, 84, 84, 32, 32, 73, 73, 78, 78, 84, 84, 79, 79, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, 86, 10, 41, 32, 32, 86, 86, 65, 65, 76, 76, 85, 85, 69, 69, 83, 83, 32, 32, 40, 40, 39, 78, 10, 92, 10, 92, 41, 44, 44, 59, 32, 78, 48, 57, 41, 57, 48, 57, 41, 57, 85, 85, 76, 76, 76, 76, 34, 116, 79, 79, 67, 67, 75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 69, 83, 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 87, 87, 82, 82, 73, 73, 84, 84, 69, 69, 69, 69, 84, 84, 32, 32, 10, 59, 10, 59, 78, 83, 76, 76, 79, 79, 67, 67, 75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 69, 83, 83, 69, 69, 9, 85, 0 }; local _parse_sql_key_spans = { [0] = 0, 1, 1, 1, 33, 38, 1, 1, 1, 1, 1, 1, 17, 1, 1, 1, 1, 1, 1, 1, 39, 88, 1, 1, 1, 33, 38, 87, 87, 39, 51, 1, 1, 1, 33, 38, 1, 1, 1, 1, 1, 24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 10, 1, 22, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 50, 50, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 47, 32, 1, 1, 1, 1, 1, 1, 1, 1, 1, 40, 83, 83, 4, 16, 47, 10, 17, 10, 17, 1, 1, 1, 83, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 50, 50, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 77 }; local _parse_sql_index_offsets = { [0] = 0, 0, 2, 4, 6, 40, 79, 81, 83, 85, 87, 89, 91, 109, 111, 113, 115, 117, 119, 121, 123, 163, 252, 254, 256, 258, 292, 331, 419, 507, 547, 599, 601, 603, 605, 639, 678, 680, 682, 684, 686, 688, 713, 715, 717, 719, 721, 723, 725, 727, 729, 731, 733, 735, 737, 739, 741, 829, 917, 919, 921, 923, 934, 936, 959, 961, 963, 965, 967, 1055, 1143, 1145, 1147, 1149, 1151, 1153, 1155, 1157, 1159, 1161, 1163, 1165, 1167, 1169, 1171, 1173, 1175, 1177, 1179, 1181, 1269, 1357, 1359, 1410, 1461, 1463, 1465, 1467, 1469, 1471, 1473, 1475, 1477, 1479, 1481, 1483, 1485, 1487, 1489, 1491, 1493, 1495, 1497, 1499, 1501, 1503, 1591, 1679, 1681, 1683, 1685, 1687, 1689, 1691, 1693, 1695, 1697, 1699, 1701, 1703, 1705, 1793, 1881, 1883, 1931, 1964, 1966, 1968, 1970, 1972, 1974, 1976, 1978, 1980, 1982, 2023, 2107, 2191, 2196, 2213, 2261, 2272, 2290, 2301, 2319, 2321, 2323, 2325, 2409, 2411, 2413, 2415, 2417, 2419, 2421, 2423, 2425, 2427, 2429, 2431, 2433, 2521, 2609, 2611, 2613, 2615, 2617, 2619, 2621, 2623, 2625, 2627, 2678, 2729, 2736, 2738, 2740, 2742, 2744, 2746, 2748, 2750, 2752, 2754, 2756, 2758, 2760 }; local _parse_sql_indicies = { [0] = 0, 1, 2, 0, 3, 1, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 3, 3, 3, 6, 3, 7, 1, 8, 1, 9, 1, 10, 1, 11, 1, 12, 1, 13, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 14, 1, 15, 1, 16, 1, 17, 1, 18, 1, 19, 1, 20, 1, 21, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, 1, 25, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, 1, 25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 26, 1, 27, 1, 23, 27, 28, 1, 29, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 30, 28, 29, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 22, 28, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 1, 31, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 33, 31, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36, 1, 37, 1, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36, 1, 37, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 38, 1, 35, 38, 39, 1, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 39, 39, 39, 34, 39, 42, 1, 43, 1, 44, 1, 45, 1, 46, 1, 47, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 48, 1, 49, 1, 50, 1, 51, 1, 52, 1, 53, 1, 54, 1, 55, 1, 56, 1, 57, 1, 58, 1, 59, 1, 60, 1, 61, 1, 48, 1, 63, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 1, 62, 65, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 66, 64, 67, 1, 68, 1, 69, 1, 70, 1, 1, 1, 1, 1, 1, 1, 1, 71, 1, 72, 1, 73, 1, 1, 1, 1, 74, 1, 1, 1, 1, 75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 76, 1, 77, 1, 78, 1, 79, 1, 80, 1, 82, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 1, 81, 82, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 83, 81, 69, 83, 84, 1, 85, 1, 86, 1, 87, 1, 88, 1, 89, 1, 90, 1, 91, 1, 92, 1, 93, 1, 83, 1, 94, 1, 95, 1, 96, 1, 97, 1, 98, 1, 99, 1, 73, 1, 101, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 1, 100, 103, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 104, 102, 105, 83, 106, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 107, 71, 108, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 107, 71, 109, 1, 110, 1, 111, 1, 112, 1, 113, 1, 114, 1, 115, 1, 116, 1, 117, 1, 118, 1, 119, 1, 120, 1, 121, 1, 122, 1, 123, 1, 124, 1, 125, 1, 126, 1, 127, 1, 128, 1, 129, 1, 131, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 1, 130, 131, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 132, 130, 6, 1, 133, 1, 134, 1, 135, 1, 136, 1, 137, 1, 138, 1, 139, 1, 140, 1, 141, 1, 142, 1, 143, 1, 144, 1, 146, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 1, 145, 148, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 149, 147, 150, 1, 151, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 152, 1, 153, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 154, 151, 155, 1, 152, 1, 156, 1, 157, 1, 158, 1, 159, 1, 160, 1, 161, 1, 162, 1, 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, 165, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 166, 1, 168, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 169, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 170, 167, 172, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 173, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 174, 171, 175, 1, 1, 176, 1, 161, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 177, 1, 178, 1, 1, 1, 1, 1, 1, 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, 165, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 166, 1, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 180, 1, 1, 181, 1, 182, 1, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, 1, 180, 1, 1, 181, 1, 1, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, 1, 184, 1, 185, 1, 186, 1, 171, 1, 1, 171, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 171, 1, 171, 1, 1, 171, 1, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 171, 1, 1, 1, 171, 1, 171, 1, 187, 1, 188, 1, 189, 1, 190, 1, 191, 1, 192, 1, 193, 1, 194, 1, 195, 1, 196, 1, 197, 1, 198, 1, 200, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 1, 199, 200, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 201, 199, 202, 1, 203, 1, 204, 1, 205, 1, 206, 1, 132, 1, 207, 1, 208, 1, 209, 1, 210, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 211, 209, 2, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 211, 209, 212, 1, 1, 1, 1, 213, 1, 214, 1, 215, 1, 216, 1, 217, 1, 218, 1, 219, 1, 220, 1, 221, 1, 222, 1, 223, 1, 132, 1, 127, 1, 6, 2, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 224, 1, 225, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 226, 227, 1, 1, 1, 1, 228, 1, 1, 229, 1, 1, 1, 1, 1, 1, 230, 1, 231, 1, 0 }; local _parse_sql_trans_targs = { [0] = 2, 0, 196, 4, 4, 5, 196, 7, 8, 9, 10, 11, 12, 13, 36, 14, 15, 16, 17, 18, 19, 20, 21, 21, 22, 24, 27, 23, 25, 25, 26, 28, 28, 29, 30, 30, 31, 33, 32, 34, 34, 35, 37, 38, 39, 40, 41, 42, 56, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 57, 57, 57, 57, 58, 59, 60, 61, 62, 92, 63, 64, 71, 82, 89, 65, 66, 67, 68, 69, 69, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 90, 90, 90, 90, 91, 70, 92, 93, 196, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 116, 117, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 131, 131, 131, 132, 133, 134, 137, 134, 135, 136, 138, 139, 140, 141, 142, 143, 144, 145, 150, 151, 154, 146, 146, 147, 157, 146, 146, 147, 157, 148, 149, 196, 144, 151, 148, 149, 152, 153, 155, 156, 147, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 171, 172, 173, 174, 175, 176, 177, 179, 180, 181, 181, 182, 184, 195, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 1, 3, 6, 94, 118, 158, 178, 183 }; local _parse_sql_trans_actions = { [0] = 1, 0, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 20, 1, 3, 30, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 20, 1, 3, 26, 3, 3, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 20, 1, 3, 42, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 11, 1, 5, 5, 1, 5, 20, 46, 5, 1, 3, 34, 1, 14, 1, 17, 1, 1, 51, 38, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; local parse_sql_start = 196; local parse_sql_first_final = 196; local parse_sql_error = 0; local parse_sql_en_main = 196; local _sql_unescapes = setmetatable({ ["\\0"] = "\0"; ["\\'"] = "'"; ["\\\""] = "\""; ["\\b"] = "\b"; ["\\n"] = "\n"; ["\\r"] = "\r"; ["\\t"] = "\t"; ["\\Z"] = "\26"; ["\\\\"] = "\\"; ["\\%"] = "%"; ["\\_"] = "_"; },{ __index = function(t, s) assert(false, "Unknown escape sequences: "..s); end }); function parse_sql(data, h) local p = 1; local pe = #data + 1; local cs; local pos_char, pos_line = 1, 1; local mark, token; local table_name, columns, value_lists, value_list, value_count; cs = parse_sql_start; -- ragel flat exec local testEof = false; local _slen = 0; local _trans = 0; local _keys = 0; local _inds = 0; local _acts = 0; local _nacts = 0; local _tempval = 0; local _goto_level = 0; local _resume = 10; local _eof_trans = 15; local _again = 20; local _test_eof = 30; local _out = 40; while true do -- goto loop local _continue = false; repeat local _trigger_goto = false; if _goto_level <= 0 then -- noEnd if p == pe then _goto_level = _test_eof; _continue = true; break; end -- errState != 0 if cs == 0 then _goto_level = _out; _continue = true; break; end end -- _goto_level <= 0 if _goto_level <= _resume then _keys = cs * 2; -- LOCATE_TRANS _inds = _parse_sql_index_offsets[cs]; _slen = _parse_sql_key_spans[cs]; if _slen > 0 and _parse_sql_trans_keys[_keys] <= data:byte(p) and data:byte(p) <= _parse_sql_trans_keys[_keys + 1] then _trans = _parse_sql_indicies[ _inds + data:byte(p) - _parse_sql_trans_keys[_keys] ]; else _trans =_parse_sql_indicies[ _inds + _slen ]; end cs = _parse_sql_trans_targs[_trans]; if _parse_sql_trans_actions[_trans] ~= 0 then _acts = _parse_sql_trans_actions[_trans]; _nacts = _parse_sql_actions[_acts]; _acts = _acts + 1; while _nacts > 0 do _nacts = _nacts - 1; _acts = _acts + 1; _tempval = _parse_sql_actions[_acts - 1]; -- start action switch if _tempval == 0 then --4 FROM_STATE_ACTION_SWITCH -- line 34 "sql.rl" -- end of line directive pos_char = pos_char + 1; -- ACTION elseif _tempval == 1 then --4 FROM_STATE_ACTION_SWITCH -- line 35 "sql.rl" -- end of line directive pos_line = pos_line + 1; pos_char = 1; -- ACTION elseif _tempval == 2 then --4 FROM_STATE_ACTION_SWITCH -- line 38 "sql.rl" -- end of line directive mark = p; -- ACTION elseif _tempval == 3 then --4 FROM_STATE_ACTION_SWITCH -- line 39 "sql.rl" -- end of line directive token = data:sub(mark, p-1); -- ACTION elseif _tempval == 4 then --4 FROM_STATE_ACTION_SWITCH -- line 52 "sql.rl" -- end of line directive table.insert(columns, token); columns[#columns] = token; -- ACTION elseif _tempval == 5 then --4 FROM_STATE_ACTION_SWITCH -- line 58 "sql.rl" -- end of line directive table_name,columns = token,{}; -- ACTION elseif _tempval == 6 then --4 FROM_STATE_ACTION_SWITCH -- line 59 "sql.rl" -- end of line directive h.create(table_name, columns); -- ACTION elseif _tempval == 7 then --4 FROM_STATE_ACTION_SWITCH -- line 65 "sql.rl" -- end of line directive value_count = value_count + 1; value_list[value_count] = token:gsub("\\.", _sql_unescapes); -- ACTION elseif _tempval == 8 then --4 FROM_STATE_ACTION_SWITCH -- line 68 "sql.rl" -- end of line directive value_count = value_count + 1; value_list[value_count] = tonumber(token); -- ACTION elseif _tempval == 9 then --4 FROM_STATE_ACTION_SWITCH -- line 69 "sql.rl" -- end of line directive value_count = value_count + 1; -- ACTION elseif _tempval == 10 then --4 FROM_STATE_ACTION_SWITCH -- line 71 "sql.rl" -- end of line directive value_list,value_count = {},0; -- ACTION elseif _tempval == 11 then --4 FROM_STATE_ACTION_SWITCH -- line 71 "sql.rl" -- end of line directive table.insert(value_lists, value_list); -- ACTION elseif _tempval == 12 then --4 FROM_STATE_ACTION_SWITCH -- line 74 "sql.rl" -- end of line directive table_name,value_lists = token,{}; -- ACTION elseif _tempval == 13 then --4 FROM_STATE_ACTION_SWITCH -- line 75 "sql.rl" -- end of line directive h.insert(table_name, value_lists); -- ACTION end -- line 355 "sql.lua" -- end of line directive -- end action switch end -- while _nacts end if _trigger_goto then _continue = true; break; end end -- endif if _goto_level <= _again then if cs == 0 then _goto_level = _out; _continue = true; break; end p = p + 1; if p ~= pe then _goto_level = _resume; _continue = true; break; end end -- _goto_level <= _again if _goto_level <= _test_eof then end -- _goto_level <= _test_eof if _goto_level <= _out then break; end _continue = true; until true; if not _continue then break; end end -- endif _goto_level <= out -- end of execute block if cs < parse_sql_first_final then print("parse_sql: there was an error, line "..pos_line.." column "..pos_char); else print("Success. EOF at line "..pos_line.." column "..pos_char) end end end -- import modules package.path = package.path..";../?.lua;"; local my_name = arg[0]; if my_name:match("[/\\]") then package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua"); package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so"); end -- ugly workaround for getting datamanager to work outside of prosody :( prosody = { }; prosody.platform = "unknown"; if os.getenv("WINDIR") then prosody.platform = "windows"; elseif package.config:sub(1,1) == "/" then prosody.platform = "_posix"; end package.loaded["util.logger"] = {init = function() return function() end; end} local dm = require "prosody.util.datamanager"; dm.set_data_path("data"); local datetime = require "prosody.util.datetime"; 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 local ret, err = dm.store(username, host, "accounts", {password = password}); print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password); end function store_vcard(username, host, stanza) -- create or update vCard for username@host local ret, err = dm.store(username, host, "vcard", st.preserialize(stanza)); print("["..(err or "success").."] stored vCard: "..username.."@"..host); end function store_roster(username, host, roster_items) -- fetch current roster-table for username@host if they already have one local roster = dm.load(username, host, "roster") or {}; -- merge imported roster-items with loaded roster for item_tag in roster_items:childtags() do -- jid for this roster-item local item_jid = item_tag.attr.jid -- validate item stanzas if (item_tag.name == "item") and (item_jid ~= "") then -- prepare roster item -- TODO: is the subscription attribute optional? local item = {subscription = item_tag.attr.subscription, groups = {}}; -- optional: give roster item a real name if item_tag.attr.name then item.name = item_tag.attr.name; end -- optional: iterate over group stanzas inside item stanza for group_tag in item_tag:childtags() do local group_name = group_tag:get_text(); if (group_tag.name == "group") and (group_name ~= "") then item.groups[group_name] = true; else print("[error] invalid group stanza: "..group_tag:pretty_print()); end end -- store item in roster roster[item_jid] = item; print("[success] roster entry: " ..username.."@"..host.." - "..item_jid); else print("[error] invalid roster stanza: " ..item_tag:pretty_print()); end end -- store merged roster-table local ret, err = dm.store(username, host, "roster", roster); print("["..(err or "success").."] stored roster: " ..username.."@"..host); end function store_subscription_request(username, host, presence_stanza) local from_bare = presence_stanza.attr.from; -- fetch current roster-table for username@host if they already have one local roster = dm.load(username, host, "roster") or {}; local item = roster[from_bare]; if item and (item.subscription == "from" or item.subscription == "both") then return; -- already subscribed, do nothing end -- add to table of pending subscriptions if not roster.pending then roster.pending = {}; end roster.pending[from_bare] = true; -- store updated roster-table local ret, err = dm.store(username, host, "roster", roster); print("["..(err or "success").."] stored subscription request: " ..username.."@"..host.." - "..from_bare); end local os_date = os.date; local os_time = os.time; local os_difftime = os.difftime; function datetime_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+%-]?.*)$"); if year then local time_offset = os_difftime(os_time(os_date("*t")), os_time(os_date("!*t"))); -- to deal with local timezone local tzd_offset = 0; if tzd ~= "" and tzd ~= "Z" then local sign, h, m = tzd:match("([+%-])(%d%d):?(%d*)"); if not sign then return; end if #m ~= 2 then m = "0"; end h, m = tonumber(h), tonumber(m); 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}); end end end function store_offline_messages(username, host, stanza) -- TODO: maybe use list_load(), append and list_store() instead -- of constantly reopening the file with list_append()? --for ch in offline_messages:childtags() do --print("message :"..ch:pretty_print()); stanza.attr.node = nil; local stamp = stanza:get_child("x", "jabber:x:delay"); if not stamp or not stamp.attr.stamp then print(2) return; end for i=1,#stanza do if stanza[i] == stamp then table.remove(stanza, i); break; end end for i=1,#stanza.tags do if stanza.tags[i] == stamp then table.remove(stanza.tags, i); break; end end local parsed_stamp = datetime_parse(stamp.attr.stamp); if not parsed_stamp then print(1, stamp.attr.stamp) return; end stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(parsed_stamp), datetime.legacy(parsed_stamp); local ret, err = dm.list_append(username, host, "offline", st.preserialize(stanza)); print("["..(err or "success").."] stored offline message: " ..username.."@"..host.." - "..stanza.attr.from); --end end -- load data local arg = ...; local help = "/? -? ? /h -h /help -help --help"; if not arg or help:find(arg, 1, true) then print([[XEP-227 importer for Prosody Usage: jabberd14sql2prosody.lua filename.sql ]]); os.exit(1); end local f = io.open(arg); local s = f:read("*a"); f:close(); local table_count = 0; local insert_count = 0; local row_count = 0; -- parse parse_sql(s, { create = function(table_name, columns) --[[print(table_name);]] table_count = table_count + 1; end; insert = function(table_name, value_lists) --[[print(table_name, #value_lists);]] insert_count = insert_count + 1; row_count = row_count + #value_lists; for _,value_list in ipairs(value_lists) do if table_name == "users" then local user, realm, password = unpack(value_list); store_password(user, realm, password); elseif table_name == "roster" then local user, realm, xml = unpack(value_list); local stanza,err = parse_xml(xml); if stanza then store_roster(user, realm, stanza); else print("[error] roster: XML parsing failed for "..user.."@"..realm..": "..err); end elseif table_name == "vcard" then local user, realm, name, email, nickname, birthday, photo, xml = unpack(value_list); if xml then local stanza,err = parse_xml(xml); if stanza then store_vcard(user, realm, stanza); else print("[error] vcard: XML parsing failed for "..user.."@"..realm..": "..err); end else --print("[warn] vcard: NULL vCard for "..user.."@"..realm..": "..err); end elseif table_name == "storedsubscriptionrequests" then local user, realm, fromjid, xml = unpack(value_list); local stanza,err = parse_xml(xml); if stanza then store_subscription_request(user, realm, stanza); else print("[error] storedsubscriptionrequests: XML parsing failed for "..user.."@"..realm..": "..err); end elseif table_name == "messages" then --local user, realm, node, correspondent, type, storetime, delivertime, subject, body, xml = unpack(value_list); local user, realm, type, xml = value_list[1], value_list[2], value_list[5], value_list[10]; if type == "offline" and xml ~= "" then local stanza,err = parse_xml(xml); if stanza then store_offline_messages(user, realm, stanza); else print("[error] offline messages: XML parsing failed for "..user.."@"..realm..": "..err); print(unpack(value_list)); end end end end end; }); print("table_count", table_count); print("insert_count", insert_count); print("row_count", row_count); prosody-13.0.1/tools/PaxHeaders/linedebug.lua0000644000000000000000000000011714773555365016201 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.807711396 prosody-13.0.1/tools/linedebug.lua0000644000175000017500000000070214773555365020377 0ustar00prosodyprosody00000000000000local data = {} local getinfo = debug.getinfo; local function linehook(ev, li) local S = getinfo(2, "S"); if S and S.source and S.source:match"^@" then local file = S.source:sub(2); local lines = data[file]; if not lines then lines = {}; data[file] = lines; for line in io.lines(file) do lines[#lines+1] = line; end end io.stderr:write(ev, " ", file, " ", li, " ", lines[li], "\n"); end end debug.sethook(linehook, "l"); prosody-13.0.1/tools/PaxHeaders/make_repo.lua0000644000000000000000000000011714773555365016205 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.807711396 prosody-13.0.1/tools/make_repo.lua0000644000175000017500000000345414773555365020412 0ustar00prosodyprosody00000000000000print("Getting all the available modules") if os.execute '[ -e "./downloaded_modules" ]' then os.execute("rm -rf downloaded_modules") end os.execute("hg clone https://hg.prosody.im/prosody-modules/ downloaded_modules") local i, popen = 0, io.popen local flag = "mod_" if os.execute '[ -e "./repository" ]' then os.execute("mkdir repository") end local pfile = popen('ls -a "downloaded_modules"') for filename in pfile:lines() do i = i + 1 if filename:sub(1, #flag) == flag then local file = io.open("repository/"..filename.."-scm-1.rockspec", "w") file:write('package = "'..filename..'"', '\n') file:write('version = "scm-1"', '\n') file:write('source = {', '\n') file:write('\turl = "hg+https://hg.prosody.im/prosody-modules",', '\n') file:write('\tdir = "prosody-modules"', '\n') file:write('}', '\n') file:write('description = {', '\n') file:write('\thomepage = "https://prosody.im/",', '\n') file:write('\tlicense = "MIT"', '\n') file:write('}', '\n') file:write('dependencies = {', '\n') file:write('\t"lua >= 5.1"', '\n') file:write('}', '\n') file:write('build = {', '\n') file:write('\ttype = "builtin",', '\n') file:write('\tmodules = {', '\n') file:write('\t\t["'..filename..'.'..filename..'"] = "'..filename..'/'..filename..'.lua"', '\n') file:write('\t}', '\n') file:write('}', '\n') file:close() end end pfile:close() os.execute("cd repository/ && luarocks-admin make_manifest ./ && chmod -R 644 ./*") print("") print("Done!. Modules' sources are locally available at ./downloaded_modules") print("Repository is available at ./repository") print("The repository contains all of prosody modules' respective rockspecs, as well as manifest files and an html Index") print("You can now either point your server to this folder, or copy its contents to another configured folder.") prosody-13.0.1/tools/PaxHeaders/migration0000644000000000000000000000013114773555365015450 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.807711396 30 ctime=1743706869.811711412 prosody-13.0.1/tools/migration/0000755000175000017500000000000014773555365017730 5ustar00prosodyprosody00000000000000prosody-13.0.1/tools/migration/PaxHeaders/Makefile0000644000000000000000000000011714773555365017171 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.807711396 prosody-13.0.1/tools/migration/Makefile0000644000175000017500000000246614773555365021400 0ustar00prosodyprosody00000000000000 include ../../config.unix BIN = $(DESTDIR)$(PREFIX)/bin CONFIG = $(DESTDIR)$(SYSCONFDIR) SOURCE = $(DESTDIR)$(LIBDIR)/prosody DATA = $(DESTDIR)$(DATADIR) MAN = $(DESTDIR)$(PREFIX)/share/man INSTALLEDSOURCE = $(LIBDIR)/prosody INSTALLEDCONFIG = $(SYSCONFDIR) INSTALLEDMODULES = $(LIBDIR)/prosody/modules INSTALLEDDATA = $(DATADIR) all: prosody-migrator.install migrator.cfg.lua.install prosody-migrator.lua install: prosody-migrator.install migrator.cfg.lua.install install -d $(BIN) $(CONFIG) $(SOURCE) install -d $(MAN)/man1 install -m755 ./prosody-migrator.install $(BIN)/prosody-migrator test -e $(CONFIG)/migrator.cfg.lua || install -m644 migrator.cfg.lua.install $(CONFIG)/migrator.cfg.lua clean: rm -f prosody-migrator.install rm -f migrator.cfg.lua.install prosody-migrator.install: prosody-migrator.lua sed "1s/\blua\b/$(RUNWITH)/; \ s|^CFG_SOURCEDIR=.*;$$|CFG_SOURCEDIR='$(INSTALLEDSOURCE)';|; \ s|^CFG_CONFIGDIR=.*;$$|CFG_CONFIGDIR='$(INSTALLEDCONFIG)';|; \ s|^CFG_DATADIR=.*;$$|CFG_DATADIR='$(INSTALLEDDATA)';|; \ s|^CFG_PLUGINDIR=.*;$$|CFG_PLUGINDIR='$(INSTALLEDMODULES)/';|;" \ < prosody-migrator.lua > prosody-migrator.install migrator.cfg.lua.install: migrator.cfg.lua sed "s|^local data_path = .*;$$|local data_path = '$(INSTALLEDDATA)';|;" \ < migrator.cfg.lua > migrator.cfg.lua.install prosody-13.0.1/tools/migration/PaxHeaders/migrator0000644000000000000000000000013114773555365017274 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.811711412 30 ctime=1743706869.811711412 prosody-13.0.1/tools/migration/migrator/0000755000175000017500000000000014773555365021554 5ustar00prosodyprosody00000000000000prosody-13.0.1/tools/migration/migrator/PaxHeaders/jabberd14.lua0000644000000000000000000000011714773555365021616 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.811711412 prosody-13.0.1/tools/migration/migrator/jabberd14.lua0000644000175000017500000001075414773555365024024 0ustar00prosodyprosody00000000000000 if not pcall(require, "prosody.loader") then pcall(require, "loader"); end local lfs = require "lfs"; 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; local ipairs = ipairs; local coroutine = coroutine; local print = print; local function is_dir(path) return lfs.attributes(path, "mode") == "directory"; end local function is_file(path) return lfs.attributes(path, "mode") == "file"; end local function clean_path(path) return path:gsub("\\", "/"):gsub("//+", "/"):gsub("^~", os_getenv("HOME") or "~"); end local function load_xml(path) local f, err = io_open(path); if not f then return f, err; end local data = f:read("*a"); f:close(); if not data then return; end return parse_xml(data); end local function load_spool_file(host, filename, path) local xml = load_xml(path); if not xml then return; end local register_element = xml:get_child("query", "jabber:iq:register"); local username_element = register_element and register_element:get_child("username", "jabber:iq:register"); local password_element = register_element and register_element:get_child("password", "jabber:iq:auth"); local username = username_element and username_element:get_text(); local password = password_element and password_element:get_text(); if not username then print("[warn] Missing /xdb/{jabber:iq:register}register/username> in file "..filename) return; elseif username..".xml" ~= filename then print("[warn] Missing /xdb/{jabber:iq:register}register/username does not match filename "..filename); return; end local userdata = { user = username; host = host; stores = {}; }; local stores = userdata.stores; stores.accounts = { password = password }; for i=1,#xml.tags do local tag = xml.tags[i]; local xname = (tag.attr.xmlns or "")..":"..tag.name; if tag.attr.j_private_flag == "1" and tag.attr.xmlns then -- Private XML stores.private = stores.private or {}; tag.attr.j_private_flag = nil; stores.private[tag.attr.xmlns] = st.preserialize(tag); elseif xname == "jabber:iq:auth:password" then if stores.accounts.password ~= tag:get_text() then if password then print("[warn] conflicting passwords") else stores.accounts.password = tag:get_text(); end end elseif xname == "jabber:iq:register:query" then -- already processed elseif xname == "jabber:xdb:nslist:foo" then -- ignore elseif xname == "jabber:iq:auth:0k:zerok" then -- ignore elseif xname == "jabber:iq:roster:query" then -- Roster local roster = {}; local subscription_types = { from = true, to = true, both = true, none = true }; for _,item_element in ipairs(tag.tags) do assert(item_element.name == "item"); assert(item_element.attr.jid); assert(subscription_types[item_element.attr.subscription]); assert((item_element.attr.ask or "subscribe") == "subscribe") if item_element.name == "item" then local groups = {}; for _,group_element in ipairs(item_element.tags) do assert(group_element.name == "group"); groups[group_element:get_text()] = true; end local item = { name = item_element.attr.name; subscription = item_element.attr.subscription; ask = item_element.attr.ask; groups = groups; }; roster[item_element.attr.jid] = item; end end stores.roster = roster; elseif xname == "jabber:iq:last:query" then -- Last activity elseif xname == "jabber:x:offline:foo" then -- Offline messages elseif xname == "vcard-temp:vCard" then -- vCards stores.vcard = st.preserialize(tag); else print("[warn] Unknown tag: "..xname); end end return userdata; end local function loop_over_users(path, host, cb) for file in lfs.dir(path) do if file:match("%.xml$") then local user = load_spool_file(host, file, path.."/"..file); if user then cb(user); end end end end local function loop_over_hosts(path, cb) for host in lfs.dir(path) do if host ~= "." and host ~= ".." and is_dir(path.."/"..host) then loop_over_users(path.."/"..host, host, cb); end end end local function reader(input) local path = clean_path(assert(input.path, "no input.path specified")); assert(is_dir(path), "input.path is not a directory"); if input.host then return coroutine.wrap(function() loop_over_users(input.path, input.host, coroutine.yield) end); else return coroutine.wrap(function() loop_over_hosts(input.path, coroutine.yield) end); end end return { reader = reader; }; prosody-13.0.1/tools/migration/PaxHeaders/migrator.cfg.lua0000644000000000000000000000011714773555365020616 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.807711396 prosody-13.0.1/tools/migration/migrator.cfg.lua0000644000175000017500000000145214773555365023017 0ustar00prosodyprosody00000000000000local data_path = "../../data"; local vhost = { "accounts", "account_details", "account_roles", "roster", "vcard", "private", "blocklist", "privacy", "archive-archive", "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; } output { type = "sql"; driver = "SQLite3"; database = data_path.."/prosody.sqlite"; } --[[ input { type = "internal"; path = data_path; } output { type = "sql"; driver = "SQLite3"; database = data_path.."/prosody.sqlite"; } ]] prosody-13.0.1/tools/migration/PaxHeaders/prosody-migrator.lua0000644000000000000000000000011714773555365021555 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.811711412 prosody-13.0.1/tools/migration/prosody-migrator.lua0000644000175000017500000001713514773555365023763 0ustar00prosodyprosody00000000000000#!/usr/bin/env lua CFG_SOURCEDIR=CFG_SOURCEDIR or os.getenv("PROSODY_SRCDIR"); CFG_CONFIGDIR=CFG_CONFIGDIR or os.getenv("PROSODY_CFGDIR"); CFG_PLUGINDIR=CFG_PLUGINDIR or os.getenv("PROSODY_PLUGINDIR"); CFG_DATADIR=CFG_DATADIR or os.getenv("PROSODY_DATADIR"); -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- local function is_relative(path) local path_sep = package.config:sub(1,1); return ((path_sep == "/" and path:sub(1,1) ~= "/") or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\"))) end -- Tell Lua where to find our libraries if CFG_SOURCEDIR then local function filter_relative_paths(path) if is_relative(path) then return ""; end end local function sanitise_paths(paths) return (paths:gsub("[^;]+;?", filter_relative_paths):gsub(";;+", ";")); end package.path = sanitise_paths(CFG_SOURCEDIR.."/?.lua;"..package.path); package.cpath = sanitise_paths(CFG_SOURCEDIR.."/?.so;"..package.cpath); end -- Substitute ~ with path to home directory in data path if CFG_DATADIR then if os.getenv("HOME") then CFG_DATADIR = CFG_DATADIR:gsub("^~", os.getenv("HOME")); end end local default_config = (CFG_CONFIGDIR or ".").."/migrator.cfg.lua"; local function usage() print("Usage: " .. arg[0] .. " [OPTIONS] FROM_STORE TO_STORE"); print(" --config FILE Specify config file") print(" --keep-going Keep going in case of errors"); print(" -v, --verbose Increase log-level"); print(""); print("If no stores are specified, 'input' and 'output' are used."); end 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" }; value_params = { config = true }; }); startup.init_global_state(); prosody.process_type = "migrator"; if prosody.opts.help then usage(); os.exit(0); end startup.force_console_logging(); startup.init_logging(); startup.init_gc(); startup.init_errors(); startup.setup_plugindir(); startup.setup_plugin_install_path(); startup.setup_datadir(); startup.chdir(); startup.read_version(); startup.switch_user(); startup.check_dependencies(); startup.log_startup_warnings(); prosody.config_loaded = true; startup.load_libraries(); startup.init_http_client(); prosody.core_post_stanza = function () -- silence assert in core.moduleapi error("Attempt to send stanzas from inside migrator.", 0); end end -- Command-line parsing local options = prosody.opts; local envloadfile = require "prosody.util.envload".envloadfile; local config_file = options.config or default_config; local from_store = arg[1] or "input"; local to_store = arg[2] or "output"; config = {}; local config_env = setmetatable({}, { __index = function(t, k) return function(tbl) config[k] = tbl; end; end }); local config_chunk, err = envloadfile(config_file, config_env); if not config_chunk then print("There was an error loading the config file, check that the file exists"); print("and that the syntax is correct:"); print("", err); os.exit(1); end config_chunk(); local have_err; if #arg > 0 and #arg ~= 2 then have_err = true; print("Error: Incorrect number of parameters supplied."); end if not config[from_store] then have_err = true; print("Error: Input store '"..from_store.."' not found in the config file."); end if not config[to_store] then have_err = true; print("Error: Output store '"..to_store.."' not found in the config file."); end for store, conf in pairs(config) do -- COMPAT if conf.type == "prosody_files" then conf.type = "internal"; elseif conf.type == "prosody_sql" then conf.type = "sql"; end end if have_err then print(""); usage(); print(""); print("The available stores in your migrator config are:"); print(""); for store in pairs(config) do print("", store); end print(""); os.exit(1); end local async = require "prosody.util.async"; local server = require "prosody.net.server"; local watchers = { error = function (_, err) error(err); end; waiting = function () server.loop(); end; }; 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 log("debug", "Using store user iterator") return store:users(); else log("debug", "Using usermanager user iterator") return um.users(host); end end local function prepare_config(host, conf) if conf.type == "internal" then sm.olddm.set_data_path(conf.path or prosody.paths.data); 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) prepare_config(host, conf); return assert(sm.load_driver(host, conf.type)); end local migrate_once = { keyval = function(origin, destination, user) local data, err = origin:get(user); assert(not err, err); assert(destination:set(user, data)); end; archive = function(origin, destination, user) local iter, err = origin:find(user); assert(iter, err); for id, item, when, with in iter do assert(destination:append(user, id, item, when, with)); end end; } migrate_once.pubsub = function(origin, destination, user, prefix, input_driver, output_driver) if not user and prefix == "pubsub_" then return end local data, err = origin:get(user); assert(not err, err); if not data then return end assert(destination:set(user, data)); if prefix == "pubsub_" then user = nil end for node in pairs(data) do local pep_origin = assert(input_driver:open(prefix .. node, "archive")); local pep_destination = assert(output_driver:open(prefix .. node, "archive")); migrate_once.archive(pep_origin, pep_destination, user); end end if options["keep-going"] then 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) if user then log("error", "Error migrating data for user %q: %s", user, err); else log("error", "Error migrating data for host: %s", err); end log("debug", "%s", debug.traceback(nil, 2)); end xpcall(f, log_err, origin, destination, user, ...); end end end local migration_runner = async.runner(function (job) for host, stores in pairs(job.input.hosts) do prosody.hosts[host] = startup.make_host(host); sm.initialize_host(host); um.initialize_host(host); local input_driver = get_driver(host, job.input); local output_driver = get_driver(host, job.output); for _, store in ipairs(stores) do local p, typ = store:match("()%-(%w+)$"); if typ then store = store:sub(1, p-1); else typ = "keyval"; end log("info", "Migrating host %s store %s (%s)", host, store, typ); local migrate = assert(migrate_once[typ], "Unknown store type: "..typ); local prefix = store .. "_"; if typ == "pubsub" then typ = "keyval"; end if store == "pubsub_nodes" then prefix = "pubsub_"; end local origin = assert(input_driver:open(store, typ)); local destination = assert(output_driver:open(store, typ)); migrate(origin, destination, nil, prefix, input_driver, output_driver); -- host data for user in users(origin, host) do log("info", "Migrating user %s@%s store %s (%s)", user, host, store, typ); migrate(origin, destination, user, prefix, input_driver, output_driver); end end end end, watchers); io.stderr:write("Migrating...\n"); migration_runner:run({ input = config[from_store], output = config[to_store] }); io.stderr:write("Done!\n"); prosody-13.0.1/tools/PaxHeaders/mod2spec.sh0000644000000000000000000000011714773555365015610 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.811711412 prosody-13.0.1/tools/mod2spec.sh0000755000175000017500000000006414773555365020012 0ustar00prosodyprosody00000000000000#!/bin/bash set -eu echo "spec/${1//./_}_spec.lua" prosody-13.0.1/tools/PaxHeaders/modtrace.lua0000644000000000000000000000011714773555365016041 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.811711412 prosody-13.0.1/tools/modtrace.lua0000644000175000017500000000771514773555365020252 0ustar00prosodyprosody00000000000000-- Trace module calls and method calls on created objects -- -- Very rough and for debugging purposes only. It makes many -- assumptions and there are many ways it could fail. -- -- Example use: -- -- local dbuffer = require "tools.modtrace".trace("util.dbuffer"); -- local t_pack = table.pack; local serialize = require "util.serialization".serialize; local unpack = table.unpack; local set = require "util.set"; local serialize_cfg = { preset = "oneline"; freeze = true; fatal = false; fallback = function (v) return "<"..tostring(v)..">" end; }; local function stringify_value(v) if type(v) == "string" and #v > 20 then return (""):format(#v); elseif type(v) == "function" then return tostring(v); end return serialize(v, serialize_cfg); end local function stringify_params(...) local n = select("#", ...); local r = {}; for i = 1, n do table.insert(r, stringify_value((select(i, ...)))); end return table.concat(r, ", "); end local function stringify_result(ret) local r = {}; for i = 1, ret.n do table.insert(r, stringify_value(ret[i])); end return table.concat(r, ", "); end local function stringify_call(method_name, ...) return ("%s(%s)"):format(method_name, stringify_params(...)); end local function wrap_method(original_obj, original_method, method_name) method_name = ("<%s>:%s"):format(getmetatable(original_obj).__name or "object", method_name); return function (new_obj_self, ...) local opts = new_obj_self._modtrace_opts; local f = opts.output or io.stderr; f:write(stringify_call(method_name, ...)); local ret = t_pack(original_method(original_obj, ...)); if ret.n > 0 then f:write(" = ", stringify_result(ret), "\n"); else f:write("\n"); end return unpack(ret, 1, ret.n); end; end local function wrap_function(original_function, function_name, opts) local f = opts.output or io.stderr; return function (...) f:write(stringify_call(function_name, ...)); local ret = t_pack(original_function(...)); if ret.n > 0 then f:write(" = ", stringify_result(ret), "\n"); else f:write("\n"); end return unpack(ret, 1, ret.n); end; end local function wrap_metamethod(name, method) if name == "__index" then return function (new_obj, k) local original_method; if type(method) == "table" then original_method = new_obj._modtrace_original_obj[k]; else original_method = method(new_obj._modtrace_original_obj, k); end if original_method == nil then return nil; end return wrap_method(new_obj._modtrace_original_obj, original_method, k); end; end return function (new_obj, ...) return method(new_obj._modtrace_original_obj, ...); end; end local function wrap_mt(original_mt) local new_mt = {}; for k, v in pairs(original_mt) do new_mt[k] = wrap_metamethod(k, v); end return new_mt; end local function wrap_obj(original_obj, opts) local new_mt = wrap_mt(getmetatable(original_obj)); return setmetatable({_modtrace_original_obj = original_obj, _modtrace_opts = opts}, new_mt); end local function wrap_new(original_new, function_name, opts) local f = opts.output or io.stderr; return function (...) f:write(stringify_call(function_name, ...)); local ret = t_pack(original_new(...)); local obj = ret[1]; if ret.n == 1 and type(ret[1]) == "table" then f:write(" = <", getmetatable(ret[1]).__name or "object", ">", "\n"); elseif ret.n > 0 then f:write(" = ", stringify_result(ret), "\n"); else f:write("\n"); end if obj then ret[1] = wrap_obj(obj, opts); end return unpack(ret, 1, ret.n); end; end local function trace(module, opts) if type(module) == "string" then module = require(module); end opts = opts or {}; local new_methods = set.new(opts.new_methods or {"new"}); local fake_module = setmetatable({}, { __index = function (_, k) if new_methods:contains(k) then return wrap_new(module[k], k, opts); else return wrap_function(module[k], k, opts); end end; }); return fake_module; end return { wrap = trace; trace = trace; } prosody-13.0.1/tools/PaxHeaders/openfire2prosody.lua0000644000000000000000000000011714773555365017554 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.811711412 prosody-13.0.1/tools/openfire2prosody.lua0000644000175000017500000000632214773555365021756 0ustar00prosodyprosody00000000000000#!/usr/bin/env lua -- Prosody IM -- Copyright (C) 2008-2009 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- package.path = package.path..";../?.lua"; package.cpath = package.cpath..";../?.so"; -- needed for util.pposix used in datamanager local my_name = arg[0]; 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 -- ugly workaround for getting datamanager to work outside of prosody :( prosody = { }; prosody.platform = "unknown"; if os.getenv("WINDIR") then prosody.platform = "windows"; elseif package.config:sub(1,1) == "/" then prosody.platform = "posix"; end local parse_xml = require "prosody.util.xml".parse; ----------------------------------------------------------------------- package.loaded["util.logger"] = {init = function() return function() end; end} local dm = require "prosody.util.datamanager" dm.set_data_path("data"); local arg = ...; local help = "/? -? ? /h -h /help -help --help"; if not arg or help:find(arg, 1, true) then print([[Openfire importer for Prosody Usage: openfire2prosody.lua filename.xml hostname ]]); os.exit(1); end local host = select(2, ...) or "localhost"; local file = assert(io.open(arg)); local data = assert(file:read("*a")); file:close(); local xml = assert(parse_xml(data)); assert(xml.name == "Openfire", "The input file is not an Openfire XML export"); local substatus_mapping = { ["0"] = "none", ["1"] = "to", ["2"] = "from", ["3"] = "both" }; for _,tag in ipairs(xml.tags) do if tag.name == "User" then local username, password, roster; for _,tag in ipairs(tag.tags) do if tag.name == "Username" then username = tag:get_text(); elseif tag.name == "Password" then password = tag:get_text(); elseif tag.name == "Roster" then roster = {}; local pending = {}; for _,tag in ipairs(tag.tags) do if tag.name == "Item" then local jid = assert(tag.attr.jid, "Roster item has no JID"); if tag.attr.substatus ~= "-1" then local item = {}; item.name = tag.attr.name; item.subscription = assert(substatus_mapping[tag.attr.substatus], "invalid substatus"); item.ask = tag.attr.askstatus == "0" and "subscribe" or nil; local groups = {}; for _,tag in ipairs(tag) do if tag.name == "Group" then groups[tag:get_text()] = true; end end item.groups = groups; roster[jid] = item; end if tag.attr.recvstatus == "1" then pending[jid] = true; end end end if next(pending) then roster[false] = { pending = pending }; end end end assert(username and password, "No username or password"); local ret, err = dm.store(username, host, "accounts", {password = password}); print("["..(err or "success").."] stored account: "..username.."@"..host.." = "..password); if roster then local ret, err = dm.store(username, host, "roster", roster); print("["..(err or "success").."] stored roster: "..username.."@"..host.." = "..password); end end end prosody-13.0.1/tools/PaxHeaders/tb2err0000644000000000000000000000011714773555365014663 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.811711412 prosody-13.0.1/tools/tb2err0000755000175000017500000000127714773555365017074 0ustar00prosodyprosody00000000000000#!/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+: .*)") if src then src = src:gsub("\\", path_sep); local cut = src:match("/()core/") 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]*$") 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 end prosody-13.0.1/tools/PaxHeaders/test_mutants.sh.lua0000644000000000000000000000011714773555365017406 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.811711412 prosody-13.0.1/tools/test_mutants.sh.lua0000755000175000017500000001264614773555365021621 0ustar00prosodyprosody00000000000000#!/bin/bash POLYGLOT=1--[===[ set -o pipefail if [[ "$#" == "0" ]]; then echo "Lua mutation testing tool" echo echo "Usage:" echo " $BASH_SOURCE MODULE_NAME SPEC_FILE" echo echo "Requires 'lua', 'ltokenp' and 'busted' in PATH" exit 1; fi MOD_NAME="$1" MOD_FILE="$(lua "$BASH_SOURCE" resolve "$MOD_NAME")" if [[ "$MOD_FILE" == "" || ! -f "$MOD_FILE" ]]; then echo "EE: Failed to locate module '$MOD_NAME' ($MOD_FILE)"; exit 1; fi SPEC_FILE="$2" if [[ "$SPEC_FILE" == "" ]]; then SPEC_FILE="spec/${MOD_NAME/./_}_spec.lua" fi if [[ "$SPEC_FILE" == "" || ! -f "$SPEC_FILE" ]]; then echo "EE: Failed to find test spec file ($SPEC_FILE)" exit 1; fi if ! busted --helper=loader "$SPEC_FILE"; then echo "EE: Tests fail on original source. Fix it"\!; exit 1; fi export MUTANT_N=0 LIVING_MUTANTS=0 FILE_PREFIX="${MOD_FILE%.*}.mutant-" FILE_SUFFIX=".${MOD_FILE##*.}" gen_mutant () { echo "Generating mutant $2 to $3..." ltokenp -s "$BASH_SOURCE" "$1" > "$3" return "$?" } # $1 = MOD_NAME, $2 = MUTANT_N, $3 = SPEC_FILE test_mutant () { ( ulimit -m 131072 # 128MB ulimit -t 16 # 16s ulimit -f 32768 # 128MB (?) exec busted --helper="$BASH_SOURCE" -Xhelper mutate="$1":"$2" "$3" ) >/dev/null return "$?"; } MUTANT_FILE="${FILE_PREFIX}${MUTANT_N}${FILE_SUFFIX}" gen_mutant "$MOD_FILE" "$MUTANT_N" "$MUTANT_FILE" while [[ "$?" == "0" ]]; do if ! test_mutant "$MOD_NAME" "$MUTANT_N" "$SPEC_FILE"; then echo "Tests successfully killed mutant $MUTANT_N"; rm "$MUTANT_FILE"; else echo "Mutant $MUTANT_N lives on"\! LIVING_MUTANTS=$((LIVING_MUTANTS+1)) fi MUTANT_N=$((MUTANT_N+1)) MUTANT_FILE="${FILE_PREFIX}${MUTANT_N}${FILE_SUFFIX}" gen_mutant "$MOD_FILE" "$MUTANT_N" "$MUTANT_FILE" done if [[ "$?" != "2" ]]; then echo "Failed: $?" exit "$?"; fi MUTANT_SCORE="$(lua -e "print(('%0.2f'):format((1-($LIVING_MUTANTS/$MUTANT_N))*100))")" if test -f mutant-scores.txt; then echo "$MOD_NAME $MUTANT_SCORE" >> mutant-scores.txt fi echo "$MOD_NAME: All $MUTANT_N mutants generated, $LIVING_MUTANTS survived (score: $MUTANT_SCORE%)" rm "$MUTANT_FILE"; # Last file is always unmodified exit 0; ]===] -- busted helper that runs mutations if arg then if arg[1] == "resolve" then local filename = package.searchpath(assert(arg[2], "no module name given"), package.path); if filename then print(filename); end os.exit(filename and 0 or 1); end local mutants = {}; for i = 1, #arg do local opt = arg[i]; print("LOAD", i, opt) local module_name, mutant_n = opt:match("^mutate=([^:]+):(%d+)"); if module_name then mutants[module_name] = tonumber(mutant_n); end end local orig_lua_searcher = package.searchers[2]; local function mutant_searcher(module_name) local mutant_n = mutants[module_name]; if not mutant_n then return orig_lua_searcher(module_name); end local base_file, err = package.searchpath(module_name, package.path); if not base_file then return base_file, err; end local mutant_file = base_file:gsub("%.lua$", (".mutant-%d.lua"):format(mutant_n)); return loadfile(mutant_file), mutant_file; end if next(mutants) then table.insert(package.searchers, 1, mutant_searcher); end end -- filter for ltokenp to mutate scripts do local last_output = {}; local function emit(...) last_output = {...}; io.write(...) io.write(" ") return true; end local did_mutate = false; local count = -1; local threshold = tonumber(os.getenv("MUTANT_N")) or 0; local function should_mutate() count = count + 1; return count == threshold; end local function mutate(name, value) if name == "if" then -- Bypass conditionals if should_mutate() then return emit("if true or"); elseif should_mutate() then return emit("if false and"); end elseif name == "" then -- Introduce off-by-one errors if should_mutate() then return emit(("%d"):format(tonumber(value)+1)); elseif should_mutate() then return emit(("%d"):format(tonumber(value)-1)); end elseif name == "and" then if should_mutate() then return emit("or"); end elseif name == "or" then if should_mutate() then return emit("and"); end end end local current_line_n, current_line_input, current_line_output = 0, {}, {}; function FILTER(line_n,token,name,value) if current_line_n ~= line_n then -- Finished a line, moving to the next? if did_mutate and did_mutate.line == current_line_n then -- The line we finished was mutated. Store the original and modified outputs. did_mutate.line_original_src = table.concat(current_line_input, " "); did_mutate.line_modified_src = table.concat(current_line_output, " "); end current_line_input = {}; current_line_output = {}; end current_line_n = line_n; if name == "" then return; end if name == "" then if not did_mutate then return os.exit(2); else emit(("\n-- Mutated line %d (changed '%s' to '%s'):\n"):format(did_mutate.line, did_mutate.original, did_mutate.modified)) emit( ("-- Original: %s\n"):format(did_mutate.line_original_src)) emit( ("-- Modified: %s\n"):format(did_mutate.line_modified_src)); return; end end if name == "" then value = string.format("%q",value); end if mutate(name, value) then did_mutate = { original = value; modified = table.concat(last_output); line = line_n; }; else emit(value); end table.insert(current_line_input, value); table.insert(current_line_output, table.concat(last_output)); end end prosody-13.0.1/tools/PaxHeaders/xepchanges.sh0000644000000000000000000000011714773555365016221 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.815711427 prosody-13.0.1/tools/xepchanges.sh0000755000175000017500000000072614773555365020430 0ustar00prosodyprosody00000000000000#!/bin/sh -eu wget -N https://xmpp.org/extensions/xeplist.xml xml2 xepinfos.csv xml2 < doc/doap.xml | 2csv -d ' ' xmpp:SupportedXep @rdf:resource xmpp:version | sed -r 's/https?:\/\/xmpp\.org\/extensions\/xep-0*([1-9][0-9]*)\.html/\1/' | while read -r xep ver ; do grep "^$xep," xepinfos.csv | awk -F, "\$2 != \"$ver\" { print (\"XEP-\"\$1\" updated to \"\$2\" from $ver\") }" done prosody-13.0.1/PaxHeaders/util0000644000000000000000000000013114773555365013274 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.827711475 30 ctime=1743706869.907711794 prosody-13.0.1/util/0000755000175000017500000000000014773555365015554 5ustar00prosodyprosody00000000000000prosody-13.0.1/util/PaxHeaders/adhoc.lua0000644000000000000000000000011714773555365015136 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.827711475 prosody-13.0.1/util/adhoc.lua0000644000175000017500000000241314773555365017335 0ustar00prosodyprosody00000000000000-- luacheck: ignore 212/self local function new_simple_form(form, result_handler) return function(self, data, state) if state or data.form then if data.action == "cancel" then return { status = "canceled" }; end local fields, err = form:data(data.form); return result_handler(fields, err, data); else return { status = "executing", actions = {"next", "complete", default = "complete"}, form = form }, "executing"; end end end local function new_initial_data_form(form, initial_data, result_handler) return function(self, data, state) if state or data.form then if data.action == "cancel" then return { status = "canceled" }; end local fields, err = form:data(data.form); return result_handler(fields, err, data); else local values, err = initial_data(data); if type(err) == "table" then return {status = "error"; error = err} elseif type(err) == "string" then return {status = "error"; error = {type = "cancel"; condition = "internal-server-error", err}} end return { status = "executing", actions = {"next", "complete", default = "complete"}, form = { layout = form, values = values } }, "executing"; end end end return { new_simple_form = new_simple_form, new_initial_data_form = new_initial_data_form }; prosody-13.0.1/util/PaxHeaders/adminstream.lua0000644000000000000000000000011714773555365016364 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.827711475 prosody-13.0.1/util/adminstream.lua0000644000175000017500000002354214773555365020571 0ustar00prosodyprosody00000000000000local 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 "prosody.util.logger".init("adminstream"); local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; local stream_callbacks = { default_ns = "xmpp:prosody.im/admin" }; function stream_callbacks.streamopened(session, attr) -- run _streamopened in async context session.thread:run({ stream = "opened", attr = attr }); end function stream_callbacks._streamopened(session, attr) --luacheck: ignore 212/attr if session.type ~= "client" then session:open_stream(); end session.notopen = nil; end function stream_callbacks.streamclosed(session, attr) -- run _streamclosed in async context session.thread:run({ stream = "closed", attr = attr }); end function stream_callbacks._streamclosed(session) session.log("debug", "Received "); session:close(false); 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}"))); session:close("invalid-namespace"); elseif error == "parse-error" then session.log("debug", "Client XML parse error: %s", data); session:close("not-well-formed"); elseif error == "stream-error" then local condition, text = "undefined-condition"; for child in data:childtags(nil, xmlns_xmpp_streams) do if child.name ~= "text" then condition = child.name; else text = child:get_text(); end if condition ~= "undefined-condition" and text then break; end end text = condition .. (text and (" ("..text..")") or ""); session.log("info", "Session closed by remote with error: %s", text); session:close(nil, text); end end function stream_callbacks.handlestanza(session, stanza) session.thread:run(stanza); end local runner_callbacks = {}; function runner_callbacks:error(err) self.data.log("error", "Traceback[c2s]: %s", err); end local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; local function destroy_session(session, reason) if session.destroyed then return; end session.destroyed = true; session.log("debug", "Destroying session: %s", reason or "unknown reason"); end local function session_close(session, reason) local log = session.log or log; local conn = session.conn; if conn then if session.notopen then session:open_stream(); end if reason then -- nil == no err, initiated by us, false == initiated by client local stream_error = st.stanza("stream:error"); if type(reason) == "string" then -- assume stream error stream_error:tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' }); elseif type(reason) == "table" then if reason.condition then stream_error:tag(reason.condition, stream_xmlns_attr):up(); if reason.text then stream_error:tag("text", stream_xmlns_attr):text(reason.text):up(); end if reason.extra then stream_error:add_child(reason.extra); end elseif reason.name then -- a stanza stream_error = reason; end end stream_error = tostring(stream_error); log("debug", "Disconnecting client, is: %s", stream_error); session.send(stream_error); end session.send(""); function session.send() return false; end local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason; session.log("debug", "c2s stream for %s closed: %s", session.full_jid or session.ip or "", reason_text or "session closed"); -- Authenticated incoming stream may still be sending us stanzas, so wait for from remote if reason_text == nil and not session.notopen and session.type == "c2s" then -- Grace time to process data from authenticated cleanly-closed stream add_task(stream_close_timeout, function () if not session.destroyed then session.log("warn", "Failed to receive a stream close response, closing connection anyway..."); destroy_session(session); conn:close(); end end); else destroy_session(session, reason_text); conn:close(); end else local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason; destroy_session(session, reason_text); end end --- Public methods local function new_connection(socket_path, listeners) local have_unix, unix = pcall(require, "socket.unix"); if have_unix and type(unix) == "function" then -- COMPAT #1717 -- Before the introduction of datagram support, only the stream socket -- 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 accommodate both variants. unix = { stream = unix }; end if type(unix) ~= "table" then have_unix = false; end local conn, sock; return { connect = function () if not have_unix then return nil, "no unix socket support"; end if sock or conn then return nil, "already connected"; end sock = unix.stream(); sock:settimeout(0); local ok, err = sock:connect(socket_path); if not ok then return nil, err; end conn = server.wrapclient(sock, nil, nil, listeners, "*a"); return true; end; disconnect = function () if conn then conn:close(); conn = nil; end if sock then sock:close(); sock = nil; end return true; end; }; end local function new_server(sessions, stanza_handler) local s = { events = events.new(); listeners = {}; }; function s.listeners.onconnect(conn) log("debug", "New connection"); local session = sessionlib.new("admin"); sessionlib.set_id(session); sessionlib.set_logger(session); sessionlib.set_conn(session, conn); session.conntime = gettime(); session.type = "admin"; local stream = new_xmpp_stream(session, stream_callbacks); session.stream = stream; session.notopen = true; session.thread = runner(function (stanza) if st.is_stanza(stanza) then stanza_handler(session, stanza); elseif stanza.stream == "opened" then stream_callbacks._streamopened(session, stanza.attr); elseif stanza.stream == "closed" then stream_callbacks._streamclosed(session, stanza.attr); end end, runner_callbacks, session); function session.data(data) -- Parse the data, which will store stanzas in session.pending_stanzas if data then local ok, err = stream:feed(data); if not ok then session.log("debug", "Received invalid XML (%s) %d bytes: %q", err, #data, data:sub(1, 300)); session:close("not-well-formed"); end end end session.close = session_close; session.send = function (t) session.log("debug", "Sending[%s]: %s", session.type, t.top_tag and t:top_tag() or t:match("^[^>]*>?")); return session.rawsend(tostring(t)); end function session.rawsend(t) local ret, err = conn:write(t); if not ret then session.log("debug", "Error writing to connection: %s", err); return false, err; end return true; end sessions[conn] = session; end function s.listeners.onincoming(conn, data) local session = sessions[conn]; if session then session.data(data); end end function s.listeners.ondisconnect(conn, err) local session = sessions[conn]; if session then session.log("info", "Admin client disconnected: %s", err or "connection closed"); session.conn = nil; sessions[conn] = nil; s.events.fire_event("disconnected", { session = session }); end end function s.listeners.onreadtimeout(conn) return conn:send(" "); end return s; end local function new_client() local client = { type = "client"; events = events.new(); log = log; }; local listeners = {}; function listeners.onconnect(conn) log("debug", "Connected"); client.conn = conn; local stream = new_xmpp_stream(client, stream_callbacks); client.stream = stream; client.notopen = true; client.thread = runner(function (stanza) if st.is_stanza(stanza) then if not client.events.fire_event("received", stanza) and not stanza.attr.xmlns then client.events.fire_event("received/"..stanza.name, stanza); end elseif stanza.stream == "opened" then stream_callbacks._streamopened(client, stanza.attr); client.events.fire_event("connected"); elseif stanza.stream == "closed" then client.events.fire_event("disconnected"); stream_callbacks._streamclosed(client, stanza.attr); end end, runner_callbacks, client); client.close = session_close; function client.send(t) client.log("debug", "Sending: %s", t.top_tag and t:top_tag() or t:match("^[^>]*>?")); return client.rawsend(tostring(t)); end function client.rawsend(t) local ret, err = conn:write(t); if not ret then client.log("debug", "Error writing to connection: %s", err); return false, err; end return true; end client.log("debug", "Opening stream..."); client:open_stream(); end function listeners.onincoming(conn, data) --luacheck: ignore 212/conn local ok, err = client.stream:feed(data); if not ok then client.log("debug", "Received invalid XML (%s) %d bytes: %q", err, #data, data:sub(1, 300)); client:close("not-well-formed"); end end function listeners.ondisconnect(conn, err) --luacheck: ignore 212/conn client.log("info", "Admin client disconnected: %s", err or "connection closed"); client.conn = nil; client.events.fire_event("disconnected"); end function listeners.onreadtimeout(conn) conn:send(" "); end client.listeners = listeners; return client; end return { connection = new_connection; server = new_server; client = new_client; }; prosody-13.0.1/util/PaxHeaders/argparse.lua0000644000000000000000000000011714773555365015664 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.831711491 prosody-13.0.1/util/argparse.lua0000644000175000017500000000417114773555365020066 0ustar00prosodyprosody00000000000000local 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 kv_params = config and config.kv_params or {}; local strict = config and config.strict; local stop_on_positional = not config or config.stop_on_positional ~= false; local parsed_opts = {}; if #arg == 0 then return parsed_opts; end while true do local raw_param = arg[1]; if not raw_param then break; end local prefix = raw_param:match("^%-%-?"); if not prefix and stop_on_positional then break; elseif prefix == "--" and raw_param == "--" then table.remove(arg, 1); break; end if prefix then local param = table.remove(arg, 1):sub(#prefix+1); if #param == 1 and short_params then param = short_params[param]; end if not param then return nil, "param-not-found", raw_param; end local uparam = param:match("^[^=]*"):gsub("%-", "_"); local param_k, param_v; if value_params[uparam] or array_params[uparam] then param_k = uparam; param_v = param:match("^=(.*)$", #uparam+1); if not param_v then param_v = table.remove(arg, 1); if not param_v then return nil, "missing-value", raw_param; end end else param_k, param_v = param:match("^([^=]+)=(.+)$"); if not param_k then if param:match("^no%-") then param_k, param_v = param:sub(4), false; else param_k, param_v = param, true; end end param_k = param_k:gsub("%-", "_"); if strict and not kv_params[param_k] then return nil, "param-not-found", raw_param; end end if array_params[uparam] 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 elseif not stop_on_positional then table.insert(parsed_opts, table.remove(arg, 1)); end end if stop_on_positional then for i = 1, #arg do parsed_opts[i] = arg[i]; end end return parsed_opts; end return { parse = parse; } prosody-13.0.1/util/PaxHeaders/array.lua0000644000000000000000000000011714773555365015176 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.831711491 prosody-13.0.1/util/array.lua0000644000175000017500000001215314773555365017377 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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; local math_random = math.random; local math_floor = math.floor; local pairs, ipairs = pairs, ipairs; local tostring = tostring; local type = type; local array = {}; local array_base = {}; local array_methods = {}; local array_mt = { __index = array_methods; __name = "array"; __tostring = function (self) return "["..self:concat(", ").."]"; end; }; function array_mt:__freeze() return self; end local function new_array(self, t, _s, _var) if type(t) == "function" then -- Assume iterator t = self.collect(t, _s, _var); end return setmetatable(t or {}, array_mt); end function array.new(t) return setmetatable(t or {}, array_mt); end function array_mt.__add(a1, a2) local res = new_array(); return res:append(a1):append(a2); end function array_mt.__eq(a, b) if getmetatable(a) ~= array_mt or getmetatable(b) ~= array_mt then -- Lua 5.3+ calls this if both operands are tables, even if metatables differ return false; end if #a == #b then for i = 1, #a do if a[i] ~= b[i] then return false; end end else return false; end return true; end function array_mt.__div(a1, func) local a2 = new_array(); local o = 0; for i = 1, #a1 do local new_value = func(a1[i]); if new_value ~= nil then o = o + 1; a2[o] = new_value; end end return a2; end setmetatable(array, { __call = new_array }); -- Read-only methods function array_methods:random() return self[math_random(1, #self)]; end -- Return a random value excluding the one at idx function array_methods:random_other(idx) local max = #self; return self[((math.random(1, max-1)+(idx-1))%max)+1]; end -- These methods can be called two ways: -- array.method(existing_array, [params [, ...]]) -- Create new array for result -- existing_array:method([params, ...]) -- Transform existing array into result -- function array_base.map(outa, ina, func) for k, v in ipairs(ina) do outa[k] = func(v); end return outa; end function array_base.filter(outa, ina, func) local inplace, start_length = ina == outa, #ina; local write = 1; for read = 1, start_length do local v = ina[read]; if func(v) then outa[write] = v; write = write + 1; end end if inplace and write <= start_length then for i = write, start_length do outa[i] = nil; end end return outa; end function array_base.slice(outa, ina, i, j) if j == nil then j = -1; end if j < 0 then j = #ina + (j+1); end if i < 0 then i = #ina + (i+1); end if i < 1 then i = 1; end if j > #ina then j = #ina; end if i > j then for idx = 1, #outa do outa[idx] = nil; end return outa; end t_move(ina, i, j, 1, outa); if ina == outa then -- Clear (nil) remainder of range t_move(ina, #outa+1, #outa*2, 2+j-i, ina); end return outa; end function array_base.sort(outa, ina, ...) if ina ~= outa then outa:append(ina); end t_sort(outa, ...); return outa; end function array_base.unique(outa, ina) local seen = {}; return array_base.filter(outa, ina, function (item) if seen[item] then return false; else seen[item] = true; return true; end end); end function array_base.pluck(outa, ina, key, default) for i = 1, #ina do local v = ina[i][key]; if v == nil then v = default; end outa[i] = v; end return outa; end function array_base.reverse(outa, ina) local len = #ina; if ina == outa then local middle = math_floor(len/2); len = len + 1; local o; -- opposite for i = 1, middle do o = len - i; outa[i], outa[o] = outa[o], outa[i]; end else local off = len + 1; for i = 1, len do outa[i] = ina[off - i]; end end return outa; end --- These methods only mutate the array function array_methods:shuffle() local len = #self; for i = 1, #self do local r = math_random(i, len); self[i], self[r] = self[r], self[i]; end return self; end function array_methods:append(ina) t_move(ina, 1, #ina, #self+1, self); return self; end function array_methods:push(x) t_insert(self, x); return self; end array_methods.pop = t_remove; function array_methods:concat(sep) return t_concat(array.map(self, tostring), sep); end function array_methods:length() return #self; end --- These methods always create a new array function array.collect(f, s, var) local t = {}; while true do var = f(s, var); if var == nil then break; end t_insert(t, var); end return setmetatable(t, array_mt); end --- -- Setup methods from array_base for method, f in pairs(array_base) do local base_method = f; -- Setup global array method which makes new array array[method] = function (old_a, ...) local a = new_array(); return base_method(a, old_a, ...); end -- Setup per-array (mutating) method array_methods[method] = function (self, ...) return base_method(self, self, ...); end end return array; prosody-13.0.1/util/PaxHeaders/async.lua0000644000000000000000000000011714773555365015175 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.831711491 prosody-13.0.1/util/async.lua0000644000175000017500000002176314773555365017405 0ustar00prosodyprosody00000000000000local logger = require "prosody.util.logger"; local log = logger.init("util.async"); 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(); if not thread or main then error("Not running in an async context, see https://prosody.im/doc/developers/util/async"); end return thread; end -- Configurable functions local schedule_task = nil; -- schedule_task(seconds, callback) local next_tick = function (f) f(); end local function runner_from_thread(thread) local level = 0; -- Find the 'level' of the top-most function (0 == current level, 1 == caller, ...) while debug.getinfo(thread, level, "") do level = level + 1; end local name, runner = debug.getlocal(thread, level-1, 1); if name ~= "self" or type(runner) ~= "table" or runner.thread ~= thread then return nil; end return runner; end local function call_watcher(runner, watcher_name, ...) local watcher = runner.watchers[watcher_name]; if not watcher then return false; end runner:log("debug", "Calling '%s' watcher", watcher_name); local ok, err = xpcall(watcher, debug.traceback, runner, ...); if not ok then runner:log("error", "Error in '%s' watcher: %s", watcher_name, err); return nil, err; end return true; 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 (%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); if not ok then local err = state; -- Running the coroutine failed, which means we have to find the runner manually, -- in order to inform the error handler runner = runner_from_thread(thread); if not runner then log("error", "unexpected async state: unable to locate runner during error handling"); return false; end call_watcher(runner, "error", debug.traceback(thread, err)); runner.state = "ready"; return runner:run(); elseif state == "ready" then -- If state is 'ready', it is our responsibility to update runner.state from 'waiting'. -- We also have to :run(), because the queue might have further items that will not be -- processed otherwise. FIXME: It's probably best to do this in a nexttick (0 timer). next_tick(function () runner.state = "ready"; runner:run(); end); end return true; end local function waiter(num, allow_many) local thread = checkthread(); num = num or 1; local waiting; return function () if num == 0 then return; end -- already done waiting = true; coroutine.yield("wait"); end, function () num = num - 1; if num == 0 and waiting then runner_continue(thread); elseif not allow_many and num < 0 then error("done() called too many times"); end end; end local function guarder() local guards = {}; local default_id = {}; return function (id, func) id = id or default_id; local thread = checkthread(); local guard = guards[id]; if not guard then guard = {}; guards[id] = guard; log("debug", "New guard!"); else table.insert(guard, thread); log("debug", "Guarded. %d threads waiting.", #guard) coroutine.yield("wait"); end local function exit() local next_waiting = table.remove(guard, 1); if next_waiting then log("debug", "guard: Executing next waiting thread (%d left)", #guard) runner_continue(next_waiting); else log("debug", "Guard off duty.") guards[id] = nil; end end if func then func(); exit(); return; end return exit; end; end local function sleep(seconds) if not schedule_task then error("async.sleep() is not available - configure schedule function"); end local wait, done = waiter(); schedule_task(seconds, done); wait(); 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 func(coroutine.yield("ready", self)); end end); debug.sethook(thread, debug.gethook()); assert(coroutine.resume(thread, self)); -- Start it up, it will return instantly to wait for the first input return thread; end local function default_error_watcher(runner, err) runner:log("error", "Encountered error: %s", err); error(err); end local function default_func(f) f(); end local function runner(func, watchers, data) local id = new_id(); local _log = logger.init("runner" .. id); return setmetatable({ func = func or default_func, thread = false, state = "ready", notified_state = "ready", queue = {}, watchers = watchers or { error = default_error_watcher }, data = data, id = id, _log = _log; } , runner_mt); end -- Add a task item for the runner to process function runner_mt:run(input) if input ~= nil then table.insert(self.queue, input); --self:log("debug", "queued new work item, %d items queued", #self.queue); end if self.state ~= "ready" then -- The runner is busy. Indicate that the task item has been -- queued, and return information about the current runner state return true, self.state, #self.queue; end local q, thread = self.queue, self.thread; if not thread or coroutine.status(thread) == "dead" then --luacheck: ignore 143/coroutine if thread and coroutine.close then coroutine.close(thread); end self:log("debug", "creating new coroutine"); -- Create a new coroutine for this runner thread = runner_create_thread(self.func, self); self.thread = thread; end -- Process task item(s) while the queue is not empty, and we're not blocked local n, state, err = #q, self.state, nil; self.state = "running"; --self:log("debug", "running main loop"); while n > 0 and state == "ready" and not err do local consumed; -- 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 consumed, state, err = i, "ready", debug.traceback(thread, new_state); self.thread = nil; break; elseif new_state == "wait" then -- Runner is blocked on waiting for a task item to complete consumed, state = i, "waiting"; break; end end -- Loop ended - either queue empty because all tasks passed without blocking (consumed == nil) -- or runner is blocked/errored, and consumed will contain the number of tasks processed so far if not consumed then consumed = n; end -- Remove consumed items from the queue array if q[n+1] ~= nil then n = #q; end for i = 1, n do q[i] = q[consumed+i]; end n = #q; 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 [%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 self.notified_state = state; end local handler = self.watchers[state]; if handler then handler(self, err); end end if n > 0 then return self:run(); end waiting_runners[self] = state == "waiting" and time_now() or nil; return true, state, n; end -- Add a task item to the queue without invoking the runner, even if it is idle function runner_mt:enqueue(input) table.insert(self.queue, input); self:log("debug", "queued new work item, %d items queued", #self.queue); return self; end function runner_mt:log(level, fmt, ...) return self._log(level, fmt, ...); end function runner_mt:onready(f) self.watchers.ready = f; return self; end function runner_mt:onwaiting(f) self.watchers.waiting = f; return self; end function runner_mt:onerror(f) self.watchers.error = f; return self; end local function ready() return pcall(checkthread); end local function wait_for(promise) local async_wait, async_done = waiter(); local ret, err = nil, nil; promise:next( function (r) ret = r; end, function (e) err = e; end) :finally(async_done); async_wait(); if ret then return ret; else return nil, err; end end return { ready = ready; waiter = waiter; guarder = guarder; runner = runner; wait = wait_for; -- COMPAT w/trunk pre-0.12 wait_for = wait_for; sleep = sleep; 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; }; prosody-13.0.1/util/PaxHeaders/bit53.lua0000644000000000000000000000011714773555365015006 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.831711491 prosody-13.0.1/util/bit53.lua0000644000175000017500000000132614773555365017207 0ustar00prosodyprosody00000000000000-- Only the operators needed by net.websocket.frames are provided at this point return { band = function (a, b, ...) local ret = a & b; if ... then for i = 1, select("#", ...) do ret = ret & (select(i, ...)); end end return ret; end; bor = function (a, b, ...) local ret = a | b; if ... then for i = 1, select("#", ...) do ret = ret | (select(i, ...)); end end return ret; end; bxor = function (a, b, ...) local ret = a ~ b; if ... then for i = 1, select("#", ...) do ret = ret ~ (select(i, ...)); end 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; }; prosody-13.0.1/util/PaxHeaders/bitcompat.lua0000644000000000000000000000011714773555365016042 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.835711507 prosody-13.0.1/util/bitcompat.lua0000644000175000017500000000065014773555365020242 0ustar00prosodyprosody00000000000000-- Compatibility layer for bitwise operations -- First try the bit32 lib -- Lua 5.3 has it with compat enabled -- Lua 5.2 has it by default if rawget(_G, "bit32") then return _G.bit32; end do -- Lua 5.3 and 5.4 would be able to use native infix operators local ok, bitop = pcall(require, "prosody.util.bit53") if ok then return bitop; end end error "No bit module found. See https://prosody.im/doc/depends#bitop"; prosody-13.0.1/util/PaxHeaders/cache.lua0000644000000000000000000000011714773555365015123 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.835711507 prosody-13.0.1/util/cache.lua0000644000175000017500000000746214773555365017333 0ustar00prosodyprosody00000000000000 local function _remove(list, m) if m.prev then m.prev.next = m.next; end if m.next then m.next.prev = m.prev; end if list._tail == m then list._tail = m.prev; end if list._head == m then list._head = m.next; end list._count = list._count - 1; end local function _insert(list, m) if list._head then list._head.prev = m; end m.prev, m.next = nil, list._head; list._head = m; if not list._tail then list._tail = m; end list._count = list._count + 1; end local cache_methods = {}; local cache_mt = { __name = "cache", __index = cache_methods }; function cache_methods:set(k, v) local m = self._data[k]; if m then -- Key already exists if v ~= nil then -- Bump to head of list _remove(self, m); _insert(self, m); m.value = v; else -- Remove from list _remove(self, m); self._data[k] = nil; end return true; end -- New key if v == nil then return true; end -- Check whether we need to remove oldest 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; 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 end m = { key = k, value = v, prev = nil, next = nil }; self._data[k] = m; _insert(self, m); return true; end function cache_methods:get(k) local m = self._data[k]; if m then return m.value; end return nil; end function cache_methods:items() local m = self._head; return function () if not m then return; end local k, v = m.key, m.value; m = m.next; return k, v; end end function cache_methods:values() local m = self._head; return function () if not m then return; end local v = m.value; m = m.next; return v; end end function cache_methods:count() return self._count; end function cache_methods:head() local head = self._head; if not head then return nil, nil; end return head.key, head.value; end function cache_methods:tail() local tail = self._tail; if not tail then return nil, nil; end return tail.key, tail.value; end function cache_methods:resize(new_size) new_size = assert(tonumber(new_size), "cache size must be a number"); new_size = math.floor(new_size); assert(new_size > 0, "cache size must be greater than zero"); local on_evict = self._on_evict; 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, self) == false) then -- Cache is full, and we're not allowed to evict return false; end _remove(self, tail); self._data[evicted_key] = nil; end self.size = new_size; return true; end function cache_methods:table() --luacheck: ignore 212/t if not self.proxy_table then self.proxy_table = setmetatable({}, { __index = function (t, k) return self:get(k); end; __newindex = function (t, k, v) if not self:set(k, v) then error("failed to insert key into cache - full"); end end; __pairs = function (t) return self:items(); end; __len = function (t) return self:count(); end; }); end return self.proxy_table; end function cache_methods:clear() self._data = {}; self._count = 0; self._head = nil; self._tail = nil; end local function new(size, on_evict) size = assert(tonumber(size), "cache size must be a number"); size = math.floor(size); assert(size > 0, "cache size must be greater than zero"); local data = {}; return setmetatable({ _data = data, _count = 0, size = size, _head = nil, _tail = nil, _on_evict = on_evict }, cache_mt); end return { new = new; } prosody-13.0.1/util/PaxHeaders/caps.lua0000644000000000000000000000011714773555365015006 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.835711507 prosody-13.0.1/util/caps.lua0000644000175000017500000000414114773555365017205 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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; local _ENV = nil; -- luacheck: std none local function calculate_hash(disco_info) local identities, features, extensions = {}, {}, {}; for _, tag in ipairs(disco_info) do if tag.name == "identity" then t_insert(identities, (tag.attr.category or "").."\0"..(tag.attr.type or "").."\0"..(tag.attr["xml:lang"] or "").."\0"..(tag.attr.name or "")); elseif tag.name == "feature" then t_insert(features, tag.attr.var or ""); elseif tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then local form = {}; local FORM_TYPE; for _, field in ipairs(tag.tags) do if field.name == "field" and field.attr.var then local values = {}; for _, val in ipairs(field.tags) do val = #val.tags == 0 and val:get_text(); if val then t_insert(values, val); end end t_sort(values); if field.attr.var == "FORM_TYPE" then FORM_TYPE = values[1]; elseif #values > 0 then t_insert(form, field.attr.var.."\0"..t_concat(values, "<")); else t_insert(form, field.attr.var); end end end t_sort(form); form = t_concat(form, "<"); if FORM_TYPE then form = FORM_TYPE.."\0"..form; end t_insert(extensions, form); end end t_sort(identities); t_sort(features); t_sort(extensions); if #identities > 0 then identities = t_concat(identities, "<"):gsub("%z", "/").."<"; else identities = ""; end if #features > 0 then features = t_concat(features, "<").."<"; else features = ""; end if #extensions > 0 then extensions = t_concat(extensions, "<"):gsub("%z", "<").."<"; else extensions = ""; end local S = identities..features..extensions; local ver = base64(sha1(S)); return ver, S; end return { calculate_hash = calculate_hash; }; prosody-13.0.1/util/PaxHeaders/dataforms.lua0000644000000000000000000000011714773555365016040 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.835711507 prosody-13.0.1/util/dataforms.lua0000644000175000017500000002335514773555365020247 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local setmetatable = setmetatable; local ipairs = ipairs; local type, next = type, next; local tonumber = tonumber; local tostring = tostring; local t_concat = table.concat; 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 local xmlns_forms = 'jabber:x:data'; local xmlns_validate = 'http://jabber.org/protocol/xdata-validate'; local form_t = {}; local form_mt = { __index = form_t }; local function new(layout) return setmetatable(layout, form_mt); end function form_t.form(layout, data, formtype) if not formtype then formtype = "form" end local form = st.stanza("x", { xmlns = xmlns_forms, type = formtype }); if formtype == "cancel" then return form; end if formtype ~= "submit" then if layout.title then form:tag("title"):text(layout.title):up(); end if layout.instructions then form:tag("instructions"):text(layout.instructions):up(); end end for _, field in ipairs(layout) do local field_type = field.type or "text-single"; -- Add field tag form:tag("field", { type = field_type, var = field.var or field.name, label = formtype ~= "submit" and field.label or nil }); if formtype ~= "submit" then if field.desc then form:text_tag("desc", field.desc); end end if formtype == "form" and field.datatype then form:tag("validate", { xmlns = xmlns_validate, datatype = field.datatype }); if field.range_min or field.range_max then form:tag("range", { min = field.range_min and tostring(field.range_min), max = field.range_max and tostring(field.range_max), }):up(); end -- assumed form:up(); end local value = field.value; local options = field.options; if data and data[field.name] ~= nil then value = data[field.name]; if formtype == "form" and type(value) == "table" and (field_type == "list-single" or field_type == "list-multi") then -- Allow passing dynamically generated options as values options, value = value, nil; end end if formtype == "form" and options then local defaults = {}; for _, val in ipairs(options) do if type(val) == "table" then form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up(); if val.default then defaults[#defaults+1] = val.value; end else form:tag("option", { label= val }):tag("value"):text(val):up():up(); end end if not value then if field_type == "list-single" then value = defaults[1]; elseif field_type == "list-multi" then value = defaults; end end end if value ~= nil then if type(value) == "number" then if field.datatype == "xs:dateTime" then value = datetime.datetime(value); elseif field_type == "boolean" then value = value ~= 0; elseif field.datatype == "xs:double" or field.datatype == "xs:decimal" then value = ("%f"):format(value); else value = ("%d"):format(value); end end -- Add value, depending on type if field_type == "hidden" then if type(value) == "table" then -- Assume an XML snippet form:tag("value") :add_child(value) :up(); else form:tag("value"):text(value):up(); end elseif field_type == "boolean" then form:tag("value"):text((value and "1") or "0"):up(); elseif field_type == "fixed" then form:tag("value"):text(value):up(); elseif field_type == "jid-multi" then for _, jid in ipairs(value) do form:tag("value"):text(jid):up(); end elseif field_type == "jid-single" then form:tag("value"):text(value):up(); elseif field_type == "text-single" or field_type == "text-private" then form:tag("value"):text(value):up(); elseif field_type == "text-multi" then -- Split into multiple tags, one for each line for line in value:gmatch("([^\r\n]+)\r?\n*") do form:tag("value"):text(line):up(); end elseif field_type == "list-single" then form:tag("value"):text(value):up(); elseif field_type == "list-multi" then for _, val in ipairs(value) do form:tag("value"):text(val):up(); end end end local media = field.media; if media then form:tag("media", { xmlns = "urn:xmpp:media-element", height = ("%d"):format(media.height), width = ("%d"):format(media.width) }); for _, val in ipairs(media) do form:tag("uri", { type = val.type }):text(val.uri):up() end form:up(); end if formtype == "form" and field.required then form:tag("required"):up(); end -- Jump back up to list of fields form:up(); end return form; end local field_readers = {}; local data_validators = {}; function form_t.data(layout, stanza, current) local data = {}; local errors = {}; local present = {}; for _, field in ipairs(layout) do local tag; for field_tag in stanza:childtags("field") do if (field.var or field.name) == field_tag.attr.var then tag = field_tag; break; end end if not tag then if current and current[field.name] ~= nil then data[field.name] = current[field.name]; elseif field.required then errors[field.name] = "Required value missing"; end elseif field.name then present[field.name] = true; local reader = field_readers[field.type]; if reader then local value, err = reader(tag, field.required); local validator = field.datatype and data_validators[field.datatype]; if value ~= nil and validator then local valid, ret = validator(value, field); if valid then value = ret; else value, err = nil, ret or ("Invalid value for data of type " .. field.datatype); end end data[field.name], errors[field.name] = value, err; end end end if next(errors) then return data, errors, present; end return data, nil, present; end local function simple_text(field_tag, required) local data = field_tag:get_child_text("value"); -- XEP-0004 does not say if an empty string is acceptable for a required value -- so we will follow HTML5 which says that empty string means missing if required and (data == nil or data == "") then return nil, "Required value missing"; end return data; -- Return whatever get_child_text returned, even if empty string end field_readers["text-single"] = simple_text; field_readers["text-private"] = simple_text; field_readers["jid-single"] = function (field_tag, required) local raw_data, err = simple_text(field_tag, required); if not raw_data then return raw_data, err; end local data = jid_prep(raw_data); if not data then return nil, "Invalid JID: " .. raw_data; end return data; end field_readers["jid-multi"] = function (field_tag, required) local result = {}; local err = {}; for value_tag in field_tag:childtags("value") do local raw_value = value_tag:get_text(); local value = jid_prep(raw_value); result[#result+1] = value; if raw_value and not value then err[#err+1] = ("Invalid JID: " .. raw_value); end end if #result > 0 then return result, (#err > 0 and t_concat(err, "\n") or nil); elseif required then return nil, "Required value missing"; end end field_readers["list-multi"] = function (field_tag, required) local result = {}; for value in field_tag:childtags("value") do result[#result+1] = value:get_text(); end if #result > 0 then return result; elseif required then return nil, "Required value missing"; end end field_readers["text-multi"] = function (field_tag, required) local data, err = field_readers["list-multi"](field_tag, required); if data then data = t_concat(data, "\n"); end return data, err; end field_readers["list-single"] = simple_text; local boolean_values = { ["1"] = true, ["true"] = true, ["0"] = false, ["false"] = false, }; field_readers["boolean"] = function (field_tag, required) local raw_value, err = simple_text(field_tag, required); if not raw_value then return raw_value, err; end local value = boolean_values[raw_value]; if value == nil then return nil, "Invalid boolean representation:" .. raw_value; end return value; end field_readers["hidden"] = function (field_tag) return field_tag:get_child_text("value"); end data_validators["xs:integer"] = function (data, field) local n = tonumber(data); if not n then return false, "not a number"; elseif n % 1 ~= 0 then return false, "not an integer"; end if field.range_max and n > field.range_max then return false, "out of bounds"; elseif field.range_min and n < field.range_min then return false, "out of bounds"; end return true, n; end data_validators["pubsub:integer-or-max"] = function (data, field) if data == "max" then return true, data; else return data_validators["xs:integer"](data, field); end end data_validators["xs:dateTime"] = function(data, field) -- luacheck: ignore 212/field local n = datetime.parse(data); if not n then return false, "invalid timestamp"; end return true, n; end local function get_form_type(form) if not st.is_stanza(form) then return nil, "not a stanza object"; elseif form.attr.xmlns ~= "jabber:x:data" or form.name ~= "x" then return nil, "not a dataform element"; end for field in form:childtags("field") do if field.attr.var == "FORM_TYPE" then return field:get_child_text("value"); end end return ""; end return { new = new; get_type = get_form_type; }; --[=[ Layout: { title = "MUC Configuration", instructions = [[Use this form to configure options for this MUC room.]], { name = "FORM_TYPE", type = "hidden", required = true }; { name = "field-name", type = "field-type", required = false }; } --]=] prosody-13.0.1/util/PaxHeaders/datamanager.lua0000644000000000000000000000011714773555365016324 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.839711522 prosody-13.0.1/util/datamanager.lua0000644000175000017500000005123714773555365020533 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local string = string; local format = string.format; local setmetatable = setmetatable; local ipairs = ipairs; local char = string.char; local pcall = pcall; 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"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 "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); local _ENV = nil; -- luacheck: std none ---- utils ----- local encode, decode, store_encode; do local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber(k, 16)); return t[k]; end }); decode = function (s) return s and (s:gsub("%%(%x%x)", urlcodes)); end encode = function (s) return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end)); end -- Special encode function for store names, which historically were unencoded. -- All currently known stores use a-z and underscore, so this one preserves underscores. store_encode = function (s) return s and (s:gsub("[^_%w]", function (c) return format("%%%02x", c:byte()); end)); end end if not atomic_append then function atomic_append(f, data) local pos = f:seek(); if not f:write(data) or not f:flush() then f:seek("set", pos); f:write((" "):rep(#data)); f:flush(); return nil, "write-failed"; end return true; end end local _mkdir = {}; local function mkdir(path) path = path:gsub("/", path_separator); -- TODO as an optimization, do this during path creation rather than here if not _mkdir[path] then raw_mkdir(path); _mkdir[path] = true; end return path; end local data_path = (prosody and prosody.paths and prosody.paths.data) or "."; local callbacks = {}; ------- API ------------- local function set_data_path(path) log("debug", "Setting data path to: %s", path); data_path = path; end local function callback(username, host, datastore, data) for _, f in ipairs(callbacks) do username, host, datastore, data = f(username, host, datastore, data); if username == false then break; end end return username, host, datastore, data; end local function add_callback(func) if not callbacks[func] then -- Would you really want to set the same callback more than once? callbacks[func] = true; callbacks[#callbacks+1] = func; return true; end end local function remove_callback(func) if callbacks[func] then for i, f in ipairs(callbacks) do if f == func then callbacks[i] = nil; callbacks[f] = nil; return true; end end end end local function getpath(username, host, datastore, ext, create) ext = ext or "dat"; host = (host and encode(host)) or "_global"; username = username and encode(username); datastore = store_encode(datastore); if username then if create then mkdir(mkdir(mkdir(data_path).."/"..host).."/"..datastore); end return format("%s/%s/%s/%s.%s", data_path, host, datastore, username, ext); else if create then mkdir(mkdir(data_path).."/"..host); end return format("%s/%s/%s.%s", data_path, host, datastore, ext); end end local function load(username, host, datastore) local data, err, errno = envloadfile(getpath(username, host, datastore), {}); if not data then if errno == ENOENT then -- No such file, ok to ignore return nil; end log("error", "Failed to load %s storage ('%s') for user: %s@%s", datastore, err, username or "nil", host or "nil"); return nil, "Error reading storage"; end local success, ret = pcall(data); if not success then log("error", "Unable to load %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil"); return nil, "Error reading storage"; end return ret; end local function atomic_store(filename, data) local scratch = filename.."~"; local f, ok, msg, errno; -- luacheck: ignore errno -- TODO return util.error with code=errno? f, msg, errno = io_open(scratch, "w"); if not f then return nil, msg; end ok, msg = f:write(data); if not ok then f:close(); os_remove(scratch); return nil, msg; end ok, msg = f:close(); if not ok then os_remove(scratch); return nil, msg; end return os_rename(scratch, filename); end if prosody and prosody.platform ~= "posix" then -- os.rename does not overwrite existing files on Windows -- TODO We could use Transactional NTFS on Vista and above function atomic_store(filename, data) local f, err = io_open(filename, "w"); if not f then return f, err; end local ok, msg = f:write(data); if not ok then f:close(); return ok, msg; end return f:close(); end end local function store(username, host, datastore, data) if not data then data = {}; end username, host, datastore, data = callback(username, host, datastore, data); if username == false then return true; -- Don't save this data at all end -- save the datastore local d = "return " .. serialize(data) .. ";\n"; local mkdir_cache_cleared; repeat local ok, msg = atomic_store(getpath(username, host, datastore, nil, true), d); if not ok then if not mkdir_cache_cleared then -- We may need to recreate a removed directory _mkdir = {}; mkdir_cache_cleared = true; else log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil"); return nil, "Error saving to storage"; end end if next(data) == nil then -- try to delete empty datastore log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil"); os_remove(getpath(username, host, datastore)); end -- we write data even when we are deleting because lua doesn't have a -- platform independent way of checking for nonexisting files until ok; return true; end -- Append a blob of data to a file local function append(username, host, datastore, ext, data) if type(data) ~= "string" then return; end local filename = getpath(username, host, datastore, ext, true); local f = io_open(filename, "r+"); if not f then return atomic_store(filename, data); -- File did probably not exist, let's create it 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); if not ok then f:close(); return ok, msg, "write"; end ok, msg = f:close(); if not ok then return ok, msg, "close"; end 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 -- save the datastore data = "item(" .. serialize(data) .. ");\n"; local ok, msg, where = append(username, host, datastore, "list", data); if not ok then log("error", "Unable to write to %s storage ('%s' in %s) for user: %s@%s", 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 local function list_store(username, host, datastore, data) if not data then data = {}; end if callback(username, host, datastore) == false then return true; end -- save the datastore local d = {}; 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"); return; end if next(data) == nil then -- try to delete empty datastore log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil"); os_remove(getpath(username, host, datastore, "list")); end -- we write data even when we are deleting because lua doesn't have a -- platform independent way of checking for nonexisting files 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}); if not data then if errno == ENOENT then -- No such file, ok to ignore return nil; end log("error", "Failed to load %s storage ('%s') for user: %s@%s", datastore, err, username or "nil", host or "nil"); return nil, "Error reading storage"; end local success, ret = pcall(data); if not success then log("error", "Unable to load %s storage ('%s') for user: %s@%s", datastore, ret, username or "nil", host or "nil"); return nil, "Error reading storage"; end 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"; } local function users(host, store, typ) -- luacheck: ignore 431/store typ = "."..(type_map[typ or "keyval"] or typ); local store_dir = format("%s/%s/%s", data_path, encode(host), store_encode(store)); local mode, err = lfs.attributes(store_dir, "mode"); if not mode then return function() log("debug", "%s", err or (store_dir .. " does not exist")) end end local next, state = lfs.dir(store_dir); -- luacheck: ignore 431/next 431/state return function(state) -- luacheck: ignore 431/state for node in next, state do if node:sub(-#typ, -1) == typ then return decode(node:sub(1, -#typ-1)); end end end, state; end local function stores(username, host, typ) typ = type_map[typ or "keyval"]; local store_dir = format("%s/%s/", data_path, encode(host)); local mode, err = lfs.attributes(store_dir, "mode"); if not mode then return function() log("debug", "Could not iterate over stores in %s: %s", store_dir, err); end end local next, state = lfs.dir(store_dir); -- luacheck: ignore 431/next 431/state return function(state) -- luacheck: ignore 431/state for node in next, state do if not node:match"^%." then if username == true then if lfs.attributes(store_dir..node, "mode") == "directory" then return decode(node); end elseif username then local store_name = decode(node); if lfs.attributes(getpath(username, host, store_name, typ), "mode") then return store_name; end elseif lfs.attributes(node, "mode") == "file" then local file, ext = node:match("^(.*)%.([dalist]+)$"); if ext == typ then return decode(file) end end end end end, state; end local function do_remove(path) local ok, err = os_remove(path); if not ok and lfs.attributes(path, "mode") then return ok, err; end return true end local function purge(username, host) local host_dir = format("%s/%s/", data_path, encode(host)); local ok, iter, state, var = pcall(lfs.dir, host_dir); if not ok then return ok, iter; end local errs = {}; for file in iter, state, var do if lfs.attributes(host_dir..file, "mode") == "directory" then local store_name = decode(file); local ok, err = do_remove(getpath(username, host, store_name)); if not ok then errs[#errs+1] = err; end 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, ", "); end return { set_data_path = set_data_path; add_callback = add_callback; remove_callback = remove_callback; getpath = getpath; load = load; store = store; append_raw = append; store_raw = atomic_store; list_append = list_append; list_store = list_store; list_load = list_load; users = users; stores = stores; purge = purge; path_decode = decode; path_encode = encode; build_list_index = build_list_index; list_open = list_open; list_shift = list_shift; }; prosody-13.0.1/util/PaxHeaders/datamapper.lua0000644000000000000000000000011714773555365016176 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.839711522 prosody-13.0.1/util/datamapper.lua0000644000175000017500000002253514773555365020404 0ustar00prosodyprosody00000000000000-- This file is generated from teal-src/util/datamapper.lua 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 = {} local function toboolean(s) if s == "true" or s == "1" then return true elseif s == "false" or s == "0" then return false elseif s then return true end end local function totype(t, s) if not s then return nil end if t == "string" then return s elseif t == "boolean" then return toboolean(s) elseif t == "number" or t == "integer" then return tonumber(s) end end local value_goes = {} local function resolve_schema(schema, root) if type(schema) == "table" then if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then return pointer.resolve(root, schema["$ref"]:sub(2)) end end return schema end local function guess_schema_type(schema) local schema_types = schema.type if type(schema_types) == "string" then return schema_types elseif schema_types ~= nil then error("schema has unsupported 'type' property") elseif schema.properties then return "object" elseif schema.items then return "array" end return "string" end local function unpack_propschema(propschema, propname, current_ns) local proptype = "string" local value_where = propname and "in_text_tag" or "in_text" local name = propname local namespace local prefix local single_attribute local enums if type(propschema) == "table" then proptype = guess_schema_type(propschema); elseif type(propschema) == "string" then error("schema as string is not supported: " .. propschema .. " {" .. current_ns .. "}" .. propname) end if proptype == "object" or proptype == "array" then value_where = "in_children" end if type(propschema) == "table" then local xml = propschema.xml if xml then if xml.name then name = xml.name end if xml.namespace and xml.namespace ~= current_ns then namespace = xml.namespace end if xml.prefix then prefix = xml.prefix end if proptype == "array" and xml.wrapped then value_where = "in_wrapper" elseif xml.attribute then value_where = "in_attribute" elseif xml.text then value_where = "in_text" elseif xml.x_name_is_value then value_where = "in_tag_name" elseif xml.x_single_attribute then single_attribute = xml.x_single_attribute value_where = "in_single_attribute" end end if propschema["const"] then enums = {propschema["const"]} elseif propschema["enum"] then enums = propschema["enum"] end end return proptype, value_where, name, namespace, prefix, single_attribute, enums end local parse_object local parse_array local function extract_value(s, value_where, proptype, name, namespace, prefix, single_attribute, enums) if value_where == "in_tag_name" then local c if proptype == "boolean" then c = s:get_child(name, namespace); elseif enums and proptype == "string" then for i = 1, #enums do c = s:get_child(enums[i], namespace); if c then break end end else c = s:get_child(nil, namespace); end if c then return c.name end elseif value_where == "in_attribute" then local attr = name if prefix then attr = prefix .. ":" .. name elseif namespace and namespace ~= s.attr.xmlns then attr = namespace .. "\1" .. name end return s.attr[attr] elseif value_where == "in_text" then return s:get_text() elseif value_where == "in_single_attribute" then local c = s:get_child(name, namespace) return c and c.attr[single_attribute] elseif value_where == "in_text_tag" then return s:get_child_text(name, namespace) end end function parse_object(schema, s, root) local out = {} schema = resolve_schema(schema, root) if type(schema) == "table" and schema.properties then for prop, propschema in pairs(schema.properties) do propschema = resolve_schema(propschema, root) local proptype, value_where, name, namespace, prefix, single_attribute, enums = unpack_propschema(propschema, prop, s.attr.xmlns) if value_where == "in_children" and type(propschema) == "table" then if proptype == "object" then local c = s:get_child(name, namespace) if c then out[prop] = parse_object(propschema, c, root); end elseif proptype == "array" then local a = parse_array(propschema, s, root); if a and a[1] ~= nil then out[prop] = a; end else error("unreachable") end elseif value_where == "in_wrapper" and type(propschema) == "table" and proptype == "array" then local wrapper = s:get_child(name, namespace); if wrapper then out[prop] = parse_array(propschema, wrapper, root); end else local value = extract_value(s, value_where, proptype, name, namespace, prefix, single_attribute, enums) out[prop] = totype(proptype, value) end end end return out end function parse_array(schema, s, root) local itemschema = resolve_schema(schema.items, root); local proptype, value_where, child_name, namespace, prefix, single_attribute, enums = unpack_propschema(itemschema, nil, s.attr.xmlns) local attr_name if value_where == "in_single_attribute" then value_where = "in_attribute"; attr_name = single_attribute; end local out = {} if proptype == "object" then if type(itemschema) == "table" then for c in s:childtags(child_name, namespace) do table.insert(out, parse_object(itemschema, c, root)); end else error("array items must be schema object") end elseif proptype == "array" then if type(itemschema) == "table" then for c in s:childtags(child_name, namespace) do table.insert(out, parse_array(itemschema, c, root)); end end else for c in s:childtags(child_name, namespace) do local value = extract_value(c, value_where, proptype, attr_name or child_name, namespace, prefix, single_attribute, enums) table.insert(out, totype(proptype, value)); end end return out end local function parse(schema, s) local s_type = guess_schema_type(schema) if s_type == "object" then return parse_object(schema, s, schema) elseif s_type == "array" then return parse_array(schema, s, schema) else error("top-level scalars unsupported") end end local function toxmlstring(proptype, v) if proptype == "string" and type(v) == "string" then return v elseif proptype == "number" and type(v) == "number" then return string.format("%g", v) elseif proptype == "integer" and type(v) == "number" then return string.format("%d", v) elseif proptype == "boolean" then return v and "1" or "0" end end local unparse local function unparse_property(out, v, proptype, propschema, value_where, name, namespace, current_ns, prefix, single_attribute, root) if value_where == "in_attribute" then local attr = name if prefix then attr = prefix .. ":" .. name elseif namespace and namespace ~= current_ns then attr = namespace .. "\1" .. name end out.attr[attr] = toxmlstring(proptype, v) elseif value_where == "in_text" then out:text(toxmlstring(proptype, v)) elseif value_where == "in_single_attribute" then assert(single_attribute) local propattr = {} if namespace and namespace ~= current_ns then propattr.xmlns = namespace end propattr[single_attribute] = toxmlstring(proptype, v) out:tag(name, propattr):up(); else local propattr if namespace ~= current_ns then propattr = {xmlns = namespace} end if value_where == "in_tag_name" then if proptype == "string" and type(v) == "string" then out:tag(v, propattr):up(); elseif proptype == "boolean" and v == true then out:tag(name, propattr):up(); end elseif proptype == "object" and type(propschema) == "table" and type(v) == "table" then local c = unparse(propschema, v, name, namespace, nil, root); if c then out:add_direct_child(c); end elseif proptype == "array" and type(propschema) == "table" and type(v) == "table" then if value_where == "in_wrapper" then local c = unparse(propschema, v, name, namespace, nil, root); if c then out:add_direct_child(c); end else unparse(propschema, v, name, namespace, out, root); end else out:text_tag(name, toxmlstring(proptype, v), propattr) end end end function unparse(schema, t, current_name, current_ns, ctx, root) if root == nil then root = schema end if schema.xml then if schema.xml.name then current_name = schema.xml.name end if schema.xml.namespace then current_ns = schema.xml.namespace end end local out = ctx or st.stanza(current_name, {xmlns = current_ns}) local s_type = guess_schema_type(schema) if s_type == "object" then for prop, propschema in pairs(schema.properties) do propschema = resolve_schema(propschema, root) local v = t[prop] if v ~= nil then local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(propschema, prop, current_ns) unparse_property(out, v, proptype, propschema, value_where, name, namespace, current_ns, prefix, single_attribute, root) end end return out elseif s_type == "array" then local itemschema = resolve_schema(schema.items, root) local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns) for _, item in ipairs(t) do unparse_property(out, item, proptype, itemschema, value_where, name, namespace, current_ns, prefix, single_attribute, root) end return out end end return {parse = parse; unparse = unparse} prosody-13.0.1/util/PaxHeaders/datetime.lua0000644000000000000000000000011714773555365015654 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.839711522 prosody-13.0.1/util/datetime.lua0000644000175000017500000000370414773555365020057 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- XEP-0082: XMPP Date and Time Profiles 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 and floor(t) or nil); end local function datetime(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) 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 and floor(t) or nil); end local function parse(s) if s then 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 local tzd_offset = 0; if tzd ~= "" and tzd ~= "Z" then local sign, h, m = tzd:match("([+%-])(%d%d):?(%d*)"); if not sign then return; end if #m ~= 2 then m = "0"; end h, m = tonumber(h), tonumber(m); tzd_offset = h * 60 * 60 + m * 60; if sign == "-" then tzd_offset = -tzd_offset; end end 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 return { date = date; datetime = datetime; time = time; legacy = legacy; parse = parse; }; prosody-13.0.1/util/PaxHeaders/dbuffer.lua0000644000000000000000000000011714773555365015475 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.839711522 prosody-13.0.1/util/dbuffer.lua0000644000175000017500000001210614773555365017674 0ustar00prosodyprosody00000000000000local queue = require "prosody.util.queue"; local s_byte, s_sub = string.byte, string.sub; local dbuffer_methods = {}; local dynamic_buffer_mt = { __name = "dbuffer", __index = dbuffer_methods }; function dbuffer_methods:write(data) if self.max_size and #data + self._length > self.max_size then return nil; end local ok = self.items:push(data); if not ok then self:collapse(); ok = self.items:push(data); end if not ok then return nil; end self._length = self._length + #data; return true; end function dbuffer_methods:read_chunk(requested_bytes) local chunk, consumed = self.items:peek(), self.front_consumed; if not chunk then return; end local chunk_length = #chunk; local remaining_chunk_length = chunk_length - consumed; if not requested_bytes then requested_bytes = remaining_chunk_length; end if remaining_chunk_length <= requested_bytes then self.front_consumed = 0; self._length = self._length - remaining_chunk_length; self.items:pop(); assert(#chunk:sub(consumed + 1, -1) == remaining_chunk_length); return chunk:sub(consumed + 1, -1), remaining_chunk_length; end local end_pos = consumed + requested_bytes; self.front_consumed = end_pos; self._length = self._length - requested_bytes; assert(#chunk:sub(consumed + 1, end_pos) == requested_bytes); return chunk:sub(consumed + 1, end_pos), requested_bytes; end function dbuffer_methods:read(requested_bytes) local chunks; if requested_bytes and requested_bytes > self._length then return nil; end local chunk, read_bytes = self:read_chunk(requested_bytes); if not requested_bytes then return chunk; elseif chunk then requested_bytes = requested_bytes - read_bytes; if requested_bytes == 0 then -- Already read everything we need return chunk; end chunks = {}; else return nil; end -- Need to keep reading more chunks while chunk do table.insert(chunks, chunk); if requested_bytes > 0 then chunk, read_bytes = self:read_chunk(requested_bytes); requested_bytes = requested_bytes - read_bytes; else break; end end return table.concat(chunks); end -- Read to, and including, the specified character sequence (return nil if not found) function dbuffer_methods:read_until(char) local buffer_pos = 0; for i, chunk in self.items:items() do local start = 1 + ((i == 1) and self.front_consumed or 0); local char_pos = chunk:find(char, start, true); if char_pos then return self:read(1 + buffer_pos + char_pos - start); end buffer_pos = buffer_pos + #chunk - (start - 1); end return nil; end function dbuffer_methods:discard(requested_bytes) 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); requested_bytes = requested_bytes - read_bytes; if requested_bytes == 0 then -- Already read everything we need return true; end while chunk do if requested_bytes > 0 then chunk, read_bytes = self:read_chunk(requested_bytes); requested_bytes = requested_bytes - read_bytes; else break; end end return true; end -- Normalize i, j into absolute offsets within the -- front chunk (accounting for front_consumed), and -- ensure there is enough data in the first chunk -- to cover any subsequent :sub() or :byte() operation function dbuffer_methods:_prep_sub(i, j) if j == nil then j = -1; end if j < 0 then j = self._length + (j+1); end if i < 0 then i = self._length + (i+1); end if i < 1 then i = 1; end if j > self._length then j = self._length; end if i > j then return nil; end self:collapse(j); if self.front_consumed > 0 then i = i + self.front_consumed; j = j + self.front_consumed; end return i, j; end function dbuffer_methods:sub(i, j) i, j = self:_prep_sub(i, j); if not i then return ""; end return s_sub(self.items:peek(), i, j); end function dbuffer_methods:byte(i, j) i = i or 1; j = j or i; i, j = self:_prep_sub(i, j); if not i then return; end return s_byte(self.items:peek(), i, j); end function dbuffer_methods:length() return self._length; end dbuffer_methods.len = dbuffer_methods.length; -- strings have :len() dynamic_buffer_mt.__len = dbuffer_methods.length; -- support # operator function dbuffer_methods:collapse(bytes) bytes = bytes or self._length; local front_chunk = self.items:peek(); if not front_chunk or #front_chunk - self.front_consumed >= bytes then return; end local front_chunks = { front_chunk:sub(self.front_consumed+1) }; local front_bytes = #front_chunks[1]; while front_bytes < bytes do self.items:pop(); local chunk = self.items:peek(); front_bytes = front_bytes + #chunk; table.insert(front_chunks, chunk); end self.items:replace(table.concat(front_chunks)); self.front_consumed = 0; end local function new(max_size, max_chunks) if max_size and max_size <= 0 then return nil; end return setmetatable({ front_consumed = 0; _length = 0; max_size = max_size; items = queue.new(max_chunks or 32); }, dynamic_buffer_mt); end return { new = new; }; prosody-13.0.1/util/PaxHeaders/debug.lua0000644000000000000000000000011714773555365015146 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.839711522 prosody-13.0.1/util/debug.lua0000644000175000017500000001441414773555365017351 0ustar00prosodyprosody00000000000000-- Variables ending with these names will not -- have their values printed ('password' includes -- 'new_password', etc.) -- -- luacheck: ignore 122/debug local censored_names = { password = true; passwd = true; pass = true; pwd = true; }; local optimal_line_length = 65; local termcolours = require "prosody.util.termcolours"; local getstring = termcolours.getstring; local styles; do local _ = termcolours.getstyle; styles = { boundary_padding = _("bright"); filename = _("bright", "blue"); level_num = _("green"); funcname = _("yellow"); location = _("yellow"); }; end local function get_locals_table(thread, level) local locals = {}; for local_num = 1, math.huge do local name, value; if thread then name, value = debug.getlocal(thread, level, local_num); else name, value = debug.getlocal(level+1, local_num); end if not name then break; end table.insert(locals, { name = name, value = value }); end return locals; end local function get_upvalues_table(func) local upvalues = {}; if func then for upvalue_num = 1, math.huge do local name, value = debug.getupvalue(func, upvalue_num); if not name then break; end if name == "" then name = ("[%d]"):format(upvalue_num); end table.insert(upvalues, { name = name, value = value }); end end return upvalues; end local function string_from_var_table(var_table, max_line_len, indent_str) local var_string = {}; local col_pos = 0; max_line_len = max_line_len or math.huge; indent_str = "\n"..(indent_str or ""); for _, var in ipairs(var_table) do local name, value = var.name, var.value; if name:sub(1,1) ~= "(" then if type(value) == "string" then if censored_names[name:match("%a+$")] then value = ""; else value = ("%q"):format(value); end else value = tostring(value); end if #value > max_line_len then value = value:sub(1, max_line_len-3).."…"; end local str = ("%s = %s"):format(name, tostring(value)); col_pos = col_pos + #str; if col_pos > max_line_len then table.insert(var_string, indent_str); col_pos = 0; end table.insert(var_string, str); end end if #var_string == 0 then return nil; else return "{ "..table.concat(var_string, ", "):gsub(indent_str..", ", indent_str).." }"; end end local function get_traceback_table(thread, start_level) local levels = {}; for level = start_level, math.huge do local info; if thread then info = debug.getinfo(thread, level); else info = debug.getinfo(level+1); end if not info then break; end levels[(level-start_level)+1] = { level = level; info = info; locals = get_locals_table(thread, level+1); upvalues = get_upvalues_table(info.func); }; end return levels; end local function build_source_boundary_marker(last_source_desc) local padding = string.rep("-", math.floor(((optimal_line_length - 6) - #last_source_desc)/2)); return getstring(styles.boundary_padding, "v"..padding).." ".. getstring(styles.filename, last_source_desc).." ".. getstring(styles.boundary_padding, padding..(#last_source_desc%2==0 and "-v" or "v ")); end local function _traceback(thread, message, level) -- Lua manual says: debug.traceback ([thread,] [message [, level]]) -- I fathom this to mean one of: -- () -- (thread) -- (message, level) -- (thread, message, level) if thread == nil then -- Defaults thread, message, level = coroutine.running(), message, level; elseif type(thread) == "string" then thread, message, level = coroutine.running(), thread, message; elseif type(thread) ~= "thread" then return nil; -- debug.traceback() does this end level = level or 0; message = message and (message.."\n") or ""; -- +3 counts for this function, and the pcall() and wrapper above us, the +1... I don't know. local levels = get_traceback_table(thread, level+(thread == nil and 4 or 0)); local last_source_desc; local lines = {}; for nlevel, current_level in ipairs(levels) do local info = current_level.info; local line; local func_type = info.namewhat.." "; local source_desc = (info.short_src == "[C]" and "C code") or info.short_src or "Unknown"; if func_type == " " then func_type = ""; end; if info.short_src == "[C]" then line = "[ C ] "..func_type.."C function "..getstring(styles.location, (info.name and ("%q"):format(info.name) or "(unknown name)")); elseif info.what == "main" then line = "[Lua] "..getstring(styles.location, info.short_src.." line "..info.currentline); else local name = info.name or " "; if name ~= " " then name = ("%q"):format(name); end if func_type == "global " or func_type == "local " then func_type = func_type.."function "; end line = "[Lua] "..getstring(styles.location, info.short_src.." line ".. info.currentline).." in "..func_type..getstring(styles.funcname, name).. " (defined on line "..info.linedefined..")"; end if source_desc ~= last_source_desc then -- Venturing into a new source, add marker for previous last_source_desc = source_desc; table.insert(lines, "\t "..build_source_boundary_marker(last_source_desc)); end nlevel = nlevel-1; table.insert(lines, "\t"..(nlevel==0 and ">" or " ")..getstring(styles.level_num, "("..nlevel..") ")..line); local npadding = (" "):rep(#tostring(nlevel)); if current_level.locals then local locals_str = string_from_var_table(current_level.locals, optimal_line_length, "\t "..npadding); if locals_str then table.insert(lines, "\t "..npadding.."Locals: "..locals_str); end end local upvalues_str = string_from_var_table(current_level.upvalues, optimal_line_length, "\t "..npadding); if upvalues_str then table.insert(lines, "\t "..npadding.."Upvals: "..upvalues_str); end end -- table.insert(lines, "\t "..build_source_boundary_marker(last_source_desc)); return message.."stack traceback:\n"..table.concat(lines, "\n"); end local function traceback(...) local ok, ret = pcall(_traceback, ...); if not ok then return "Error in error handling: "..ret; end return ret; end local function use() debug.traceback = traceback; end return { get_locals_table = get_locals_table; get_upvalues_table = get_upvalues_table; string_from_var_table = string_from_var_table; get_traceback_table = get_traceback_table; traceback = traceback; use = use; }; prosody-13.0.1/util/PaxHeaders/dependencies.lua0000644000000000000000000000011714773555365016506 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.843711539 prosody-13.0.1/util/dependencies.lua0000644000175000017500000001457614773555365020722 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local function softreq(...) local ok, lib = pcall(require, ...); if ok then return lib; else return nil, lib; end end -- Required to be able to find packages installed with luarocks if not softreq "luarocks.loader" then -- LuaRocks 2.x softreq "luarocks.require"; -- LuaRocks <1.x end local function missingdep(name, sources, msg, err) -- luacheck: ignore err -- TODO print something about the underlying error, useful for debugging print(""); print("**************************"); print("Prosody was unable to find "..tostring(name)); print("This package can be obtained in the following ways:"); print(""); for _, row in ipairs(sources) do 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.")); print("More help can be found on our website, at https://prosody.im/doc/depends"); print("**************************"); print(""); end local function check_dependencies() if _VERSION < "Lua 5.2" then print "***********************************" print("Unsupported Lua version: ".._VERSION); print("At least Lua 5.2 is required."); print "***********************************" return false; end local fatal; local lxp, err = softreq "lxp" if not lxp then missingdep("luaexpat", { { "Debian/Ubuntu", "sudo apt install lua-expat" }; { "luarocks", "luarocks install luaexpat" }; { "Source", "http://matthewwild.co.uk/projects/luaexpat/" }; }, nil, err); fatal = true; end local socket, err = softreq "socket" if not socket then missingdep("luasocket", { { "Debian/Ubuntu", "sudo apt install lua-socket" }; { "luarocks", "luarocks install luasocket" }; { "Source", "http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/" }; }, nil, err); fatal = true; elseif not socket.tcp4 then -- COMPAT LuaSocket before being IP-version agnostic socket.tcp4 = socket.tcp; socket.udp4 = socket.udp; end local lfs, err = softreq "lfs" if not lfs then missingdep("luafilesystem", { { "luarocks", "luarocks install luafilesystem" }; { "Debian/Ubuntu", "sudo apt install lua-filesystem" }; { "Source", "http://www.keplerproject.org/luafilesystem/" }; }, nil, err); fatal = true; end local ssl, err = softreq "ssl" if not ssl then missingdep("LuaSec", { { "Debian/Ubuntu", "sudo apt install lua-sec" }; { "luarocks", "luarocks install luasec" }; { "Source", "https://github.com/brunoos/luasec" }; }, nil, err); end local bit, err = softreq"prosody.util.bitcompat"; if not bit then missingdep("lua-bitops", { { "Debian/Ubuntu", "sudo apt install lua-bitop" }; { "luarocks", "luarocks install luabitop" }; { "Source", "http://bitop.luajit.org/" }; }, "WebSocket support will not be available", err); end local unbound, err = softreq"lunbound"; -- luacheck: ignore 211/err if not unbound then missingdep("lua-unbound", { { "Debian/Ubuntu", "sudo apt install lua-unbound" }; { "luarocks", "luarocks install luaunbound" }; { "Source", "https://www.zash.se/luaunbound.html" }; }, "Old DNS resolver library will be used", err); else package.preload["prosody.net.adns"] = function () local ub = require "prosody.net.unbound"; return ub; end end local encodings, err = softreq "prosody.util.encodings" if not encodings then if err:match("module '[^']*' not found") then 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" }; }); else print "***********************************" print("util/encodings couldn't be loaded. Check that you have a recent version of libidn"); print "" print("The full error was:"); print(err) print "***********************************" end fatal = true; end local hashes, err = softreq "prosody.util.hashes" if not hashes then if err:match("module '[^']*' not found") then 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" }; }); else print "***********************************" print("util/hashes couldn't be loaded. Check that you have a recent version of OpenSSL (libcrypto in particular)"); print "" print("The full error was:"); print(err) print "***********************************" end fatal = true; end return not fatal; end 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 support is deprecated, upgrade as soon as possible", _VERSION); end local ssl = softreq"ssl"; if ssl then local major, minor, veryminor, patched = ssl._VERSION:match("(%d+)%.(%d+)%.?(%d*)(M?)"); if not major or ((tonumber(major) == 0 and (tonumber(minor) or 0) <= 3 and (tonumber(veryminor) or 0) <= 2) and patched ~= "M") then prosody.log("error", "This version of LuaSec contains a known bug that causes disconnects, see https://prosody.im/doc/depends"); end end local lxp = softreq"lxp"; if lxp then if not pcall(lxp.new, { StartDoctypeDecl = false }) then prosody.log("error", "The version of LuaExpat on your system leaves Prosody " .."vulnerable to denial-of-service attacks. You should upgrade to " .."LuaExpat 1.3.0 or higher as soon as possible. See " .."https://prosody.im/doc/depends#luaexpat for more information."); end if not lxp.new({}).getcurrentbytecount then prosody.log("error", "The version of LuaExpat on your system does not support " .."stanza size limits, which may leave servers on untrusted " .."networks (e.g. the internet) vulnerable to denial-of-service " .."attacks. You should upgrade to LuaExpat 1.3.0 or higher as " .."soon as possible. See " .."https://prosody.im/doc/depends#luaexpat for more information."); end end end return { softreq = softreq; missingdep = missingdep; check_dependencies = check_dependencies; log_warnings = log_warnings; }; prosody-13.0.1/util/PaxHeaders/dns.lua0000644000000000000000000000011714773555365014644 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.843711539 prosody-13.0.1/util/dns.lua0000644000175000017500000001432214773555365017045 0ustar00prosodyprosody00000000000000-- libunbound based net.adns replacement for Prosody IM -- Copyright (C) 2012-2015 Kim Alvefur -- Copyright (C) 2012 Waqas Hussain -- -- This file is MIT licensed. local setmetatable = setmetatable; local table = table; local t_concat = table.concat; local t_insert = table.insert; local s_byte = string.byte; local s_format = string.format; local s_sub = string.sub; 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 local parsers = {}; -- No support for pointers, but libunbound appears to take care of that. local function readDnsName(packet, pos) if s_byte(packet, pos) == 0 then return ".", pos+1; end local pack_len, r, len = #packet, {}; pos = pos or 1; repeat len = s_byte(packet, pos) or 0; t_insert(r, s_sub(packet, pos + 1, pos + len)); pos = pos + len + 1; until len == 0 or pos >= pack_len; return t_concat(r, "."), pos; end -- These are just simple names. parsers.CNAME = readDnsName; parsers.NS = readDnsName parsers.PTR = readDnsName; local soa_mt = { __tostring = function(rr) return s_format("%s %s %d %d %d %d %d", rr.mname, rr.rname, rr.serial, rr.refresh, rr.retry, rr.expire, rr.minimum); end; }; function parsers.SOA(packet) local mname, rname, offset; mname, offset = readDnsName(packet, 1); rname, offset = readDnsName(packet, offset); -- Extract all the bytes of these fields in one call local s1, s2, s3, s4, -- serial r1, r2, r3, r4, -- refresh t1, t2, t3, t4, -- retry e1, e2, e3, e4, -- expire m1, m2, m3, m4 -- minimum = s_byte(packet, offset, offset + 19); return setmetatable({ mname = mname; rname = rname; serial = s1*0x1000000 + s2*0x10000 + s3*0x100 + s4; refresh = r1*0x1000000 + r2*0x10000 + r3*0x100 + r4; retry = t1*0x1000000 + t2*0x10000 + t3*0x100 + t4; expire = e1*0x1000000 + e2*0x10000 + e3*0x100 + e4; minimum = m1*0x1000000 + m2*0x10000 + m3*0x100 + m4; }, soa_mt); end parsers.A = inet_ntop; parsers.AAAA = inet_ntop; local mx_mt = { __tostring = function(rr) return s_format("%d %s", rr.pref, rr.mx) end }; function parsers.MX(packet) local name = readDnsName(packet, 3); local b1,b2 = s_byte(packet, 1, 2); return setmetatable({ pref = b1*256+b2; mx = name; }, mx_mt); end local srv_mt = { __tostring = function(rr) return s_format("%d %d %d %s", rr.priority, rr.weight, rr.port, rr.target); end }; function parsers.SRV(packet) local name = readDnsName(packet, 7); local b1, b2, b3, b4, b5, b6 = s_byte(packet, 1, 6); return setmetatable({ priority = b1*256+b2; weight = b3*256+b4; port = b5*256+b6; target = name; }, srv_mt); end local txt_mt = { __tostring = t_concat }; function parsers.TXT(packet) local pack_len = #packet; local r, pos, len = {}, 1; repeat len = s_byte(packet, pos) or 0; t_insert(r, s_sub(packet, pos + 1, pos + len)); pos = pos + len + 1; until pos >= pack_len; return setmetatable(r, txt_mt); end parsers.SPF = parsers.TXT; -- Acronyms from RFC 7218 local tlsa_usages = { [0] = "PKIX-CA"; [1] = "PKIX-EE"; [2] = "DANE-TA"; [3] = "DANE-EE"; [255] = "PrivCert"; }; local tlsa_selectors = { [0] = "Cert", [1] = "SPKI", [255] = "PrivSel", }; local tlsa_match_types = { [0] = "Full", [1] = "SHA2-256", [2] = "SHA2-512", [255] = "PrivMatch", }; local tlsa_mt = { __tostring = function(rr) return s_format("%s %s %s %s", tlsa_usages[rr.use] or rr.use, tlsa_selectors[rr.select] or rr.select, tlsa_match_types[rr.match] or rr.match, tohex(rr.data)); end; __index = { getUsage = function(rr) return tlsa_usages[rr.use] end; getSelector = function(rr) return tlsa_selectors[rr.select] end; getMatchType = function(rr) return tlsa_match_types[rr.match] end; } }; function parsers.TLSA(packet) local use, select, match = s_byte(packet, 1,3); return setmetatable({ use = use; select = select; match = match; data = s_sub(packet, 4); }, tlsa_mt); end local svcb_params = {"alpn"; "no-default-alpn"; "port"; "ipv4hint"; "ech"; "ipv6hint"}; setmetatable(svcb_params, {__index = function(_, n) return "key" .. tostring(n); end}); local svcb_mt = { __tostring = function (rr) local kv = {}; for i = 1, #rr.fields do t_insert(kv, s_format("%s=%q", svcb_params[rr.fields[i].key], tostring(rr.fields[i].value))); -- FIXME the =value part may be omitted when the value is "empty" end return s_format("%d %s %s", rr.prio, rr.name, t_concat(kv, " ")); end; }; local svbc_ip_mt = {__tostring = function(ip) return t_concat(ip, ", "); end} function parsers.SVCB(packet) local prio_h, prio_l = packet:byte(1,2); local prio = prio_h*256+prio_l; local name, pos = readDnsName(packet, 3); local fields = {}; while #packet > pos do local key_h, key_l = packet:byte(pos+0,pos+1); local len_h, len_l = packet:byte(pos+2,pos+3); local key = key_h*256+key_l; local len = len_h*256+len_l; local value = packet:sub(pos+4,pos+4-1+len) if key == 1 then value = setmetatable(parsers.TXT(value), svbc_ip_mt); elseif key == 3 then local port_h, port_l = value:byte(1,2); local port = port_h*256+port_l; value = port; elseif key == 4 then local ip = {}; for i = 1, #value, 4 do t_insert(ip, parsers.A(value:sub(i, i+3))); end value = setmetatable(ip, svbc_ip_mt); elseif key == 6 then local ip = {}; for i = 1, #value, 16 do t_insert(ip, parsers.AAAA(value:sub(i, i+15))); end value = setmetatable(ip, svbc_ip_mt); end t_insert(fields, { key = key, value = value, len = len }); pos = pos+len+4; end return setmetatable({ prio = prio, name = name, fields = fields, }, svcb_mt); end parsers.HTTPS = parsers.SVCB; local params = { TLSA = { use = tlsa_usages; select = tlsa_selectors; match = tlsa_match_types; }; }; local fallback_mt = { __tostring = function(rr) return s_format([[\# %d %s]], #rr.raw, tohex(rr.raw)); end; }; local function fallback_parser(packet) return setmetatable({ raw = packet },fallback_mt); end setmetatable(parsers, { __index = function() return fallback_parser end }); return { parsers = parsers; classes = iana_data.classes; types = iana_data.types; errors = iana_data.errors; params = params; }; prosody-13.0.1/util/PaxHeaders/dnsregistry.lua0000644000000000000000000000011714773555365016435 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.843711539 prosody-13.0.1/util/dnsregistry.lua0000644000175000017500000001064314773555365020640 0ustar00prosodyprosody00000000000000-- Source: https://www.iana.org/assignments/dns-parameters/dns-parameters.xml -- Generated on 2025-02-09 return { classes = { ["IN"] = 1; [1] = "IN"; ["CH"] = 3; [3] = "CH"; ["HS"] = 4; [4] = "HS"; ["ANY"] = 255; [255] = "ANY"; }; types = { ["A"] = 1; [1] = "A"; ["NS"] = 2; [2] = "NS"; ["MD"] = 3; [3] = "MD"; ["MF"] = 4; [4] = "MF"; ["CNAME"] = 5; [5] = "CNAME"; ["SOA"] = 6; [6] = "SOA"; ["MB"] = 7; [7] = "MB"; ["MG"] = 8; [8] = "MG"; ["MR"] = 9; [9] = "MR"; ["NULL"] = 10; [10] = "NULL"; ["WKS"] = 11; [11] = "WKS"; ["PTR"] = 12; [12] = "PTR"; ["HINFO"] = 13; [13] = "HINFO"; ["MINFO"] = 14; [14] = "MINFO"; ["MX"] = 15; [15] = "MX"; ["TXT"] = 16; [16] = "TXT"; ["RP"] = 17; [17] = "RP"; ["AFSDB"] = 18; [18] = "AFSDB"; ["X25"] = 19; [19] = "X25"; ["ISDN"] = 20; [20] = "ISDN"; ["RT"] = 21; [21] = "RT"; ["NSAP"] = 22; [22] = "NSAP"; ["NSAP-PTR"] = 23; [23] = "NSAP-PTR"; ["SIG"] = 24; [24] = "SIG"; ["KEY"] = 25; [25] = "KEY"; ["PX"] = 26; [26] = "PX"; ["GPOS"] = 27; [27] = "GPOS"; ["AAAA"] = 28; [28] = "AAAA"; ["LOC"] = 29; [29] = "LOC"; ["NXT"] = 30; [30] = "NXT"; ["EID"] = 31; [31] = "EID"; ["NIMLOC"] = 32; [32] = "NIMLOC"; ["SRV"] = 33; [33] = "SRV"; ["ATMA"] = 34; [34] = "ATMA"; ["NAPTR"] = 35; [35] = "NAPTR"; ["KX"] = 36; [36] = "KX"; ["CERT"] = 37; [37] = "CERT"; ["A6"] = 38; [38] = "A6"; ["DNAME"] = 39; [39] = "DNAME"; ["SINK"] = 40; [40] = "SINK"; ["OPT"] = 41; [41] = "OPT"; ["APL"] = 42; [42] = "APL"; ["DS"] = 43; [43] = "DS"; ["SSHFP"] = 44; [44] = "SSHFP"; ["IPSECKEY"] = 45; [45] = "IPSECKEY"; ["RRSIG"] = 46; [46] = "RRSIG"; ["NSEC"] = 47; [47] = "NSEC"; ["DNSKEY"] = 48; [48] = "DNSKEY"; ["DHCID"] = 49; [49] = "DHCID"; ["NSEC3"] = 50; [50] = "NSEC3"; ["NSEC3PARAM"] = 51; [51] = "NSEC3PARAM"; ["TLSA"] = 52; [52] = "TLSA"; ["SMIMEA"] = 53; [53] = "SMIMEA"; ["HIP"] = 55; [55] = "HIP"; ["NINFO"] = 56; [56] = "NINFO"; ["RKEY"] = 57; [57] = "RKEY"; ["TALINK"] = 58; [58] = "TALINK"; ["CDS"] = 59; [59] = "CDS"; ["CDNSKEY"] = 60; [60] = "CDNSKEY"; ["OPENPGPKEY"] = 61; [61] = "OPENPGPKEY"; ["CSYNC"] = 62; [62] = "CSYNC"; ["ZONEMD"] = 63; [63] = "ZONEMD"; ["SVCB"] = 64; [64] = "SVCB"; ["HTTPS"] = 65; [65] = "HTTPS"; ["DSYNC"] = 66; [66] = "DSYNC"; ["SPF"] = 99; [99] = "SPF"; ["NID"] = 104; [104] = "NID"; ["L32"] = 105; [105] = "L32"; ["L64"] = 106; [106] = "L64"; ["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"; ["AXFR"] = 252; [252] = "AXFR"; ["MAILB"] = 253; [253] = "MAILB"; ["MAILA"] = 254; [254] = "MAILA"; ["*"] = 255; [255] = "*"; ["URI"] = 256; [256] = "URI"; ["CAA"] = 257; [257] = "CAA"; ["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"; }; errors = { [0] = "NoError"; ["NoError"] = "No Error"; [1] = "FormErr"; ["FormErr"] = "Format Error"; [2] = "ServFail"; ["ServFail"] = "Server Failure"; [3] = "NXDomain"; ["NXDomain"] = "Non-Existent Domain"; [4] = "NotImp"; ["NotImp"] = "Not Implemented"; [5] = "Refused"; ["Refused"] = "Query Refused"; [6] = "YXDomain"; ["YXDomain"] = "Name Exists when it should not"; [7] = "YXRRSet"; ["YXRRSet"] = "RR Set Exists when it should not"; [8] = "NXRRSet"; ["NXRRSet"] = "RR Set that should exist does not"; [9] = "NotAuth"; ["NotAuth"] = "Server Not Authoritative for zone"; -- [9] = "NotAuth"; ["NotAuth"] = "Not Authorized"; [10] = "NotZone"; ["NotZone"] = "Name not contained in zone"; [11] = "DSOTYPENI"; ["DSOTYPENI"] = "DSO-TYPE Not Implemented"; [16] = "BADVERS"; ["BADVERS"] = "Bad OPT Version"; -- [16] = "BADSIG"; ["BADSIG"] = "TSIG Signature Failure"; [17] = "BADKEY"; ["BADKEY"] = "Key not recognized"; [18] = "BADTIME"; ["BADTIME"] = "Signature out of time window"; [19] = "BADMODE"; ["BADMODE"] = "Bad TKEY Mode"; [20] = "BADNAME"; ["BADNAME"] = "Duplicate key name"; [21] = "BADALG"; ["BADALG"] = "Algorithm not supported"; [22] = "BADTRUNC"; ["BADTRUNC"] = "Bad Truncation"; [23] = "BADCOOKIE"; ["BADCOOKIE"] = "Bad/missing Server Cookie"; }; }; prosody-13.0.1/util/PaxHeaders/envload.lua0000644000000000000000000000011714773555365015510 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.843711539 prosody-13.0.1/util/envload.lua0000644000175000017500000000115714773555365017713 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2011 Florian Zeitz -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: ignore 113/setfenv 113/loadstring local load = load; local io_open = io.open; local function envload(code, source, env) return load(code, source, nil, env); 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 }; prosody-13.0.1/util/PaxHeaders/error.lua0000644000000000000000000000011714773555365015211 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.843711539 prosody-13.0.1/util/error.lua0000644000175000017500000001001714773555365017407 0ustar00prosodyprosody00000000000000local id = require "prosody.util.id"; local util_debug; -- only imported on-demand -- Library configuration (see configure()) local auto_inject_traceback = false; local error_mt = { __name = "error" }; function error_mt:__tostring() return ("error<%s:%s:%s>"):format(self.type, self.condition, self.text or ""); end local function is_error(e) return getmetatable(e) == error_mt; end 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 "prosody.util.debug"; end end end -- Do we want any more well-known fields? -- Or could we just copy all fields from `e`? -- Sometimes you want variable details in the `text`, how to handle that? -- Translations? -- Should the `type` be restricted to the stanza error types or free-form? -- What to set `type` to for stream errors or SASL errors? Those don't have a 'type' attr. local function new(e, context, registry, source) if is_error(e) then return e; end local template = registry and registry[e]; if not template then if type(e) == "table" then template = { code = e.code; type = e.type; condition = e.condition; text = e.text; extra = e.extra; }; else template = {}; end end context = context or {}; if auto_inject_traceback then context.traceback = util_debug.get_traceback_table(nil, 2); end local error_instance = setmetatable({ instance_id = id.short(); type = template.type or "cancel"; condition = template.condition or "undefined-condition"; text = template.text; code = template.code; extra = template.extra; context = context; source = source; }, error_mt); return error_instance; end -- compact --> normal form local function expand_registry(namespace, registry) local mapped = {} for err,template in pairs(registry) do local e = { type = template[1]; condition = template[2]; text = template[3]; }; if namespace and template[4] then e.extra = { namespace = namespace, condition = template[4] }; end mapped[err] = e; end return mapped; end local function init(source, namespace, registry) if type(namespace) == "table" then -- registry can be given as second argument if namespace is not used registry, namespace = namespace, nil; end local _, protoerr = next(registry, nil); if protoerr and type(next(protoerr)) == "number" then registry = expand_registry(namespace, registry); end local function wrap(e, context) if is_error(e) then return e; end local err = new(registry[e] or { type = "cancel", condition = "undefined-condition" }, context, registry, source); err.context.wrapped_error = e; return err; end return { source = source; registry = registry; new = function (e, context) return new(e, context, registry, source); end; coerce = function (ok, err, ...) if ok then return ok, err, ...; end return nil, wrap(err); end; wrap = wrap; is_error = is_error; }; end local function coerce(ok, err, ...) if ok or is_error(err) then return ok, err, ...; end local new_err = new({ type = "cancel", condition = "undefined-condition" }, { wrapped_error = err }); return ok, new_err, ...; end local function from_stanza(stanza, context, source) local error_type, condition, text, extra_tag = stanza:get_error(); local error_tag = stanza:get_child("error"); context = context or {}; context.stanza = stanza; context.by = error_tag and error_tag.attr.by or stanza.attr.from; local uri; if condition == "gone" or condition == "redirect" then uri = error_tag:get_child_text(condition, "urn:ietf:params:xml:ns:xmpp-stanzas"); end return new({ type = error_type or "cancel"; condition = condition or "undefined-condition"; text = text; extra = (extra_tag or uri) and { uri = uri; tag = extra_tag; } or nil; }, context, nil, source); end return { new = new; init = init; coerce = coerce; is_error = is_error; is_err = is_error; -- COMPAT w/ older 0.12 trunk from_stanza = from_stanza; configure = configure; } prosody-13.0.1/util/PaxHeaders/events.lua0000644000000000000000000000011714773555365015364 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.847711555 prosody-13.0.1/util/events.lua0000644000175000017500000001073114773555365017565 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local pairs = pairs; local t_insert = table.insert; local t_remove = table.remove; local t_sort = table.sort; local setmetatable = setmetatable; local next = next; local _ENV = nil; -- luacheck: std none local function new() -- Map event name to ordered list of handlers (lazily built): handlers[event_name] = array_of_handler_functions local handlers = {}; -- Array of wrapper functions that wrap all events (nil if empty) local global_wrappers; -- Per-event wrappers: wrappers[event_name] = wrapper_function local wrappers = {}; -- Event map: event_map[handler_function] = priority_number local event_map = {}; -- Debug hook, if any local active_debug_hook = nil; -- Called on-demand to build handlers entries local function _rebuild_index(self, event) local _handlers = event_map[event]; if not _handlers or next(_handlers) == nil then return; end local index = {}; for handler in pairs(_handlers) do t_insert(index, handler); end t_sort(index, function(a, b) return _handlers[a] > _handlers[b]; end); self[event] = index; return index; end; setmetatable(handlers, { __index = _rebuild_index }); local function add_handler(event, handler, priority) local map = event_map[event]; if map then map[handler] = priority or 0; else map = {[handler] = priority or 0}; event_map[event] = map; end handlers[event] = nil; end; local function remove_handler(event, handler) local map = event_map[event]; if map then map[handler] = nil; handlers[event] = nil; if next(map) == nil then event_map[event] = nil; end end end; local function get_handlers(event) return handlers[event]; end; local function add_handlers(self) for event, handler in pairs(self) do add_handler(event, handler); end end; local function remove_handlers(self) for event, handler in pairs(self) do remove_handler(event, handler); end end; local function _fire_event(event_name, event_data) local h = handlers[event_name]; if h and not active_debug_hook then for i=1,#h do local ret = h[i](event_data); if ret ~= nil then return ret; end end elseif h and active_debug_hook then for i=1,#h do local ret = active_debug_hook(h[i], event_name, event_data); if ret ~= nil then return ret; end end end end; local function fire_event(event_name, event_data) -- luacheck: ignore 432/event_name 432/event_data local w = wrappers[event_name] or global_wrappers; if w then local curr_wrapper = #w; local function c(event_name, event_data) curr_wrapper = curr_wrapper - 1; if curr_wrapper == 0 then if global_wrappers == nil or w == global_wrappers then return _fire_event(event_name, event_data); end w, curr_wrapper = global_wrappers, #global_wrappers; return w[curr_wrapper](c, event_name, event_data); else return w[curr_wrapper](c, event_name, event_data); end end return w[curr_wrapper](c, event_name, event_data); end return _fire_event(event_name, event_data); end local function add_wrapper(event_name, wrapper) local w; if event_name == false then w = global_wrappers; if not w then w = {}; global_wrappers = w; end else w = wrappers[event_name]; if not w then w = {}; wrappers[event_name] = w; end end w[#w+1] = wrapper; end local function remove_wrapper(event_name, wrapper) local w; if event_name == false then w = global_wrappers; else w = wrappers[event_name]; end if not w then return; end for i = #w, 1, -1 do if w[i] == wrapper then t_remove(w, i); end end if #w == 0 then if event_name == false then global_wrappers = nil; else wrappers[event_name] = nil; end end end local function set_debug_hook(new_hook) local old_hook = active_debug_hook; active_debug_hook = new_hook; return old_hook; end return { add_handler = add_handler; remove_handler = remove_handler; add_handlers = add_handlers; remove_handlers = remove_handlers; get_handlers = get_handlers; wrappers = { add_handler = add_wrapper; remove_handler = remove_wrapper; }; add_wrapper = add_wrapper; remove_wrapper = remove_wrapper; set_debug_hook = set_debug_hook; fire_event = fire_event; _handlers = handlers; _event_map = event_map; }; end return { new = new; }; prosody-13.0.1/util/PaxHeaders/filters.lua0000644000000000000000000000011714773555365015530 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.847711555 prosody-13.0.1/util/filters.lua0000644000175000017500000000410514773555365017727 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local t_insert, t_remove = table.insert, table.remove; local _ENV = nil; -- luacheck: std none local new_filter_hooks = {}; local function initialize(session) if not session.filters then local filters = {}; session.filters = filters; function session.filter(type, data) local filter_list = filters[type]; if filter_list then for i = 1, #filter_list do data = filter_list[i](data, session); if data == nil then break; end end end return data; end end for i=1,#new_filter_hooks do new_filter_hooks[i](session); end return session.filter; end local function add_filter(session, type, callback, priority) if not session.filters then initialize(session); end local filter_list = session.filters[type]; if not filter_list then filter_list = {}; session.filters[type] = filter_list; elseif filter_list[callback] then return; -- Filter already added end priority = priority or 0; local i = 0; repeat i = i + 1; until not filter_list[i] or filter_list[filter_list[i]] < priority; t_insert(filter_list, i, callback); filter_list[callback] = priority; end local function remove_filter(session, type, callback) if not session.filters then return; end local filter_list = session.filters[type]; if filter_list and filter_list[callback] then for i=1, #filter_list do if filter_list[i] == callback then t_remove(filter_list, i); filter_list[callback] = nil; return true; end end end end local function add_filter_hook(callback) t_insert(new_filter_hooks, callback); end local function remove_filter_hook(callback) for i=1,#new_filter_hooks do if new_filter_hooks[i] == callback then t_remove(new_filter_hooks, i); end end end return { initialize = initialize; add_filter = add_filter; remove_filter = remove_filter; add_filter_hook = add_filter_hook; remove_filter_hook = remove_filter_hook; }; prosody-13.0.1/util/PaxHeaders/format.lua0000644000000000000000000000011714773555365015350 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.847711555 prosody-13.0.1/util/format.lua0000644000175000017500000001160314773555365017550 0ustar00prosodyprosody00000000000000-- -- A string.format wrapper that gracefully handles invalid arguments since -- certain format string and argument combinations may cause errors or other -- issues like log spoofing -- -- Provides some protection from e.g. CAPEC-135, CWE-117, CWE-134, CWE-93 local tostring = tostring; local unpack = table.unpack; local pack = table.pack; local valid_utf8 = require "prosody.util.encodings".utf8.valid; local type = type; 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, }; -- In Lua 5.2 these throw an error given a negative number local expects_positive = { o = true; u = true; x = true; X = true }; -- Printable Unicode replacements for control characters local control_symbols = { -- 0x00 .. 0x1F --> U+2400 .. U+241F, 0x7F --> U+2421 ["\000"] = "\226\144\128", ["\001"] = "\226\144\129", ["\002"] = "\226\144\130", ["\003"] = "\226\144\131", ["\004"] = "\226\144\132", ["\005"] = "\226\144\133", ["\006"] = "\226\144\134", ["\007"] = "\226\144\135", ["\008"] = "\226\144\136", ["\009"] = "\226\144\137", ["\010"] = "\226\144\138", ["\011"] = "\226\144\139", ["\012"] = "\226\144\140", ["\013"] = "\226\144\141", ["\014"] = "\226\144\142", ["\015"] = "\226\144\143", ["\016"] = "\226\144\144", ["\017"] = "\226\144\145", ["\018"] = "\226\144\146", ["\019"] = "\226\144\147", ["\020"] = "\226\144\148", ["\021"] = "\226\144\149", ["\022"] = "\226\144\150", ["\023"] = "\226\144\151", ["\024"] = "\226\144\152", ["\025"] = "\226\144\153", ["\026"] = "\226\144\154", ["\027"] = "\226\144\155", ["\028"] = "\226\144\156", ["\029"] = "\226\144\157", ["\030"] = "\226\144\158", ["\031"] = "\226\144\159", ["\127"] = "\226\144\161", }; local supports_p = pcall(string.format, "%p", ""); -- >= Lua 5.4 local function format(formatstring, ...) local args = pack(...); local args_length = args.n; -- format specifier spec: -- 1. Start: '%%' -- 2. Flags: '[%-%+ #0]' -- 3. Width: '%d?%d?' -- 4. Precision: '%.?%d?%d?' -- 5. Option: '[cdiouxXaAeEfgGqs%%]' -- -- The options c, d, E, e, f, g, G, i, o, u, X, and x all expect a number as argument, whereas q and s expect a string. -- This function does not accept string values containing embedded zeros, except as arguments to the q option. -- a and A are only in Lua 5.2+ -- Lua 5.4 adds a p format that produces a pointer -- process each format specifier local i = 0; formatstring = formatstring:gsub("%%[^cdiouxXaAeEfgGpqs%%]*[cdiouxXaAeEfgGpqs%%]", function(spec) if spec == "%%" then return end i = i + 1; local arg = args[i]; if arg == nil then args[i] = "nil"; return "(%s)"; end local option = spec:sub(-1); local t = type(arg); if option == "s" and t == "string" and not arg:find("[%z\1-\31\128-\255]") then -- No UTF-8 or control characters, assumed to be the common case. return elseif t == "number" then if option == "g" or (option == "d" and num_type(arg) == "integer") then return end elseif option == "s" and t ~= "string" then arg = tostring(arg); t = "string"; end if option ~= "s" and option ~= "q" and option ~= "p" then -- all other options expect numbers if t ~= "number" then -- arg isn't number as expected? arg = tostring(arg); option = "s"; spec = "[%s]"; t = "string"; elseif expects_integer[option] and num_type(arg) ~= "integer" then args[i] = tostring(arg); return "[%s]"; elseif expects_positive[option] and arg < 0 then args[i] = tostring(arg); return "[%s]"; else return -- acceptable number end end if option == "p" and not supports_p then arg = tostring(arg); option = "s"; spec = "[%s]"; t = "string"; end if t == "string" and option ~= "p" then if not valid_utf8(arg) then option = "q"; elseif option ~= "q" then -- gets fully escaped in the next block -- Prevent funny things with ASCII control characters and ANSI escape codes (CWE-117) -- Also ensure embedded newlines can't look like another log line (CWE-93) args[i] = arg:gsub("[%z\1-\8\11-\31\127]", control_symbols):gsub("\n\t?", "\n\t"); return spec; end end if option == "q" then args[i] = dump(arg); return "%s"; end if option == "p" and (t == "boolean" or t == "number") then args[i] = tostring(arg); return "[%s]"; end end); -- process extra args while i < args_length do i = i + 1; local arg = args[i]; if arg == nil then args[i] = "(nil)"; else args[i] = tostring(arg):gsub("[%z\1-\8\11-\31\127]", control_symbols):gsub("\n\t?", "\n\t"); end formatstring = formatstring .. " [%s]" end return formatstring:format(unpack(args)); end return { format = format; }; prosody-13.0.1/util/PaxHeaders/fsm.lua0000644000000000000000000000011714773555365014645 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.847711555 prosody-13.0.1/util/fsm.lua0000644000175000017500000000764414773555365017057 0ustar00prosodyprosody00000000000000local 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; }; prosody-13.0.1/util/PaxHeaders/gc.lua0000644000000000000000000000011714773555365014451 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.847711555 prosody-13.0.1/util/gc.lua0000644000175000017500000000251414773555365016652 0ustar00prosodyprosody00000000000000local set = require "prosody.util.set"; local known_options = { incremental = set.new { "mode", "threshold", "speed", "step_size" }; generational = set.new { "mode", "minor_threshold", "major_threshold" }; }; if _VERSION ~= "Lua 5.4" then known_options.generational = nil; known_options.incremental:remove("step_size"); end local function configure(user, defaults) local mode = user.mode or defaults.mode or "incremental"; if not known_options[mode] then return nil, "GC mode not supported on ".._VERSION..": "..mode; end for k, v in pairs(user) do if not known_options[mode]:contains(k) then return nil, "Unknown GC parameter: "..k; elseif k ~= "mode" and type(v) ~= "number" then return nil, "parameter '"..k.."' should be a number"; end end if mode == "incremental" then if _VERSION == "Lua 5.4" then collectgarbage(mode, user.threshold or defaults.threshold, user.speed or defaults.speed, user.step_size or defaults.step_size ); else collectgarbage("setpause", user.threshold or defaults.threshold); collectgarbage("setstepmul", user.speed or defaults.speed); end elseif mode == "generational" then collectgarbage(mode, user.minor_threshold or defaults.minor_threshold, user.major_threshold or defaults.major_threshold ); end return true; end return { configure = configure; }; prosody-13.0.1/util/PaxHeaders/hashring.lua0000644000000000000000000000011714773555365015663 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.851711571 prosody-13.0.1/util/hashring.lua0000644000175000017500000000463514773555365020072 0ustar00prosodyprosody00000000000000local it = require "prosody.util.iterators"; local function generate_ring(nodes, num_replicas, hash) local new_ring = {}; for _, node_name in ipairs(nodes) do for replica = 1, num_replicas do local replica_hash = hash(node_name..":"..replica); new_ring[replica_hash] = node_name; table.insert(new_ring, replica_hash); end end table.sort(new_ring); return new_ring; end local hashring_methods = {}; local hashring_mt = { __index = function (self, k) -- Automatically build self.ring if it's missing if k == "ring" then local ring = generate_ring(self.nodes, self.num_replicas, self.hash); rawset(self, "ring", ring); return ring; end return rawget(hashring_methods, k); end }; 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, value) self.ring = nil; 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; 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 return true; end function hashring_methods:remove_node(node_name) self.ring = nil; 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; table.remove(self.nodes, i); return true; end end end return false; end function hashring_methods:remove_nodes(nodes) self.ring = nil; for _, node_name in ipairs(nodes) do self:remove_node(node_name); end end function hashring_methods:clone() local clone_hashring = new(self.num_replicas, self.hash); 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 node = self.ring[replica_hash]; break; end end if not node then node = self.ring[self.ring[1]]; end return node, self.nodes[node]; end return { new = new; } prosody-13.0.1/util/PaxHeaders/helpers.lua0000644000000000000000000000011714773555365015522 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.851711571 prosody-13.0.1/util/helpers.lua0000644000175000017500000000576114773555365017732 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local debug = require "prosody.util.debug"; -- Helper functions for debugging local log = require "prosody.util.logger".init("util.debug"); local function log_events(events, name, logger) local f = events.fire_event; if not f then error("Object does not appear to be a util.events object"); end logger = logger or log; name = name or tostring(events); function events.fire_event(event, ...) logger("debug", "%s firing event: %s", name, event); return f(event, ...); end local function event_handler_hook(handler, event_name, event_data) logger("debug", "calling handler for %s: %s", event_name, handler); local ok, ret = pcall(handler, event_data); if not ok then logger("error", "error in event handler %s: %s", handler, ret); error(ret); end if ret ~= nil then logger("debug", "event chain ended for %s by %s with result: %s", event_name, handler, ret); end return ret; end events.set_debug_hook(event_handler_hook); events[events.fire_event] = f; return events; end local function revert_log_events(events) events.fire_event, events[events.fire_event] = events[events.fire_event], nil; -- :)) events.set_debug_hook(nil); end local function log_host_events(host) return log_events(prosody.hosts[host].events, host); end local function revert_log_host_events(host) return revert_log_events(prosody.hosts[host].events); end local function show_events(events, specific_event) local event_handlers = events._handlers; local events_array = {}; local event_handler_arrays = {}; for event, priorities in pairs(events._event_map) do local handlers = event_handlers[event]; if handlers and (event == specific_event or not specific_event) then table.insert(events_array, event); local handler_strings = {}; for i, handler in ipairs(handlers) do local upvals = debug.string_from_var_table(debug.get_upvalues_table(handler)); handler_strings[i] = " "..(priorities[handler] or "?")..": "..tostring(handler)..(upvals and ("\n "..upvals) or ""); end event_handler_arrays[event] = handler_strings; end end table.sort(events_array); local i = 1; while i <= #events_array do local handlers = event_handler_arrays[events_array[i]]; for j=#handlers, 1, -1 do table.insert(events_array, i+1, handlers[j]); end if i > 1 then events_array[i] = "\n"..events_array[i]; end i = i + #handlers + 1 end return table.concat(events_array, "\n"); end local function get_upvalue(f, get_name) local i, name, value = 0; repeat i = i + 1; name, value = debug.getupvalue(f, i); until name == get_name or name == nil; return value; end return { log_host_events = log_host_events; revert_log_host_events = revert_log_host_events; log_events = log_events; revert_log_events = revert_log_events; show_events = show_events; get_upvalue = get_upvalue; }; prosody-13.0.1/util/PaxHeaders/hex.lua0000644000000000000000000000011714773555365014644 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.851711571 prosody-13.0.1/util/hex.lua0000644000175000017500000000105414773555365017043 0ustar00prosodyprosody00000000000000local s_char = string.char; local s_format = string.format; local s_gsub = string.gsub; local s_lower = string.lower; local char_to_hex = {}; local hex_to_char = {}; do local char, hex; for i = 0,255 do char, hex = s_char(i), s_format("%02x", i); char_to_hex[char] = hex; hex_to_char[hex] = char; end end local function to(s) return (s_gsub(s, ".", char_to_hex)); end local function from(s) return (s_gsub(s_lower(s), "%X*(%x%x)%X*", hex_to_char)); end return { encode = to, decode = from; -- COMPAT w/pre-0.12: to = to, from = from; }; prosody-13.0.1/util/PaxHeaders/hmac.lua0000644000000000000000000000011714773555365014770 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.851711571 prosody-13.0.1/util/hmac.lua0000644000175000017500000000106414773555365017170 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- COMPAT: Only for external pre-0.9 modules 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, }; prosody-13.0.1/util/PaxHeaders/http.lua0000644000000000000000000000011714773555365015037 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.851711571 prosody-13.0.1/util/http.lua0000644000175000017500000000545314773555365017245 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2013 Florian Zeitz -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local format, char = string.format, string.char; local pairs, ipairs = pairs, ipairs; local t_insert, t_concat = table.insert, table.concat; local url_codes = {}; for i = 0, 255 do local c = char(i); local u = format("%%%02x", i); url_codes[c] = u; url_codes[u] = c; url_codes[u:upper()] = c; end local function urlencode(s) return s and (s:gsub("[^a-zA-Z0-9.~_-]", url_codes)); end local function urldecode(s) return s and (s:gsub("%%%x%x", url_codes)); end local function _formencodepart(s) return s and (urlencode(s):gsub("%%20", "+")); end local function formencode(form) local result = {}; if form[1] then -- Array of ordered { name, value } for _, field in ipairs(form) do t_insert(result, _formencodepart(field.name).."=".._formencodepart(field.value)); end else -- Unordered map of name -> value for name, value in pairs(form) do t_insert(result, _formencodepart(name).."=".._formencodepart(value)); end end return t_concat(result, "&"); end local function formdecode(s) if not s:match("=") then return urldecode(s); end local r = {}; for k, v in s:gmatch("([^=&]*)=([^&]*)") do k, v = k:gsub("%+", "%%20"), v:gsub("%+", "%%20"); k, v = urldecode(k), urldecode(v); t_insert(r, { name = k, value = v }); r[k] = v; end return r; end local function contains_token(field, token) field = ","..field:gsub("[ \t]", ""):lower()..","; return field:find(","..token:lower()..",", 1, true) ~= nil; end local function normalize_path(path, is_dir) if is_dir then if path:sub(-1,-1) ~= "/" then path = path.."/"; end else if path:sub(-1,-1) == "/" then path = path:sub(1, -2); end end if path:sub(1,1) ~= "/" then path = "/"..path; end 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; }; prosody-13.0.1/util/PaxHeaders/human0000644000000000000000000000013114773555365014404 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.855711587 30 ctime=1743706869.855711587 prosody-13.0.1/util/human/0000755000175000017500000000000014773555365016664 5ustar00prosodyprosody00000000000000prosody-13.0.1/util/human/PaxHeaders/io.lua0000644000000000000000000000011714773555365015577 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.855711587 prosody-13.0.1/util/human/io.lua0000644000175000017500000001337714773555365020011 0ustar00prosodyprosody00000000000000local 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; end; local function getchar(n) local stty_ret = os.execute("stty raw -echo 2>/dev/null"); local ok, char; if stty_ret then ok, char = pcall(io.read, n or 1); os.execute("stty sane"); else ok, char = pcall(io.read, "*l"); if ok then char = char:sub(1, n or 1); end end if ok then return char; end end local function getline() local ok, line = pcall(io.read, "*l"); if ok then return line; end end local function getpass() 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 then os.execute("stty sane"); else io.write("\027[00m"); end io.write("\n"); if ok then return pass; end end local function show_yesno(prompt) io.write(prompt, " "); local choice = getchar():lower(); io.write("\n"); if not choice:match("%a") then choice = prompt:match("%[.-(%U).-%]$"); if not choice then return nil; end end return (choice == "y"); end local function read_password() local password; while true do io.write("Enter new password: "); password = getpass(); if not password then print("No password - cancelled"); return; end io.write("Retype new password: "); if getpass() ~= password then if not show_yesno [=[Passwords did not match, try again? [Y/n]]=] then return; end else break; end end return password; end local function show_prompt(prompt) io.write(prompt, " "); local line = getline(); line = line and line:gsub("\n$",""); return (line and #line > 0) and line or nil; end local function printf(fmt, ...) print(fmt:format(...)); end local function padright(s, width) return s..string.rep(" ", width-len(s)); end local function padleft(s, width) return string.rep(" ", width-len(s))..s; end local pat = "[%z\001-\127\194-\253][\128-\191]*"; local function utf8_cut(s, pos) return s:match("^"..pat:rep(pos)) or s; end if utf8.len and utf8.offset then function utf8_cut(s, pos) return s:sub(1, utf8.offset(s, pos+1)-1); 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 return utf8_cut(s, width - 1) .. "…"; end local function new_table(col_specs, max_width) max_width = max_width or term_width(80); local separator = " | "; local widths = {}; local total_width = max_width - #separator * (#col_specs-1); local free_width = total_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: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; end end -- Calculate width of proportional columns local total_proportional_width = 0; for i = 1, #col_specs do if not widths[i] then 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 return function (row) local titles; if not row then titles, row = true, array.pluck(col_specs, "title", ""); end local output = {}; for i, column in ipairs(col_specs) do 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, width, column); end if v == nil then v = column.default or ""; else v = tostring(v); end if len(v) < width then if column.align == "right" then v = padleft(v, width); else v = padright(v, width); end elseif len(v) > width then v = (column.ellipsis or ellipsis)(v, width); end table.insert(output, v); end return table.concat(output, separator); 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 { getchar = getchar; getline = getline; getpass = getpass; show_yesno = show_yesno; read_password = read_password; show_prompt = show_prompt; 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; }; prosody-13.0.1/util/human/PaxHeaders/units.lua0000644000000000000000000000011714773555365016332 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.855711587 prosody-13.0.1/util/human/units.lua0000644000175000017500000000300514773555365020527 0ustar00prosodyprosody00000000000000local math_abs = math.abs; local math_ceil = math.ceil; local math_floor = math.floor; local math_log = math.log; local math_max = math.max; local math_min = math.min; local unpack = table.unpack; local large = { "k", 1000, "M", 1000000, "G", 1000000000, "T", 1000000000000, "P", 1000000000000000, "E", 1000000000000000000, "Z", 1000000000000000000000, "Y", 1000000000000000000000000, } local small = { "m", 0.001, "μ", 0.000001, "n", 0.000000001, "p", 0.000000000001, "f", 0.000000000000001, "a", 0.000000000000000001, "z", 0.000000000000000000001, "y", 0.000000000000000000000001, } local binary = { "Ki", 2^10, "Mi", 2^20, "Gi", 2^30, "Ti", 2^40, "Pi", 2^50, "Ei", 2^60, "Zi", 2^70, "Yi", 2^80, } local function adjusted_unit(n, b) local round = math_floor; local prefixes = large; local logbase = 1000; if b == 'b' then prefixes = binary; logbase = 1024; elseif n < 1 then prefixes = small; round = math_ceil; end local m = math_max(0, math_min(8, round(math_abs(math_log(math_abs(n), logbase))))); local prefix, multiplier = unpack(prefixes, m * 2-1, m*2); return multiplier or 1, prefix; end -- n: number, the number to format -- unit: string, the base unit -- b: optional enum 'b', thousands base local function format(n, unit, b) --> string local fmt = "%.3g %s%s"; if n == 0 then return fmt:format(n, "", unit); end local multiplier, prefix = adjusted_unit(n, b); return fmt:format(n / multiplier, prefix or "", unit); end return { adjust = adjusted_unit; format = format; }; prosody-13.0.1/util/PaxHeaders/id.lua0000644000000000000000000000011714773555365014454 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.855711587 prosody-13.0.1/util/id.lua0000644000175000017500000000243014773555365016652 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2017 Matthew Wild -- Copyright (C) 2008-2017 Waqas Hussain -- Copyright (C) 2008-2017 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local s_gsub = string.gsub; local random_bytes = require "prosody.util.random".bytes; local base64_encode = require "prosody.util.encodings".base64.encode; local b64url = { ["+"] = "-", ["/"] = "_", ["="] = "" }; local function b64url_random(len) return (s_gsub(base64_encode(random_bytes(len)), "[+/=]", b64url)); end return { -- sizes divisible by 3 fit nicely into base64 without padding== -- for short lived things with low risk of collisions tiny = function() return b64url_random(3); end; -- close to 8 bytes, should be good enough for relatively short lived or uses -- scoped by host or users, half the size of an uuid short = function() return b64url_random(9); end; -- more entropy than uuid at 2/3 the size -- should be okay for globally scoped ids or security token medium = function() return b64url_random(18); end; -- as long as an uuid but MOAR entropy long = function() return b64url_random(27); end; -- pick your own adventure custom = function (size) return function () return b64url_random(size); end; end; } prosody-13.0.1/util/PaxHeaders/import.lua0000644000000000000000000000011714773555365015372 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.855711587 prosody-13.0.1/util/import.lua0000644000175000017500000000100514773555365017565 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local unpack = table.unpack; local t_insert = table.insert; function _G.import(module, ...) local m = package.loaded[module] or require(module); if type(m) == "table" and ... then local ret = {}; for _, f in ipairs{...} do t_insert(ret, m[f]); end return unpack(ret); end return m; end prosody-13.0.1/util/PaxHeaders/indexedbheap.lua0000644000000000000000000000011714773555365016500 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.855711587 prosody-13.0.1/util/indexedbheap.lua0000644000175000017500000000674514773555365020713 0ustar00prosodyprosody00000000000000 local setmetatable = setmetatable; local math_floor = math.floor; local t_remove = table.remove; local function _heap_insert(self, item, sync, item2, index) local pos = #self + 1; while true do local half_pos = math_floor(pos / 2); if half_pos == 0 or item > self[half_pos] then break; end self[pos] = self[half_pos]; sync[pos] = sync[half_pos]; index[sync[pos]] = pos; pos = half_pos; end self[pos] = item; sync[pos] = item2; index[item2] = pos; end local function _percolate_up(self, k, sync, index) local tmp = self[k]; local tmp_sync = sync[k]; while k ~= 1 do local parent = math_floor(k/2); if tmp >= self[parent] then break; end self[k] = self[parent]; sync[k] = sync[parent]; index[sync[k]] = k; k = parent; end self[k] = tmp; sync[k] = tmp_sync; index[tmp_sync] = k; return k; end local function _percolate_down(self, k, sync, index) local tmp = self[k]; local tmp_sync = sync[k]; local size = #self; local child = 2*k; while 2*k <= size do if child ~= size and self[child] > self[child + 1] then child = child + 1; end if tmp > self[child] then self[k] = self[child]; sync[k] = sync[child]; index[sync[k]] = k; else break; end k = child; child = 2*k; end self[k] = tmp; sync[k] = tmp_sync; index[tmp_sync] = k; return k; end local function _heap_pop(self, sync, index) local size = #self; if size == 0 then return nil; end local result = self[1]; local result_sync = sync[1]; index[result_sync] = nil; if size == 1 then self[1] = nil; sync[1] = nil; return result, result_sync; end self[1] = t_remove(self); sync[1] = t_remove(sync); index[sync[1]] = 1; _percolate_down(self, 1, sync, index); return result, result_sync; end local indexed_heap = {}; function indexed_heap:insert(item, priority, id) if id == nil then id = self.current_id; self.current_id = id + 1; end self.items[id] = item; _heap_insert(self.priorities, priority, self.ids, id, self.index); return id; end function indexed_heap:pop() local priority, id = _heap_pop(self.priorities, self.ids, self.index); if id then local item = self.items[id]; self.items[id] = nil; return priority, item, id; end end function indexed_heap:peek() return self.priorities[1]; end function indexed_heap:reprioritize(id, priority) local k = self.index[id]; if k == nil then return; end self.priorities[k] = priority; k = _percolate_up(self.priorities, k, self.ids, self.index); _percolate_down(self.priorities, k, self.ids, self.index); end function indexed_heap:remove_index(k) local result = self.priorities[k]; if result == nil then return; end local result_sync = self.ids[k]; local item = self.items[result_sync]; local size = #self.priorities; self.priorities[k] = self.priorities[size]; self.ids[k] = self.ids[size]; self.index[self.ids[k]] = k; t_remove(self.priorities); t_remove(self.ids); self.index[result_sync] = nil; self.items[result_sync] = nil; if size > k then k = _percolate_up(self.priorities, k, self.ids, self.index); _percolate_down(self.priorities, k, self.ids, self.index); end return result, item, result_sync; end function indexed_heap:remove(id) return self:remove_index(self.index[id]); end local mt = { __index = indexed_heap }; local _M = { create = function() return setmetatable({ ids = {}; -- heap of ids, sync'd with priorities items = {}; -- map id->items priorities = {}; -- heap of priorities index = {}; -- map of id->index of id in ids current_id = 1.5 }, mt); end }; return _M; prosody-13.0.1/util/PaxHeaders/interpolation.lua0000644000000000000000000000011714773555365016747 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.855711587 prosody-13.0.1/util/interpolation.lua0000644000175000017500000000614114773555365021150 0ustar00prosodyprosody00000000000000-- Simple template language -- -- The new() function takes a pattern and an escape function and returns -- a render() function. Both are required. -- -- The function render() takes a string template and a table of values. -- Sequences like {name} in the template string are substituted -- with values from the table, optionally depending on a modifier -- symbol. -- -- Variants are: -- {name} is substituted for values["name"] and is escaped using the -- second argument to new_render(). To disable the escaping, use {name!}. -- {name.item} can be used to access table items. -- To renter lists of items: {name# item number {idx} is {item} } -- Or key-value pairs: {name% t[ {idx} ] = {item} } -- To show a defaults for missing values {name? sub-template } can be used, -- which renders a sub-template if values["name"] is false-ish. -- {name& sub-template } does the opposite, the sub-template is rendered -- if the selected value is anything but false or nil. local type, tostring = type, tostring; local pairs, ipairs = pairs, ipairs; local s_sub, s_gsub, s_match = string.sub, string.gsub, string.match; local t_concat = table.concat; local function new_render(pat, escape, funcs) -- assert(type(pat) == "string", "bad argument #1 to 'new_render' (string expected)"); -- assert(type(escape) == "function", "bad argument #2 to 'new_render' (function expected)"); local function render(template, values) -- assert(type(template) == "string", "bad argument #1 to 'render' (string expected)"); -- assert(type(values) == "table", "bad argument #2 to 'render' (table expected)"); return (s_gsub(template, pat, function (block) block = s_sub(block, 2, -2); local name, raw, opt, e = s_match(block, "^([%a_][%w_.]*)(!?)(%p?)()"); if not name then return end local value = values[name]; if not value and name:find(".", 2, true) then value = values; for word in name:gmatch"[^.]+" do value = value[word]; if not value then break; end end end if funcs then while opt == '|' do local f; f, raw, opt, e = s_match(block, "^([%a_][%w_.]*)(!?)(%p?)()", e); f = funcs[f]; if value ~= nil and f then value = f(value); end end end if opt == '#' or opt == '%' then if type(value) ~= "table" then return ""; end local iter = opt == '#' and ipairs or pairs; local out, i, subtpl = {}, 1, s_sub(block, e); local subvalues = setmetatable({}, { __index = values }); for idx, item in iter(value) do subvalues.idx = idx; subvalues.item = item; out[i], i = render(subtpl, subvalues), i+1; end return t_concat(out); elseif opt == '&' then if not value then return ""; end return render(s_sub(block, e), values); elseif opt == '~' then if value then return ""; end return render(s_sub(block, e), values); elseif opt == '?' and not value then return render(s_sub(block, e), values); elseif value ~= nil then if type(value) ~= "string" then value = tostring(value); end if raw ~= '!' then return escape(value); end return value; end end)); end return render; end return { new = new_render; }; prosody-13.0.1/util/PaxHeaders/ip.lua0000644000000000000000000000011714773555365014470 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.855711587 prosody-13.0.1/util/ip.lua0000644000175000017500000001333114773555365016670 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2011 Florian Zeitz -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local net = require "prosody.util.net"; local strbit = require "prosody.util.strbitop"; local ip_methods = {}; local ip_mt = { __index = function (ip, key) local method = ip_methods[key]; if not method then return nil; end local ret = method(ip); ip[key] = ret; return ret; end, __tostring = function (ip) return ip.addr; end, }; ip_mt.__eq = function (ipA, ipB) if getmetatable(ipA) ~= ip_mt or getmetatable(ipB) ~= ip_mt then -- Lua 5.3+ calls this if both operands are tables, even if metatables differ return false; end return ipA.packed == ipB.packed; end local function new_ip(ipStr, proto) local zone; if (not proto or proto == "IPv6") and ipStr:find('%', 1, true) then ipStr, zone = ipStr:match("^(.-)%%(.*)"); end local packed, err = net.pton(ipStr); if not packed then return packed, err end if proto == "IPv6" and #packed ~= 16 then return nil, "invalid-ipv6"; elseif proto == "IPv4" and #packed ~= 4 then return nil, "invalid-ipv4"; elseif not proto then if #packed == 16 then proto = "IPv6"; elseif #packed == 4 then proto = "IPv4"; else return nil, "unknown protocol"; end elseif proto ~= "IPv6" and proto ~= "IPv4" then return nil, "invalid protocol"; end return setmetatable({ addr = ipStr, packed = packed, proto = proto, zone = zone }, ip_mt); end function ip_methods:normal() return net.ntop(self.packed); end -- 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.packed; end local match; local function commonPrefixLength(ipA, ipB) return strbit.common_prefix_bits(ipA.packed_full, ipB.packed_full); end -- Instantiate once local loopback = new_ip("::1"); local loopback4 = new_ip("127.0.0.0"); local sixtofour = new_ip("2002::"); local teredo = new_ip("2001::"); local linklocal = new_ip("fe80::"); local linklocal4 = new_ip("169.254.0.0"); local uniquelocal = new_ip("fc00::"); local sitelocal = new_ip("fec0::"); local sixbone = new_ip("3ffe::"); local defaultunicast = new_ip("::"); local multicast = new_ip("ff00::"); local ipv6mapped = new_ip("::ffff:0:0"); local function v4scope(ip) if match(ip, loopback4, 8) then return 0x2; elseif match(ip, linklocal4, 16) then return 0x2; else -- Global unicast return 0xE; end end local function v6scope(ip) if ip == loopback then return 0x2; elseif match(ip, linklocal, 10) then return 0x2; elseif match(ip, sitelocal, 10) then return 0x5; elseif match(ip, multicast, 10) then return ip.packed:byte(2) % 0x10; else -- Global unicast return 0xE; end end local function label(ip) if ip == loopback then return 0; elseif match(ip, sixtofour, 16) then return 2; elseif match(ip, teredo, 32) then return 5; elseif match(ip, uniquelocal, 7) then return 13; elseif match(ip, sitelocal, 10) then return 11; elseif match(ip, sixbone, 16) then return 12; elseif match(ip, defaultunicast, 96) then return 3; elseif match(ip, ipv6mapped, 96) then return 4; else return 1; end end local function precedence(ip) if ip == loopback then return 50; elseif match(ip, sixtofour, 16) then return 30; elseif match(ip, teredo, 32) then return 5; elseif match(ip, uniquelocal, 7) then return 3; elseif match(ip, sitelocal, 10) then return 1; elseif match(ip, sixbone, 16) then return 1; elseif match(ip, defaultunicast, 96) then return 1; elseif match(ip, ipv6mapped, 96) then return 35; else return 40; end end function ip_methods:toV4mapped() if self.proto ~= "IPv4" then return nil, "No IPv4 address" end local value = new_ip("::ffff:" .. self.normal); return value; end function ip_methods:label() if self.proto == "IPv4" then return label(self.toV4mapped); else return label(self); end end function ip_methods:precedence() if self.proto == "IPv4" then return precedence(self.toV4mapped); else return precedence(self); end end function ip_methods:scope() if self.proto == "IPv4" then return v4scope(self); else return v6scope(self); end end local rfc1918_8 = new_ip("10.0.0.0"); local rfc1918_12 = new_ip("172.16.0.0"); local rfc1918_16 = new_ip("192.168.0.0"); local rfc6598 = new_ip("100.64.0.0"); function ip_methods:private() local private = self.scope ~= 0xE; if not private and self.proto == "IPv4" then return match(self, rfc1918_8, 8) or match(self, rfc1918_12, 12) or match(self, rfc1918_16, 16) or match(self, rfc6598, 10); end return private; end local function parse_cidr(cidr) local bits; local ip_len = cidr:find("/", 1, true); if ip_len then bits = tonumber(cidr:sub(ip_len+1, -1)); cidr = cidr:sub(1, ip_len-1); end return new_ip(cidr), bits; end function match(ipA, ipB, bits) if not bits or bits >= 128 or ipB.proto == "IPv4" and bits >= 32 then return ipA == ipB; elseif bits < 1 then return true; end if ipA.proto ~= ipB.proto then if ipA.proto == "IPv4" then ipA = ipA.toV4mapped; elseif ipB.proto == "IPv4" then ipB = ipB.toV4mapped; bits = bits + (128 - 32); end end 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 { new_ip = new_ip, commonPrefixLength = commonPrefixLength, parse_cidr = parse_cidr, match = match, is_ip = is_ip; truncate = truncate; }; prosody-13.0.1/util/PaxHeaders/iterators.lua0000644000000000000000000000011714773555365016074 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.859711602 prosody-13.0.1/util/iterators.lua0000644000175000017500000001202014773555365020266 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- --[[ Iterators ]]-- local it = {}; local t_insert = table.insert; local next = next; local unpack = table.unpack; local pack = table.pack; local type = type; local table, setmetatable = table, setmetatable; local _ENV = nil; --luacheck: std none -- Reverse an iterator function it.reverse(f, s, var) local results = {}; -- First call the normal iterator while true do local ret = { f(s, var) }; var = ret[1]; if var == nil then break; end t_insert(results, 1, ret); end -- Then return our reverse one local i,max = 0, #results; return function (_results) if i= n then return nil; end c = c + 1; return f(_s, _var); end, s, var; end -- Skip the first n items an iterator returns function it.skip(n, f, s, var) for _ = 1, n do var = f(s, var); end return f, s, var; end -- Return the last n items an iterator returns function it.tail(n, f, s, var) local results, count = {}, 0; while true do local ret = pack(f(s, var)); var = ret[1]; if var == nil then break; end results[(count%n)+1] = ret; count = count + 1; end if n > count then n = count; end local pos = 0; return function () pos = pos + 1; if pos > n then return nil; end local ret = results[((count-1+pos)%n)+1]; return unpack(ret, 1, ret.n); end --return reverse(head(n, reverse(f, s, var))); -- ! end function it.filter(filter, f, s, var) if type(filter) ~= "function" then local filter_value = filter; function filter(x) return x ~= filter_value; end end return function (_s, _var) local ret; repeat ret = pack(f(_s, _var)); _var = ret[1]; until _var == nil or filter(unpack(ret, 1, ret.n)); return unpack(ret, 1, ret.n); end, s, var; end local function _ripairs_iter(t, key) if key > 1 then return key-1, t[key-1]; end end function it.ripairs(t) return _ripairs_iter, t, #t+1; end local function _range_iter(max, curr) if curr < max then return curr + 1; end end function it.range(x, y) if not y then x, y = 1, x; end -- Default to 1..x if y not given return _range_iter, y, x-1; end -- Convert the values returned by an iterator to an array function it.to_array(f, s, var) local t = {}; while true do var = f(s, var); if var == nil then break; end t_insert(t, var); end return t; end function it.sorted_pairs(t, sort_func) local keys = it.to_array(it.keys(t)); table.sort(keys, sort_func); local i = 0; return function () i = i + 1; local key = keys[i]; if key ~= nil then return key, t[key]; end end; end -- Treat the return of an iterator as key,value pairs, -- and build a table function it.to_table(f, s, var) local t, var2 = {}; while true do var, var2 = f(s, var); if var == nil then break; end t[var] = var2; end return t; end local function _join_iter(j_s, j_var) local iterators, current_idx = j_s[1], j_s[2]; local f, s, var = unpack(iterators[current_idx], 1, 3); if j_var ~= nil then var = j_var; end local ret = pack(f(s, var)); local var1 = ret[1]; if var1 == nil then -- End of this iterator, advance to next if current_idx == #iterators then -- No more iterators, return nil return; end j_s[2] = current_idx + 1; return _join_iter(j_s); end return unpack(ret, 1, ret.n); end local join_methods = {}; local join_mt = { __index = join_methods; __call = function (t, s, var) --luacheck: ignore 212/t return _join_iter(s, var); end; }; function join_methods:append(f, s, var) table.insert(self, { f, s, var }); return self, { self, 1 }; end function join_methods:prepend(f, s, var) table.insert(self, { f, s, var }, 1); return self, { self, 1 }; end function it.join(f, s, var) local t = setmetatable({ {f, s, var} }, join_mt); return t, { t, 1 }; end return it; prosody-13.0.1/util/PaxHeaders/jid.lua0000644000000000000000000000011714773555365014626 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.859711602 prosody-13.0.1/util/jid.lua0000644000175000017500000000653014773555365017031 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local select = select; local match, sub = string.match, string.sub; 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"; ["&"] = "\\26"; ["'"] = "\\27"; ["/"] = "\\2f"; [":"] = "\\3a"; ["<"] = "\\3c"; [">"] = "\\3e"; ["@"] = "\\40"; ["\\"] = "\\5c"; }; local unescapes = {}; local backslash_escapes = {}; for k,v in pairs(escapes) do unescapes[v] = k; backslash_escapes[v] = v:gsub("\\", escapes) end local _ENV = nil; -- luacheck: std none local function split(jid) if jid == nil then return; end local node, nodepos = match(jid, "^([^@/]+)@()"); local host, hostpos = match(jid, "^([^@/]+)()", nodepos); 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 local function bare(jid) local node, host = split(jid); if node ~= nil and host ~= nil then return node.."@"..host; end return host; end local function prepped_split(jid, strict) local node, host, resource = split(jid); if host ~= nil and host ~= "." then if sub(host, -1, -1) == "." then -- Strip empty root label host = sub(host, 1, -2); end host = nameprep(host, strict); if host == nil then return; end if node ~= nil then node = nodeprep(node, strict); if node == nil then return; end end if resource ~= nil then resource = resourceprep(resource, strict); if resource == nil then return; end end return node, host, resource; end end local function join(node, host, resource) if host == nil then return end if node ~= nil and resource ~= nil then return node.."@"..host.."/"..resource; elseif node ~= nil then return node.."@"..host; elseif resource ~= nil then return host.."/"..resource; end return host; end local function prep(jid, strict) local node, host, resource = prepped_split(jid, strict); return join(node, host, resource); end local function compare(jid, acl) -- compare jid to single acl rule -- 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 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 end local function node(jid) return (select(1, split(jid))); end local function host(jid) return (select(2, split(jid))); end 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 return { split = split; bare = bare; prepped_split = prepped_split; join = join; prep = prep; compare = compare; node = node; host = host; resource = resource; escape = escape; unescape = unescape; }; prosody-13.0.1/util/PaxHeaders/json.lua0000644000000000000000000000011714773555365015031 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.859711602 prosody-13.0.1/util/json.lua0000644000175000017500000002366714773555365017246 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 "prosody.util.iterators".sorted_pairs; local next = next; local getmetatable, setmetatable = getmetatable, setmetatable; local print = print; local has_array, array = pcall(require, "prosody.util.array"); local array_mt = has_array and getmetatable(array()) or {}; --module("json") local module = {}; local null = setmetatable({}, { __tostring = function() return "null"; end; }); module.null = null; local escapes = { ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"}; for i=0,31 do local ch = s_char(i); if not escapes[ch] then escapes[ch] = ("\\u%.4X"):format(i); end end local function codepoint_to_utf8(code) if code < 0x80 then return s_char(code); end local bits0_6 = code % 64; if code < 0x800 then local bits6_5 = (code - bits0_6) / 64; return s_char(0x80 + 0x40 + bits6_5, 0x80 + bits0_6); end local bits0_12 = code % 4096; local bits6_6 = (bits0_12 - bits0_6) / 64; local bits12_4 = (code - bits0_12) / 4096; return s_char(0x80 + 0x40 + 0x20 + bits12_4, 0x80 + bits6_6, 0x80 + bits0_6); end local valid_types = { number = true, string = true, table = true, boolean = true }; local special_keys = { __array = true; __hash = true; }; local simplesave, tablesave, arraysave, stringsave; function stringsave(o, buffer) -- FIXME do proper utf-8 and binary data detection t_insert(buffer, "\""..(o:gsub(".", escapes)).."\""); end function arraysave(o, buffer) t_insert(buffer, "["); if next(o) then for _, v in ipairs(o) do simplesave(v, buffer); t_insert(buffer, ","); end t_remove(buffer); end t_insert(buffer, "]"); end function tablesave(o, buffer) local __array = {}; local __hash = {}; local hash = {}; for i,v in ipairs(o) do __array[i] = v; end for k,v in pairs(o) do local ktype, vtype = type(k), type(v); if valid_types[vtype] or v == null then if ktype == "string" and not special_keys[k] then hash[k] = v; elseif (valid_types[ktype] or k == null) and __array[k] == nil then __hash[k] = v; end end end if next(__hash) ~= nil or next(hash) ~= nil or next(__array) == nil then t_insert(buffer, "{"); local mark = #buffer; local _pairs = buffer.ordered and spairs or pairs; for k,v in _pairs(hash) do stringsave(k, buffer); t_insert(buffer, ":"); simplesave(v, buffer); t_insert(buffer, ","); end if next(__hash) ~= nil then t_insert(buffer, "\"__hash\":["); for k,v in pairs(__hash) do simplesave(k, buffer); t_insert(buffer, ","); simplesave(v, buffer); t_insert(buffer, ","); end t_remove(buffer); t_insert(buffer, "]"); t_insert(buffer, ","); end if next(__array) then t_insert(buffer, "\"__array\":"); arraysave(__array, buffer); t_insert(buffer, ","); end if mark ~= #buffer then t_remove(buffer); end t_insert(buffer, "}"); else arraysave(__array, buffer); end end function simplesave(o, buffer) local t = type(o); if o == null then t_insert(buffer, "null"); elseif t == "number" then t_insert(buffer, tostring(o)); elseif t == "string" then stringsave(o, buffer); elseif t == "table" then local mt = getmetatable(o); if mt == array_mt then arraysave(o, buffer); else tablesave(o, buffer); end elseif t == "boolean" then t_insert(buffer, (o and "true" or "false")); else t_insert(buffer, "null"); end end function module.encode(obj) local t = {}; simplesave(obj, t); return t_concat(t); end function module.encode_ordered(obj) local t = { ordered = true }; simplesave(obj, t); return t_concat(t); end function module.encode_array(obj) local t = {}; arraysave(obj, t); return t_concat(t); end ----------------------------------- local function _skip_whitespace(json, index) return json:find("[^ \t\r\n]", index) or index; -- no need to check \r\n, we converted those to \t end local function _fixobject(obj) local __array = obj.__array; if __array then obj.__array = nil; for _, v in ipairs(__array) do t_insert(obj, v); end end local __hash = obj.__hash; if __hash then obj.__hash = nil; local k; for _, v in ipairs(__hash) do if k ~= nil then obj[k] = v; k = nil; else k = v; end end end return obj; end local _readvalue, _readstring; local function _readobject(json, index) local o = {}; while true do local key, val; index = _skip_whitespace(json, index + 1); if json:byte(index) ~= 0x22 then -- "\"" if json:byte(index) == 0x7d then return o, index + 1; end -- "}" return nil, "key expected"; end key, index = _readstring(json, index); if key == nil then return nil, index; end index = _skip_whitespace(json, index); if json:byte(index) ~= 0x3a then return nil, "colon expected"; end -- ":" val, index = _readvalue(json, index + 1); if val == nil then return nil, index; end o[key] = val; index = _skip_whitespace(json, index); local b = json:byte(index); if b == 0x7d then return _fixobject(o), index + 1; end -- "}" if b ~= 0x2c then return nil, "object eof"; end -- "," end end local function _readarray(json, index) local a = {}; while true do local val, terminated; val, index, terminated = _readvalue(json, index + 1, 0x5d); if val == nil then if terminated then -- "]" found instead of value if #a ~= 0 then -- A non-empty array here means we processed a comma, -- but it wasn't followed by a value. JSON doesn't allow -- trailing commas. return nil, "value expected"; end val, index = setmetatable(a, array_mt), index+1; end return val, index; end t_insert(a, val); index = _skip_whitespace(json, index); local b = json:byte(index); if b == 0x5d then return setmetatable(a, array_mt), index + 1; end -- "]" if b ~= 0x2c then return nil, "array eof"; end -- "," end end local _unescape_error; local function _unescape_surrogate_func(x) local lead, trail = tonumber(x:sub(3, 6), 16), tonumber(x:sub(9, 12), 16); local codepoint = lead * 0x400 + trail - 0x35FDC00; local a = codepoint % 64; codepoint = (codepoint - a) / 64; local b = codepoint % 64; codepoint = (codepoint - b) / 64; local c = codepoint % 64; codepoint = (codepoint - c) / 64; return s_char(0xF0 + codepoint, 0x80 + c, 0x80 + b, 0x80 + a); end local function _unescape_func(x) x = x:match("%x%x%x%x", 3); if x then local codepoint = tonumber(x, 16) if codepoint >= 0xD800 and codepoint <= 0xDFFF then _unescape_error = true; end -- bad surrogate pair return codepoint_to_utf8(codepoint); end _unescape_error = true; end function _readstring(json, index) index = index + 1; local endindex = json:find("\"", index, true); if endindex then local s = json:sub(index, endindex - 1); --if s:find("[%z-\31]") then return nil, "control char in string"; end -- FIXME handle control characters _unescape_error = nil; s = s:gsub("\\u[dD][89abAB]%x%x\\u[dD][cdefCDEF]%x%x", _unescape_surrogate_func); -- FIXME handle escapes beyond BMP s = s:gsub("\\u.?.?.?.?", _unescape_func); if _unescape_error then return nil, "invalid escape"; end return s, endindex + 1; end return nil, "string eof"; end local function _readnumber(json, index) local m = json:match("[0-9%.%-eE%+]+", index); -- FIXME do strict checking return tonumber(m), index + #m; end local function _readnull(json, index) local a, b, c = json:byte(index + 1, index + 3); if a == 0x75 and b == 0x6c and c == 0x6c then return null, index + 4; end return nil, "null parse failed"; end local function _readtrue(json, index) local a, b, c = json:byte(index + 1, index + 3); if a == 0x72 and b == 0x75 and c == 0x65 then return true, index + 4; end return nil, "true parse failed"; end local function _readfalse(json, index) local a, b, c, d = json:byte(index + 1, index + 4); if a == 0x61 and b == 0x6c and c == 0x73 and d == 0x65 then return false, index + 5; end return nil, "false parse failed"; end function _readvalue(json, index, terminator) index = _skip_whitespace(json, index); local b = json:byte(index); -- TODO try table lookup instead of if-else? if b == 0x7B then -- "{" return _readobject(json, index); elseif b == 0x5B then -- "[" return _readarray(json, index); elseif b == 0x22 then -- "\"" return _readstring(json, index); elseif b ~= nil and b >= 0x30 and b <= 0x39 or b == 0x2d then -- "0"-"9" or "-" return _readnumber(json, index); elseif b == 0x6e then -- "n" return _readnull(json, index); elseif b == 0x74 then -- "t" return _readtrue(json, index); elseif b == 0x66 then -- "f" return _readfalse(json, index); elseif b == terminator then return nil, index, true; else return nil, "value expected"; end end local first_escape = { ["\\\""] = "\\u0022"; ["\\\\"] = "\\u005c"; ["\\/" ] = "\\u002f"; ["\\b" ] = "\\u0008"; ["\\f" ] = "\\u000C"; ["\\n" ] = "\\u000A"; ["\\r" ] = "\\u000D"; ["\\t" ] = "\\u0009"; ["\\u" ] = "\\u"; }; function module.decode(json) json = json:gsub("\\.", first_escape) -- get rid of all escapes except \uXXXX, making string parsing much simpler --:gsub("[\r\n]", "\t"); -- \r\n\t are equivalent, we care about none of them, and none of them can be in strings -- TODO do encoding verification local val, index = _readvalue(json, 1); if val == nil then return val, index; end if json:find("[^ \t\r\n]", index) then return nil, "garbage at eof"; end return val; end function module.test(object) local encoded = module.encode(object); local decoded = module.decode(encoded); local recoded = module.encode(decoded); if encoded ~= recoded then print("FAILED"); print("encoded:", encoded); print("recoded:", recoded); else print(encoded); end return encoded == recoded; end return module; prosody-13.0.1/util/PaxHeaders/jsonpointer.lua0000644000000000000000000000011714773555365016432 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.859711602 prosody-13.0.1/util/jsonpointer.lua0000644000175000017500000000160214773555365020630 0ustar00prosodyprosody00000000000000local m_type = math.type; local function unescape_token(escaped_token) local unescaped = escaped_token:gsub("~1", "/"):gsub("~0", "~") return unescaped end local function resolve_json_pointer(ref, path) local ptr_len = #path + 1 for part, pos in path:gmatch("/([^/]*)()") do local token = unescape_token(part) if not (type(ref) == "table") then return nil end local idx = next(ref) local new_ref if type(idx) == "string" then new_ref = ref[token] elseif m_type(idx) == "integer" then local i = tonumber(token) if token == "-" then i = #ref + 1 end new_ref = ref[i + 1] else return nil, "invalid-table" end if pos == ptr_len then return new_ref elseif type(new_ref) == "table" then ref = new_ref elseif not (type(ref) == "table") then return nil, "invalid-path" end end return ref end return { resolve = resolve_json_pointer } prosody-13.0.1/util/PaxHeaders/jsonschema.lua0000644000000000000000000000011714773555365016212 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.863711619 prosody-13.0.1/util/jsonschema.lua0000644000175000017500000002546514773555365020425 0ustar00prosodyprosody00000000000000-- This file is generated from teal-src/util/jsonschema.lua if not math.type then require("prosody.util.mathcompat") end local utf8_enc = rawget(_G, "utf8") or require("prosody.util.encodings").utf8; local utf8_len = utf8_enc.len or function(s) local _, count = s:gsub("[%z\001-\127\194-\253][\128-\191]*", ""); return count end; local json = require("prosody.util.json") local null = json.null; local pointer = require("prosody.util.jsonpointer") local json_schema_object = { xml_t = {} } local function simple_validate(schema, data) if schema == nil then return true elseif schema == "object" and type(data) == "table" then return type(data) == "table" and (next(data) == nil or type((next(data, nil))) == "string") 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 math.type(data) == schema elseif schema == "null" then return data == null elseif type(schema) == "table" then for _, one in ipairs(schema) do if simple_validate(one, data) then return true end end return false else return type(data) == schema end end local function mkerr(sloc, iloc, err) return { schemaLocation = sloc; instanceLocation = iloc; error = err } end local function validate(schema, data, root, sloc, iloc, errs) if type(schema) == "boolean" then return schema end 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, 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 type(data) == "table" then for k in pairs(data) do if not (type(k) == "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 type(data) == "table" then for i in pairs(data) do 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 end if schema["enum"] ~= nil then local match = false for _, v in ipairs(schema["enum"]) do if v == data then 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 if type(data) == "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 type(data) == "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 type(data) == "table" then 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 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 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 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, 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 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 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) 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) 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 prosody-13.0.1/util/PaxHeaders/jwt.lua0000644000000000000000000000011714773555365014664 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.863711619 prosody-13.0.1/util/jwt.lua0000644000175000017500000001600514773555365017065 0ustar00prosodyprosody00000000000000local s_gsub = string.gsub; 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) return (s_gsub(base64_encode(data), "[+/=]", b64url_rep)); end local function unb64url(data) return base64_decode(s_gsub(data, "[-_]", b64url_rep).."=="); end local jwt_pattern = "^(([A-Za-z0-9-_]+)%.([A-Za-z0-9-_]+))%.([A-Za-z0-9-_]+)$" 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"; end local header = json.decode(unb64url(bheader)); if not header or type(header) ~= "table" then return nil, "invalid-header"; elseif header.alg ~= expected_alg then return nil, "unsupported-algorithm"; end 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 { 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; }; prosody-13.0.1/util/PaxHeaders/logger.lua0000644000000000000000000000011714773555365015337 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.863711619 prosody-13.0.1/util/logger.lua0000644000175000017500000000472014773555365017541 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: ignore 213/level local pairs = pairs; local ipairs = ipairs; local require = require; local t_remove = table.remove; local _ENV = nil; -- luacheck: std none local level_sinks = {}; local make_logger; local function init(name) local log_debug = make_logger(name, "debug"); local log_info = make_logger(name, "info"); local log_warn = make_logger(name, "warn"); local log_error = make_logger(name, "error"); return function (level, message, ...) if level == "debug" then return log_debug(message, ...); elseif level == "info" then return log_info(message, ...); elseif level == "warn" then return log_warn(message, ...); elseif level == "error" then return log_error(message, ...); end end end function make_logger(source_name, level) local level_handlers = level_sinks[level]; if not level_handlers then level_handlers = {}; level_sinks[level] = level_handlers; end local logger = function (message, ...) for i = 1,#level_handlers do level_handlers[i](source_name, level, message, ...); end end return logger; end local function reset() for level, handler_list in pairs(level_sinks) do -- Clear all handlers for this level for i = 1, #handler_list do handler_list[i] = nil; end end end local function add_level_sink(level, sink_function) if not level_sinks[level] then level_sinks[level] = { sink_function }; else level_sinks[level][#level_sinks[level] + 1 ] = sink_function; end end local function add_simple_sink(simple_sink_function, levels) 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 { init = init; make_logger = make_logger; reset = reset; add_level_sink = add_level_sink; add_simple_sink = add_simple_sink; new = make_logger; remove_sink = remove_sink; }; prosody-13.0.1/util/PaxHeaders/mathcompat.lua0000644000000000000000000000011714773555365016215 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.863711619 prosody-13.0.1/util/mathcompat.lua0000644000175000017500000000034614773555365020417 0ustar00prosodyprosody00000000000000if 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 prosody-13.0.1/util/PaxHeaders/mercurial.lua0000644000000000000000000000011714773555365016043 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.863711619 prosody-13.0.1/util/mercurial.lua0000644000175000017500000000174514773555365020251 0ustar00prosodyprosody00000000000000 local lfs = require"lfs"; local hg = { }; function hg.check_id(path) if lfs.attributes(path, 'mode') ~= "directory" then return nil, "not a directory"; end local hg_dirstate = io.open(path.."/.hg/dirstate"); local hgid, hgrepo if hg_dirstate then hgid = ("%02x%02x%02x%02x%02x%02x"):format(hg_dirstate:read(6):byte(1, 6)); hg_dirstate:close(); local hg_changelog = io.open(path.."/.hg/store/00changelog.i"); if hg_changelog then hg_changelog:seek("set", 0x20); hgrepo = ("%02x%02x%02x%02x%02x%02x"):format(hg_changelog:read(6):byte(1, 6)); hg_changelog:close(); end else local hg_archival,e = io.open(path.."/.hg_archival.txt"); -- luacheck: ignore 211/e if hg_archival then local repo = hg_archival:read("*l"); local node = hg_archival:read("*l"); hg_archival:close() hgid = node and node:match("^node: (%x%x%x%x%x%x%x%x%x%x%x%x)") hgrepo = repo and repo:match("^repo: (%x%x%x%x%x%x%x%x%x%x%x%x)") end end return hgid, hgrepo; end return hg; prosody-13.0.1/util/PaxHeaders/multitable.lua0000644000000000000000000000011714773555365016222 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.863711619 prosody-13.0.1/util/multitable.lua0000644000175000017500000000715414773555365020430 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local select = select; local t_insert = table.insert; local pairs, next, type = pairs, next, type; local unpack = table.unpack; local _ENV = nil; -- luacheck: std none local function get(self, ...) local t = self.data; for n = 1,select('#', ...) do t = t[select(n, ...)]; if not t then break; end end return t; end local function add(self, ...) local t = self.data; local count = select('#', ...); for n = 1,count-1 do local key = select(n, ...); local tab = t[key]; if not tab then tab = {}; t[key] = tab; end t = tab; end t_insert(t, (select(count, ...))); end local function set(self, ...) local t = self.data; local count = select('#', ...); for n = 1,count-2 do local key = select(n, ...); local tab = t[key]; if not tab then tab = {}; t[key] = tab; end t = tab; end t[(select(count-1, ...))] = (select(count, ...)); end local function r(t, n, _end, ...) if t == nil then return; end local k = select(n, ...); if n == _end then t[k] = nil; return; end if k then local v = t[k]; if v then r(v, n+1, _end, ...); if not next(v) then t[k] = nil; end end else for _,b in pairs(t) do r(b, n+1, _end, ...); if not next(b) then t[_] = nil; end end end end local function remove(self, ...) local _end = select('#', ...); for n = _end,1 do if select(n, ...) then _end = n; break; end end r(self.data, 1, _end, ...); end local function s(t, n, results, _end, ...) if t == nil then return; end local k = select(n, ...); if n == _end then if k == nil then for _, v in pairs(t) do t_insert(results, v); end else t_insert(results, t[k]); end return; end if k then local v = t[k]; if v then s(v, n+1, results, _end, ...); end else for _,b in pairs(t) do s(b, n+1, results, _end, ...); end end end -- Search for keys, nil == wildcard local function search(self, ...) local _end = select('#', ...); for n = _end,1 do if select(n, ...) then _end = n; break; end end local results = {}; s(self.data, 1, results, _end, ...); return results; end -- Append results to an existing list local function search_add(self, results, ...) if not results then results = {}; end local _end = select('#', ...); for n = _end,1 do if select(n, ...) then _end = n; break; end end s(self.data, 1, results, _end, ...); return results; end local function iter(self, ...) local query = { ... }; local maxdepth = select("#", ...); local stack = { self.data }; local keys = { }; local function it(self) -- luacheck: ignore 432/self local depth = #stack; local key = next(stack[depth], keys[depth]); if key == nil then -- Go up the stack stack[depth], keys[depth] = nil, nil; if depth > 1 then return it(self); end return; -- The end else keys[depth] = key; end local value = stack[depth][key]; if query[depth] == nil or key == query[depth] then if depth == maxdepth then -- Result local result = {}; -- Collect keys forming path to result for i = 1, depth do result[i] = keys[i]; end result[depth+1] = value; return unpack(result, 1, depth+1); elseif type(value) == "table" then t_insert(stack, value); -- Descend end end return it(self); end; return it, self; end local function new() return { data = {}; get = get; add = add; set = set; remove = remove; search = search; search_add = search_add; iter = iter; }; end return { iter = iter; new = new; }; prosody-13.0.1/util/PaxHeaders/openmetrics.lua0000644000000000000000000000011714773555365016410 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.863711619 prosody-13.0.1/util/openmetrics.lua0000644000175000017500000002636514773555365020623 0ustar00prosodyprosody00000000000000--[[ This module implements a subset of the OpenMetrics Internet Draft version 00. URL: https://datatracker.ietf.org/doc/html/draft-richih-opsawg-openmetrics-00 The following metric types are supported: - Counter - Gauge - Histogram - Summary It is used by util.statsd and util.statistics to provide the OpenMetrics API. To understand what this module is about, it is useful to familiarize oneself with the terms MetricFamily, Metric, LabelSet, Label and MetricPoint as defined in the I-D linked above. --]] -- metric constructor interface: -- metric_ctor(..., family_name, labels, extra) local time = require "prosody.util.time".now; local select = select; 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 = table.pack, table.unpack; -- BEGIN of Utility: "metric proxy" -- This allows to wrap a MetricFamily in a proxy which only provides the -- `with_labels` and `with_partial_label` methods. This allows to pre-set one -- or more labels on a metric family. This is used in particular via -- `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) return setmetatable({ _family = metric_family, with_labels = function(self, ...) return with_labels_proxy_fun(self._family, ...) end; with_partial_label = function(self, label) return new_metric_proxy(self._family, function(family, ...) return family:with_labels(label, ...) end) end }, metric_proxy_mt); end -- END of Utility: "metric proxy" -- BEGIN Rendering helper functions (internal) local function escape(text) return text:gsub("\\", "\\\\"):gsub("\"", "\\\""):gsub("\n", "\\n"); end local function escape_name(name) return name:gsub("/", "__"):gsub("[^A-Za-z0-9_]", "_"):gsub("^[^A-Za-z_]", "_%1"); end local function repr_help(metric, docstring) docstring = docstring:gsub("\\", "\\\\"):gsub("\n", "\\n"); return "# HELP "..escape_name(metric).." "..docstring.."\n"; end local function repr_unit(metric, unit) if not unit then unit = "" else unit = unit:gsub("\\", "\\\\"):gsub("\n", "\\n"); end return "# UNIT "..escape_name(metric).." "..unit.."\n"; end -- local allowed_types = { counter = true, gauge = true, histogram = true, summary = true, untyped = true }; -- local allowed_types = { "counter", "gauge", "histogram", "summary", "untyped" }; local function repr_type(metric, type_) -- if not allowed_types:contains(type_) then -- return; -- end return "# TYPE "..escape_name(metric).." "..type_.."\n"; end local function repr_label(key, value) return key.."=\""..escape(value).."\""; end local function repr_labels(labelkeys, labelvalues, extra_labels) local values = {} if labelkeys then for i, key in ipairs(labelkeys) do local value = labelvalues[i] t_insert(values, repr_label(escape_name(key), escape(value))); end end if extra_labels then for key, value in pairs(extra_labels) do t_insert(values, repr_label(escape_name(key), escape(value))); end end if #values == 0 then return ""; end return "{"..t_concat(values, ",").."}"; end local function repr_sample(metric, labelkeys, labelvalues, extra_labels, value) return escape_name(metric)..repr_labels(labelkeys, labelvalues, extra_labels).." "..string.format("%.17g", value).."\n"; end -- END Rendering helper functions (internal) local function render_histogram_le(v) if v == 1/0 then -- I-D-00: 4.1.2.2.1: -- Exposers MUST produce output for positive infinity as +Inf. return "+Inf" end return string.format("%.14g", v) 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) return function(family_name, labels, extra) return orig_ctor(buckets, family_name, labels, extra) end end local function new_metric_family(backend, type_, family_name, unit, description, label_keys, extra) local metric_ctor = assert(backend[type_], "statistics backend does not support "..type_.." metrics families") local labels = label_keys or {} local user_labels = #labels if type_ == "histogram" then local buckets = extra and extra.buckets if not buckets then error("no buckets given for histogram metric") end buckets = array(buckets) buckets:push(1/0) -- must have +inf bucket metric_ctor = histogram_metric_ctor(metric_ctor, buckets) end local data if #labels == 0 then data = metric_ctor(family_name, nil, extra) else data = new_multitable() end local mf = { family_name = family_name, data = data, type_ = type_, unit = unit, description = description, user_labels = user_labels, label_keys = labels, extra = extra, _metric_ctor = metric_ctor, } setmetatable(mf, metric_family_mt); return mf end function metric_family_mt:new_metric(labels) return self._metric_ctor(self.family_name, labels, self.extra) end function metric_family_mt:clear() for _, metric in self:iter_metrics() do metric:reset() end end function metric_family_mt:with_labels(...) local count = select('#', ...) if count ~= self.user_labels then error("number of labels passed to with_labels does not match number of label keys") end if count == 0 then return self.data end local metric = self.data:get(...) if not metric then local values = t_pack(...) metric = self:new_metric(values) values[values.n+1] = metric self.data:set(t_unpack(values, 1, values.n+1)) end return metric end function metric_family_mt:with_partial_label(label) return new_metric_proxy(self, function (family, ...) return family:with_labels(label, ...) end) end function metric_family_mt:iter_metrics() if #self.label_keys == 0 then local done = false return function() if done then return nil end done = true return {}, self.data end end local searchkeys = {}; local nlabels = #self.label_keys for i=1,nlabels do searchkeys[i] = nil; end local it, state = iter_multitable(self.data, t_unpack(searchkeys, 1, nlabels)) return function(_s) local label_values = t_pack(it(_s)) if label_values.n == 0 then return nil, nil end local metric = label_values[label_values.n] label_values[label_values.n] = nil label_values.n = label_values.n - 1 return label_values, metric end, state end -- END of generic MetricFamily implementation -- BEGIN of MetricRegistry implementation -- Helper to test whether two metrics are "equal". local function equal_metric_family(mf1, mf2) if mf1.type_ ~= mf2.type_ then return false end if #mf1.label_keys ~= #mf2.label_keys then return false end -- Ignoring unit here because in general it'll be part of the name anyway -- So either the unit was moved into/out of the name (which is a valid) -- thing to do on an upgrade or we would expect not to see any conflicts -- anyway. --[[ if mf1.unit ~= mf2.unit then return false end ]] for i, key in ipairs(mf1.label_keys) do if key ~= mf2.label_keys[i] then return false end end return true end -- If the unit is not empty, add it to the full name as per the I-D spec. local function compose_name(name, unit) local full_name = name if unit and unit ~= "" then full_name = full_name .. "_" .. unit end -- TODO: prohibit certain suffixes used by metrics if where they may cause -- conflicts return full_name end local metric_registry_mt = {} metric_registry_mt.__name = "metric_registry" metric_registry_mt.__index = metric_registry_mt local function new_metric_registry(backend) local reg = { families = {}, backend = backend, } setmetatable(reg, metric_registry_mt) return reg end function metric_registry_mt:register_metric_family(name, metric_family) local existing = self.families[name]; if existing then if not equal_metric_family(metric_family, existing) then -- We could either be strict about this, or replace the -- existing metric family with the new one. -- Being strict is nice to avoid programming errors / -- conflicts, but causes issues when a new version of a module -- is loaded. -- -- We will thus assume that the new metric is the correct one; -- That is probably OK because unless you're reaching down into -- the util.openmetrics or core.statsmanager API, your metric -- name is going to be scoped to `prosody_mod_$modulename` -- anyway and the damage is thus controlled. -- -- To make debugging such issues easier, we still log. log("debug", "replacing incompatible existing metric family %s", name) -- Below is the code to be strict. --error("conflicting declarations for metric family "..name) else return existing end end self.families[name] = metric_family return metric_family end function metric_registry_mt:gauge(name, unit, description, labels, extra) name = compose_name(name, unit) local mf = new_metric_family(self.backend, "gauge", name, unit, description, labels, extra) mf = self:register_metric_family(name, mf) return mf end function metric_registry_mt:counter(name, unit, description, labels, extra) name = compose_name(name, unit) local mf = new_metric_family(self.backend, "counter", name, unit, description, labels, extra) mf = self:register_metric_family(name, mf) return mf end function metric_registry_mt:histogram(name, unit, description, labels, extra) name = compose_name(name, unit) local mf = new_metric_family(self.backend, "histogram", name, unit, description, labels, extra) mf = self:register_metric_family(name, mf) return mf end function metric_registry_mt:summary(name, unit, description, labels, extra) name = compose_name(name, unit) local mf = new_metric_family(self.backend, "summary", name, unit, description, labels, extra) mf = self:register_metric_family(name, mf) return mf end function metric_registry_mt:get_metric_families() return self.families end function metric_registry_mt:render() local answer = {}; for metric_family_name, metric_family in pairs(self:get_metric_families()) do t_insert(answer, repr_help(metric_family_name, metric_family.description)) t_insert(answer, repr_unit(metric_family_name, metric_family.unit)) t_insert(answer, repr_type(metric_family_name, metric_family.type_)) for labelset, metric in metric_family:iter_metrics() do for suffix, extra_labels, value in metric:iter_samples() do t_insert(answer, repr_sample(metric_family_name..suffix, metric_family.label_keys, labelset, extra_labels, value)) end end end t_insert(answer, "# EOF\n") return t_concat(answer, ""); end -- END of MetricRegistry implementation -- BEGIN of general helpers for implementing high-level APIs on top of OpenMetrics local function timed(metric) local t0 = time() local submitter = assert(metric.sample or metric.set, "metric type cannot be used with timed()") return function() local t1 = time() submitter(metric, t1-t0) end end -- END of general helpers return { new_metric_proxy = new_metric_proxy; new_metric_registry = new_metric_registry; render_histogram_le = render_histogram_le; timed = timed; } prosody-13.0.1/util/PaxHeaders/openssl.lua0000644000000000000000000000011714773555365015543 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.867711635 prosody-13.0.1/util/openssl.lua0000644000175000017500000001100514773555365017737 0ustar00prosodyprosody00000000000000local type, tostring, pairs, ipairs = type, tostring, pairs, ipairs; local t_insert, t_concat = table.insert, table.concat; 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 "prosody.util.encodings".idna.to_ascii; local _M = {}; local config = {}; _M.config = config; local ssl_config = {}; local ssl_config_mt = { __index = ssl_config }; function config.new() return setmetatable({ req = { distinguished_name = "distinguished_name", req_extensions = "certrequest", x509_extensions = "selfsigned", prompt = "no", }, distinguished_name = { countryName = "GB", -- stateOrProvinceName = "", localityName = "The Internet", organizationName = "Your Organisation", organizationalUnitName = "XMPP Department", commonName = "example.com", emailAddress = "xmpp@example.com", }, certrequest = { basicConstraints = "CA:FALSE", keyUsage = "digitalSignature,keyEncipherment", extendedKeyUsage = "serverAuth,clientAuth", subjectAltName = "@subject_alternative_name", }, selfsigned = { basicConstraints = "CA:TRUE", subjectAltName = "@subject_alternative_name", }, subject_alternative_name = { DNS = {}, otherName = {}, }, }, ssl_config_mt); end local DN_order = { "countryName"; "stateOrProvinceName"; "localityName"; "streetAddress"; "organizationName"; "organizationalUnitName"; "commonName"; "emailAddress"; } _M._DN_order = DN_order; function ssl_config:serialize() local s = ""; for section, t in pairs(self) do s = s .. ("[%s]\n"):format(section); if section == "subject_alternative_name" then for san, n in pairs(t) do for i = 1, #n do s = s .. s_format("%s.%d = %s\n", san, i -1, n[i]); end end elseif section == "distinguished_name" then for _, k in ipairs(t[1] and t or DN_order) do local v = t[k]; if v then s = s .. ("%s = %s\n"):format(k, v); end end else for k, v in pairs(t) do s = s .. ("%s = %s\n"):format(k, v); end end s = s .. "\n"; end return s; end local function utf8string(s) -- This is how we tell openssl not to encode UTF-8 strings as fake Latin1 return s_format("FORMAT:UTF8,UTF8:%s", s); end local function ia5string(s) return s_format("IA5STRING:%s", s); end _M.util = { utf8string = utf8string, ia5string = ia5string, }; function ssl_config:add_dNSName(host) t_insert(self.subject_alternative_name.DNS, idna_to_ascii(host)); end function ssl_config:add_sRVName(host, service) t_insert(self.subject_alternative_name.otherName, s_format("%s;%s", oid_dnssrv, ia5string("_" .. service .. "." .. idna_to_ascii(host)))); end function ssl_config:add_xmppAddr(host) t_insert(self.subject_alternative_name.otherName, s_format("%s;%s", oid_xmppaddr, utf8string(host))); end function ssl_config:from_prosody(hosts, config, certhosts) -- luacheck: ignore 431/config -- TODO Decide if this should go elsewhere local found_matching_hosts = false; for i = 1, #certhosts do local certhost = certhosts[i]; for name in pairs(hosts) do if name == certhost or name:sub(-1-#certhost) == "." .. certhost then found_matching_hosts = true; self:add_dNSName(name); --print(name .. "#component_module: " .. (config.get(name, "component_module") or "nil")); if config.get(name, "component_module") == nil then self:add_sRVName(name, "xmpp-client"); end --print(name .. "#anonymous_login: " .. tostring(config.get(name, "anonymous_login"))); if not (config.get(name, "anonymous_login") or config.get(name, "authentication") == "anonymous") then self:add_sRVName(name, "xmpp-server"); end self:add_xmppAddr(name); end end end if not found_matching_hosts then return nil, "no-matching-hosts"; end end do -- Lua to shell calls. local function shell_escape(s) return "'" .. tostring(s):gsub("'",[['\'']]) .. "'"; end local function serialize(command, args) local commandline = { "openssl", command }; for k, v in pairs(args) do if type(k) == "string" then t_insert(commandline, ("-%s"):format(k)); if v ~= true then t_insert(commandline, shell_escape(v)); end end end for _, v in ipairs(args) do t_insert(commandline, shell_escape(v)); end return t_concat(commandline, " "); end local os_execute = os.execute; setmetatable(_M, { __index = function(_, command) return function(opts) return os_execute(serialize(command, type(opts) == "table" and opts or {})); end; end; }); end return _M; prosody-13.0.1/util/PaxHeaders/paseto.lua0000644000000000000000000000011714773555365015353 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.867711635 prosody-13.0.1/util/paseto.lua0000644000175000017500000001506214773555365017556 0ustar00prosodyprosody00000000000000local 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(" string -- Optimization: Avoid creating table for most uses if b then if c then if ... then return t_concat({a,b,c,...}, path_sep); end return a..path_sep..b..path_sep..c; end return a..path_sep..b; end return a; end function path_util.complement_lua_path(installer_plugin_path) -- Checking for duplicates -- The commands using luarocks need the path to the directory that has the /share and /lib folders. local lua_version = _VERSION:match(" (.+)$"); local lua_path_sep = package.config:sub(3,3); local dir_sep = package.config:sub(1,1); local sub_path = dir_sep.."lua"..dir_sep..lua_version..dir_sep; if not string.find(package.path, installer_plugin_path, 1, true) then package.path = package.path..lua_path_sep..installer_plugin_path..dir_sep.."share"..sub_path.."?.lua"; package.path = package.path..lua_path_sep..installer_plugin_path..dir_sep.."share"..sub_path.."?"..dir_sep.."init.lua"; end if not string.find(package.path, installer_plugin_path, 1, true) then package.cpath = package.cpath..lua_path_sep..installer_plugin_path..dir_sep.."lib"..sub_path.."?.so"; end end return path_util; prosody-13.0.1/util/PaxHeaders/pluginloader.lua0000644000000000000000000000011714773555365016545 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.867711635 prosody-13.0.1/util/pluginloader.lua0000644000175000017500000000657014773555365020754 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- luacheck: ignore 113/CFG_PLUGINDIR local dir_sep, path_sep = package.config:match("^(%S+)%s(%S+)"); local lua_version = _VERSION:match(" (.+)$"); local plugin_dir = {}; for path in (CFG_PLUGINDIR or "./plugins/"):gsub("[/\\]", dir_sep):gmatch("[^"..path_sep.."]+") do path = path..dir_sep; -- add path separator to path end path = path:gsub(dir_sep..dir_sep.."+", dir_sep); -- coalesce multiple separators plugin_dir[#plugin_dir + 1] = path; end local io_open = io.open; local envload = require "prosody.util.envload".envload; local pluginloader_methods = {}; local pluginloader_mt = { __index = pluginloader_methods }; function pluginloader_methods:load_file(names) local file, err, path; local load_filter_cb = self._options.load_filter_cb; for i=1,#plugin_dir do for j=1,#names do path = plugin_dir[i]..names[j]; file, err = io_open(path); if file then local content = file:read("*a"); file:close(); local metadata; if load_filter_cb then path, content, metadata = load_filter_cb(path, content); end if content and path then return content, path, metadata; end end end end return file, err; end function pluginloader_methods:load_resource(plugin, resource) resource = resource or "mod_"..plugin..".lua"; local names = { "mod_"..plugin..dir_sep..plugin..dir_sep..resource; -- mod_hello/hello/mod_hello.lua "mod_"..plugin..dir_sep..resource; -- mod_hello/mod_hello.lua plugin..dir_sep..resource; -- hello/mod_hello.lua resource; -- mod_hello.lua "share"..dir_sep.."lua"..dir_sep..lua_version..dir_sep..resource; "share"..dir_sep.."lua"..dir_sep..lua_version..dir_sep.."mod_"..plugin..dir_sep..resource; }; return self:load_file(names); end function pluginloader_methods:load_code(plugin, resource, env) local content, err, metadata = self:load_resource(plugin, resource); if not content then return content, err; end local path = err; local f, err = envload(content, "@"..path, env); if not f then return f, err; end return f, path, metadata; end function pluginloader_methods:load_code_ext(plugin, resource, extension, env) local content, err, metadata = self:load_resource(plugin, resource.."."..extension); if not content and extension == "lib.lua" then content, err, metadata = self:load_resource(plugin, resource..".lua"); end if not content then content, err, metadata = self:load_resource(resource, resource.."."..extension); if not content then return content, err; end end local path = err; local f, err = envload(content, "@"..path, env); if not f then return f, err; end return f, path, metadata; end local function init(options) return setmetatable({ _options = options or {}; }, pluginloader_mt); end local function bind(self, method) return function (...) return method(self, ...); end; end local default_loader = init(); return { load_file = bind(default_loader, default_loader.load_file); load_resource = bind(default_loader, default_loader.load_resource); load_code = bind(default_loader, default_loader.load_code); load_code_ext = bind(default_loader, default_loader.load_code_ext); init = init; }; prosody-13.0.1/util/PaxHeaders/presence.lua0000644000000000000000000000011714773555365015664 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.867711635 prosody-13.0.1/util/presence.lua0000644000175000017500000000164414773555365020070 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local t_insert = table.insert; local function select_top_resources(user) local priority = 0; local recipients = {}; for _, session in pairs(user.sessions) do -- find resource with greatest priority if session.presence then local p = session.priority; if p > priority then priority = p; recipients = {session}; elseif p == priority then t_insert(recipients, session); end end end return recipients; end local function recalc_resource_map(user) if user then user.top_resources = select_top_resources(user); if #user.top_resources == 0 then user.top_resources = nil; end end end return { select_top_resources = select_top_resources; recalc_resource_map = recalc_resource_map; } prosody-13.0.1/util/PaxHeaders/promise.lua0000644000000000000000000000011714773555365015536 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.871711651 prosody-13.0.1/util/promise.lua0000644000175000017500000001204714773555365017741 0ustar00prosodyprosody00000000000000local promise_methods = {}; local promise_mt = { __name = "promise", __index = promise_methods }; local xpcall = require "prosody.util.xpcall".xpcall; local unpack = table.unpack; function promise_mt:__tostring() return "promise (" .. (self._state or "invalid") .. ")"; end local function is_promise(o) local mt = getmetatable(o); return mt == promise_mt; end local function wrap_handler(f, resolve, reject, default) if not f then return default; end return function (param) local ok, ret = xpcall(f, debug.traceback, param); if ok then resolve(ret); else reject(ret); end return true; end; end local function next_pending(self, on_fulfilled, on_rejected, resolve, reject) table.insert(self._pending_on_fulfilled, wrap_handler(on_fulfilled, resolve, reject, resolve)); table.insert(self._pending_on_rejected, wrap_handler(on_rejected, resolve, reject, reject)); end local function next_fulfilled(promise, on_fulfilled, on_rejected, resolve, reject) -- luacheck: ignore 212/on_rejected wrap_handler(on_fulfilled, resolve, reject, resolve)(promise.value); end local function next_rejected(promise, on_fulfilled, on_rejected, resolve, reject) -- luacheck: ignore 212/on_fulfilled wrap_handler(on_rejected, resolve, reject, reject)(promise.reason); end local function promise_settle(promise, new_state, new_next, cbs, value) if promise._state ~= "pending" then return; end promise._state = new_state; promise._next = new_next; for _, cb in ipairs(cbs) do cb(value); end -- No need to keep references to callbacks promise._pending_on_fulfilled = nil; promise._pending_on_rejected = nil; return true; end local function new_resolve_functions(p) local function _resolve(v) if is_promise(v) then v:next(new_resolve_functions(p)); elseif promise_settle(p, "fulfilled", next_fulfilled, p._pending_on_fulfilled, v) then p.value = v; end end local function _reject(e) if promise_settle(p, "rejected", next_rejected, p._pending_on_rejected, e) then p.reason = e; end end return _resolve, _reject; end local next_tick = function (f) f(); end local function new(f) local p = setmetatable({ _state = "pending", _next = next_pending, _pending_on_fulfilled = {}, _pending_on_rejected = {} }, promise_mt); if f then next_tick(function() local resolve, reject = new_resolve_functions(p); local ok, ret = xpcall(f, debug.traceback, resolve, reject); if not ok and p._state == "pending" then reject(ret); end end); end return p; end local function all(promises) return new(function (resolve, reject) local settled, results, loop_finished = 0, {}, false; local total = 0; for k, v in pairs(promises) do if is_promise(v) then total = total + 1; v:next(function (value) results[k] = value; settled = settled + 1; if settled == total and loop_finished then resolve(results); end end, reject); else results[k] = v; end end loop_finished = true; if settled == total then resolve(results); end end); end local function all_settled(promises) return new(function (resolve) local settled, results, loop_finished = 0, {}, false; local total = 0; for k, v in pairs(promises) do if is_promise(v) then total = total + 1; v:next(function (value) results[k] = { status = "fulfilled", value = value }; settled = settled + 1; if settled == total and loop_finished then resolve(results); end end, function (e) results[k] = { status = "rejected", reason = e }; settled = settled + 1; if settled == total and loop_finished then resolve(results); end end); else results[k] = v; end end loop_finished = true; if settled == total then resolve(results); end end); end local function join(handler, ...) local promises, n = { ... }, select("#", ...); return all(promises):next(function (results) return handler(unpack(results, 1, n)); end); end local function race(promises) return new(function (resolve, reject) for i = 1, #promises do promises[i]:next(resolve, reject); end end); end local function resolve(v) return new(function (_resolve) _resolve(v); end); end local function reject(v) return new(function (_, _reject) _reject(v); end); end local function try(f) return resolve():next(function () return f(); end); end function promise_methods:next(on_fulfilled, on_rejected) return new(function (resolve, reject) --luacheck: ignore 431/resolve 431/reject self:_next(on_fulfilled, on_rejected, resolve, reject); end); end function promise_methods:catch(on_rejected) return self:next(nil, on_rejected); end function promise_methods:finally(on_finally) local function _on_finally(value) on_finally(); return value; end local function _on_catch_finally(err) on_finally(); return reject(err); end return self:next(_on_finally, _on_catch_finally); end return { new = new; resolve = resolve; join = join; reject = reject; all = all; all_settled = all_settled; race = race; try = try; is_promise = is_promise; set_nexttick = function(new_next_tick) next_tick = new_next_tick; end; } prosody-13.0.1/util/PaxHeaders/prosodyctl0000644000000000000000000000013114773555365015476 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.871711651 30 ctime=1743706869.875711667 prosody-13.0.1/util/prosodyctl/0000755000175000017500000000000014773555365017756 5ustar00prosodyprosody00000000000000prosody-13.0.1/util/prosodyctl/PaxHeaders/cert.lua0000644000000000000000000000011714773555365017217 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.871711651 prosody-13.0.1/util/prosodyctl/cert.lua0000644000175000017500000002614514773555365021426 0ustar00prosodyprosody00000000000000local lfs = require "lfs"; local pctl = require "prosody.util.prosodyctl"; local hi = require "prosody.util.human.io"; local configmanager = require "prosody.core.configmanager"; local openssl; local cert_commands = {}; -- If a file already exists, ask if the user wants to use it or replace it -- Backups the old file if replaced local function use_existing(filename) local attrs = lfs.attributes(filename); if attrs then if hi.show_yesno(filename .. " exists, do you want to replace it? [y/n]") then local backup = filename..".bkp~"..os.date("%FT%T", attrs.change); os.rename(filename, backup); pctl.show_message("%s backed up to %s", filename, backup); else -- Use the existing file return true; end end end 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 local cert_dir = configmanager.get("*", "certificates") or "certs"; cert_basedir = configmanager.resolve_relative_path(prosody.paths.config, cert_dir); end function cert_commands.config(arg) if #arg >= 1 and arg[1] ~= "--help" then local conf_filename = cert_basedir .. "/" .. arg[1] .. ".cnf"; if use_existing(conf_filename) then return nil, conf_filename; end local distinguished_name; if arg[#arg]:find("^/") then distinguished_name = table.remove(arg); end local conf = openssl.config.new(); conf:from_prosody(prosody.hosts, configmanager, arg); if distinguished_name then local dn = {}; for k, v in distinguished_name:gmatch("/([^=/]+)=([^/]+)") do table.insert(dn, k); dn[k] = v; end conf.distinguished_name = dn; else pctl.show_message("Please provide details to include in the certificate config file."); pctl.show_message("Leave the field empty to use the default value or '.' to exclude the field.") for _, k in ipairs(openssl._DN_order) do local v = conf.distinguished_name[k]; if v then local nv = nil; if k == "commonName" then v = arg[1] elseif k == "emailAddress" then v = "xmpp@" .. arg[1]; elseif k == "countryName" then local tld = arg[1]:match"%.([a-z]+)$"; if tld and #tld == 2 and tld ~= "uk" then v = tld:upper(); end end nv = hi.show_prompt(("%s (%s):"):format(k, nv or v)); nv = (not nv or nv == "") and v or nv; if nv:find"[\192-\252][\128-\191]+" then conf.req.string_mask = "utf8only" end conf.distinguished_name[k] = nv ~= "." and nv or nil; end end end local conf_file, err = io.open(conf_filename, "w"); if not conf_file then pctl.show_warning("Could not open OpenSSL config file for writing"); pctl.show_warning("%s", err); os.exit(1); end conf_file:write(conf:serialize()); conf_file:close(); print(""); pctl.show_message("Config written to %s", conf_filename); return nil, conf_filename; else pctl.show_usage("cert config HOSTNAME [HOSTNAME+]", "Builds a certificate config file covering the supplied hostname(s)") end end function cert_commands.key(arg) if #arg >= 1 and arg[1] ~= "--help" then local key_filename = cert_basedir .. "/" .. arg[1] .. ".key"; if use_existing(key_filename) then return nil, key_filename; end os.remove(key_filename); -- This file, if it exists is unlikely to have write permissions local key_size = tonumber(arg[2] or hi.show_prompt("Choose key size (2048):") or 2048); local old_umask = pposix.umask("0377"); if openssl.genrsa{out=key_filename, key_size} then os.execute(("chmod 400 '%s'"):format(key_filename)); pctl.show_message("Key written to %s", key_filename); pposix.umask(old_umask); return nil, key_filename; end pctl.show_message("There was a problem, see OpenSSL output"); else pctl.show_usage("cert key HOSTNAME ", "Generates a RSA key named HOSTNAME.key\n " .."Prompts for a key size if none given") end end function cert_commands.request(arg) if #arg >= 1 and arg[1] ~= "--help" then local req_filename = cert_basedir .. "/" .. arg[1] .. ".req"; if use_existing(req_filename) then return nil, req_filename; end local _, key_filename = cert_commands.key({arg[1]}); local _, conf_filename = cert_commands.config(arg); if openssl.req{new=true, key=key_filename, utf8=true, sha256=true, config=conf_filename, out=req_filename} then pctl.show_message("Certificate request written to %s", req_filename); else pctl.show_message("There was a problem, see OpenSSL output"); end else pctl.show_usage("cert request HOSTNAME [HOSTNAME+]", "Generates a certificate request for the supplied hostname(s)") end end function cert_commands.generate(arg) if #arg >= 1 and arg[1] ~= "--help" then local cert_filename = cert_basedir .. "/" .. arg[1] .. ".crt"; if use_existing(cert_filename) then return nil, cert_filename; end local _, key_filename = cert_commands.key({arg[1]}); local _, conf_filename = cert_commands.config(arg); if key_filename and conf_filename and cert_filename and openssl.req{new=true, x509=true, nodes=true, key=key_filename, days=365, sha256=true, utf8=true, config=conf_filename, out=cert_filename} then pctl.show_message("Certificate written to %s", cert_filename); print(); else pctl.show_message("There was a problem, see OpenSSL output"); end else pctl.show_usage("cert generate HOSTNAME [HOSTNAME+]", "Generates a self-signed certificate for the current hostname(s)") end end local function sh_esc(s) return "'" .. s:gsub("'", "'\\''") .. "'"; end local function copy(from, to, umask, owner, group) local old_umask = umask and pposix.umask(umask); local attrs = lfs.attributes(to); if attrs then -- Move old file out of the way local backup = to..".bkp~"..os.date("%FT%T", attrs.change); assert(os.rename(to, backup)); end -- FIXME friendlier error handling, maybe move above backup back? local input = assert(io.open(from)); local output = assert(io.open(to, "w")); local data = input:read(2^11); while data and output:write(data) do data = input:read(2^11); end assert(input:close()); assert(output:close()); if not prosody.installed then -- FIXME this is possibly specific to GNU chown 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, "Failed to change ownership of "..to); end if old_umask then pposix.umask(old_umask); end return true; end function cert_commands.import(arg) local hostnames = {}; -- Move hostname arguments out of arg, the rest should be a list of paths while arg[1] and prosody.hosts[ arg[1] ] do table.insert(hostnames, table.remove(arg, 1)); end if hostnames[1] == nil then local domains = os.getenv"RENEWED_DOMAINS"; -- Set if invoked via certbot if domains then for host in domains:gmatch("%S+") do table.insert(hostnames, host); end else for host in pairs(prosody.hosts) do if host ~= "*" and configmanager.get(host, "enabled") ~= false then table.insert(hostnames, host); local http_host = configmanager.get(host, "http_host") or host; if http_host ~= host then table.insert(hostnames, http_host); end end end end end if not arg[1] or arg[1] == "--help" then -- Probably forgot the path pctl.show_usage("cert import [HOSTNAME+] /path/to/certs [/other/paths/]+", "Copies certificates to "..cert_basedir); return 1; end local owner, group; if pposix.getuid() == 0 then -- We need root to change ownership owner = configmanager.get("*", "prosody_user") or "prosody"; group = configmanager.get("*", "prosody_group") or owner; end local cm = require "prosody.core.certmanager"; local files_by_name = {} for _, dir in ipairs(arg) do cm.index_certs(dir, files_by_name); end local imported = {}; table.sort(hostnames, function (a, b) -- Try to find base domain name before sub-domains, then alphabetically, so -- that the order and choice of file name is deterministic. if #a == #b then return a < b; else return #a < #b; end end); for _, host in ipairs(hostnames) do local paths = cm.find_cert_in_index(files_by_name, host); if paths and imported[paths.certificate] then -- One certificate, many names! table.insert(imported, host); elseif paths then local c = copy(paths.certificate, cert_basedir .. "/" .. host .. ".crt", nil, owner, group); local k = copy(paths.key, cert_basedir .. "/" .. host .. ".key", "0377", owner, group); if c and k then table.insert(imported, host); imported[paths.certificate] = true; else if not c then pctl.show_warning("Could not copy certificate '%s'", paths.certificate); end if not k then pctl.show_warning("Could not copy key '%s'", paths.key); end end else -- TODO Say where we looked pctl.show_warning("No certificate for host %s found :(", host); end -- TODO Additional checks -- Certificate names matches the hostname -- Private key matches public key in certificate end if imported[1] then pctl.show_message("Imported certificate and key for hosts %s", table.concat(imported, ", ")); local ok, err = pctl.reload(); if not ok and err ~= "not-running" then pctl.show_message(pctl.error_messages[err]); end else pctl.show_warning("No certificates imported :("); return 1; end end local function cert(arg) if #arg >= 1 and arg[1] ~= "--help" then openssl = require "prosody.util.openssl"; lfs = require "lfs"; local cert_dir_attrs = lfs.attributes(cert_basedir); if not cert_dir_attrs then pctl.show_warning("The directory %s does not exist", cert_basedir); return 1; -- TODO Should we create it? end local uid = pposix.getuid(); if uid ~= 0 and uid ~= cert_dir_attrs.uid then pctl.show_warning("The directory %s is not owned by the current user, won't be able to write files to it", cert_basedir); return 1; elseif not cert_dir_attrs.permissions then -- COMPAT with LuaFilesystem < 1.6.2 (hey CentOS!) pctl.show_message("Unable to check permissions on %s (LuaFilesystem 1.6.2+ required)", cert_basedir); pctl.show_message("Please confirm that Prosody (and only Prosody) can write to this directory)"); elseif cert_dir_attrs.permissions:match("^%.w..%-..%-.$") then pctl.show_warning("The directory %s not only writable by its owner", cert_basedir); return 1; end local subcmd = table.remove(arg, 1); if type(cert_commands[subcmd]) == "function" then if subcmd ~= "import" then -- hostnames are optional for import if not arg[1] then pctl.show_message"You need to supply at least one hostname" arg = { "--help" }; end if arg[1] ~= "--help" and not prosody.hosts[arg[1]] then pctl.show_message(pctl.error_messages["no-such-host"]); return 1; end end return cert_commands[subcmd](arg); elseif subcmd == "check" then 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.") for _, cmd in pairs(cert_commands) do print() cmd{ "--help" } end end return { cert = cert; }; prosody-13.0.1/util/prosodyctl/PaxHeaders/check.lua0000644000000000000000000000011714773555365017337 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.875711667 prosody-13.0.1/util/prosodyctl/check.lua0000644000175000017500000020503514773555365021543 0ustar00prosodyprosody00000000000000local 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 "prosody.util.jid".prepped_split; local modulemanager = require "prosody.core.modulemanager"; local async = require "prosody.util.async"; local httputil = require "prosody.util.http"; local human_units = require "prosody.util.human.units"; 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 "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)), { method="POST", headers={["Accept"] = "application/json"; ["Content-Type"] = "application/json"}, body=json.encode({target=target_host}), })); if not response then return false, err; end if response.code ~= 200 then return false, ("API replied with non-200 code: %d"):format(response.code); end local decoded_body, err = json.decode(response.body); if decoded_body == nil then return false, ("Failed to parse API JSON: %s"):format(err) end local success = decoded_body["success"]; return success == true, nil; end local function check_probe(base_url, probe_module, target) 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)); if not response then return false, err; end if response.code ~= 200 then return false, ("API replied with non-200 code: %d"):format(response.code); end for line in response.body:gmatch("[^\r\n]+") do local probe_success = line:match("^probe_success%s+(%d+)"); if probe_success == "1" then return true; elseif probe_success == "0" then return false; end end return false, "Probe endpoint did not return a success status"; end local function check_turn_service(turn_service, ping_service) local ip = require "prosody.util.ip"; local stun = require "prosody.net.stun"; local result = { warnings = {} }; -- Create UDP socket for communication with the server local sock = assert(require "socket".udp()); do local ok, err = sock:setsockname("*", 0); if not ok then result.error = "Unable to perform TURN test: setsockname: "..tostring(err); return result; end ok, err = sock:setpeername(turn_service.host, turn_service.port); if not ok then result.error = "Unable to perform TURN test: setpeername: "..tostring(err); return result; end end sock:settimeout(10); -- Helper function to receive a packet local function receive_packet() local raw_packet, err = sock:receive(); if not raw_packet then return nil, err; end return stun.new_packet():deserialize(raw_packet); end -- Send a "binding" query, i.e. a request for our external IP/port local bind_query = stun.new_packet("binding", "request"); bind_query:add_attribute("software", "prosodyctl check turn"); sock:send(bind_query:serialize()); local bind_result, err = receive_packet(); if not bind_result then result.error = "No STUN response: "..err; return result; elseif bind_result:is_err_resp() then result.error = ("STUN server returned error: %d (%s)"):format(bind_result:get_error()); return result; elseif not bind_result:is_success_resp() then result.error = ("Unexpected STUN response: %d (%s)"):format(bind_result:get_type()); return result; end result.external_ip = bind_result:get_xor_mapped_address(); if not result.external_ip then result.error = "STUN server did not return an address"; return result; end if ip.new_ip(result.external_ip.address).private then table.insert(result.warnings, "STUN returned a private IP! Is the TURN server behind a NAT and misconfigured?"); end -- Send a TURN "allocate" request. Expected to fail due to auth, but -- necessary to obtain a valid realm/nonce from the server. local pre_request = stun.new_packet("allocate", "request"); sock:send(pre_request:serialize()); local pre_result, err = receive_packet(); if not pre_result then result.error = "No initial TURN response: "..err; return result; elseif pre_result:is_success_resp() then result.error = "TURN server does not have authentication enabled"; return result; end local realm = pre_result:get_attribute("realm"); local nonce = pre_result:get_attribute("nonce"); if not realm then table.insert(result.warnings, "TURN server did not return an authentication realm. Is authentication enabled?"); end if not nonce then table.insert(result.warnings, "TURN server did not return a nonce"); end -- Use the configured secret to obtain temporary user/pass credentials local turn_user, turn_pass = stun.get_user_pass_from_secret(turn_service.secret); -- Send a TURN allocate request, will fail if auth is wrong local alloc_request = stun.new_packet("allocate", "request"); alloc_request:add_requested_transport("udp"); alloc_request:add_attribute("username", turn_user); if realm then alloc_request:add_attribute("realm", realm); end if nonce then alloc_request:add_attribute("nonce", nonce); end local key = stun.get_long_term_auth_key(realm or turn_service.host, turn_user, turn_pass); alloc_request:add_message_integrity(key); sock:send(alloc_request:serialize()); -- Check the response local alloc_response, err = receive_packet(); if not alloc_response then result.error = "TURN server did not response to allocation request: "..err; return result; elseif alloc_response:is_err_resp() then 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()); return result; end result.relayed_addresses = alloc_response:get_xor_relayed_addresses(); if not ping_service then -- Success! We won't be running the relay test. return result; end -- Run the relay test - i.e. send a binding request to ping_service -- and receive a response. -- Resolve the IP of the ping service local ping_host, ping_port = ping_service:match("^([^:]+):(%d+)$"); if ping_host then ping_port = tonumber(ping_port); else -- Only a hostname specified, use default STUN port ping_host, ping_port = ping_service, 3478; end if ping_host == turn_service.host then result.error = ("Unable to perform ping test: please supply an external STUN server address. See https://prosody.im/doc/turn#prosodyctl-check"); return result; end local ping_service_ip, err = socket.dns.toip(ping_host); if not ping_service_ip then result.error = "Unable to resolve ping service hostname: "..err; return result; end -- Ask the TURN server to allow packets from the ping service IP local perm_request = stun.new_packet("create-permission"); perm_request:add_xor_peer_address(ping_service_ip); perm_request:add_attribute("username", turn_user); if realm then perm_request:add_attribute("realm", realm); end if nonce then perm_request:add_attribute("nonce", nonce); end perm_request:add_message_integrity(key); sock:send(perm_request:serialize()); local perm_response, err = receive_packet(); if not perm_response then result.error = "No response from TURN server when requesting peer permission: "..err; return result; elseif perm_response:is_err_resp() then result.error = ("TURN permission request failed: %d (%s)"):format(perm_response:get_error()); return result; elseif not perm_response:is_success_resp() then result.error = ("Unexpected TURN response: %d (%s)"):format(perm_response:get_type()); return result; end -- Ask the TURN server to relay a STUN binding request to the ping server local ping_data = stun.new_packet("binding"):serialize(); local ping_request = stun.new_packet("send", "indication"); ping_request:add_xor_peer_address(ping_service_ip, ping_port); ping_request:add_attribute("data", ping_data); ping_request:add_attribute("username", turn_user); if realm then ping_request:add_attribute("realm", realm); end if nonce then ping_request:add_attribute("nonce", nonce); end ping_request:add_message_integrity(key); sock:send(ping_request:serialize()); local ping_response, err = receive_packet(); if not ping_response then result.error = "No response from ping server ("..ping_service_ip.."): "..err; return result; elseif not ping_response:is_indication() or select(2, ping_response:get_method()) ~= "data" then result.error = ("Unexpected TURN response: %s %s"):format(select(2, ping_response:get_method()), select(2, ping_response:get_type())); return result; end local pong_data = ping_response:get_attribute("data"); if not pong_data then result.error = "No data relayed from remote server"; return result; end local pong = stun.new_packet():deserialize(pong_data); result.external_ip_pong = pong:get_xor_mapped_address(); if not result.external_ip_pong then result.error = "Ping server did not return an address"; return result; end local relay_address_found, relay_port_matches; for _, relayed_address in ipairs(result.relayed_addresses) do if relayed_address.address == result.external_ip_pong.address then relay_address_found = true; relay_port_matches = result.external_ip_pong.port == relayed_address.port; end end if not relay_address_found then table.insert(result.warnings, "TURN external IP vs relay address mismatch! Is the TURN server behind a NAT and misconfigured?"); elseif not relay_port_matches then table.insert(result.warnings, "External port does not match reported relay port! This is probably caused by a NAT in front of the TURN server."); end -- return result; end local function skip_bare_jid_hosts(host) if jid_split(host) then -- See issue #779 return false; end return true; end local check_opts = { short_params = { h = "help", v = "verbose"; }; value_params = { ping = true; }; }; local function check(arg) if arg[1] == "help" or arg[1] == "--help" then show_usage([[check]], [[Perform basic checks on your Prosody installation]]); return 1; end local what = table.remove(arg, 1); local opts, opts_err, opts_info = parse_args(arg, check_opts); if opts_err == "missing-value" then print("Error: Expected a value after '"..opts_info.."'"); return 1; elseif opts_err == "param-not-found" then print("Error: Unknown parameter: "..opts_info); return 1; end 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 is_user_host(host, conf) return host ~= "*" and conf.component_module == nil; end local function is_component_host(host, conf) return host ~= "*" and conf.component_module ~= nil; end local function enabled_hosts() return it.filter(disabled_hosts, it.sorted_pairs(configmanager.getconfig())); end local function enabled_user_hosts() return it.filter(is_user_host, it.sorted_pairs(configmanager.getconfig())); end local function enabled_components() return it.filter(is_component_host, it.sorted_pairs(configmanager.getconfig())); end 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 disabled_hosts_set:add(host); end end if not disabled_hosts_set:empty() then local msg = "Checks will be skipped for these disabled hosts: %s"; if what then msg = "These hosts are disabled: %s"; end show_warning(msg, tostring(disabled_hosts_set)); if what then return 0; end print"" end end function checks.config() print("Checking config..."); if what == "config" then local files = configmanager.files(); print(" The following configuration files have been loaded:"); print(" - "..table.concat(files, "\n - ")); end local obsolete = set.new({ --> remove "archive_cleanup_interval", "dns_timeout", "muc_log_cleanup_interval", "s2s_dns_resolvers", "setgid", "setuid", }); local function instead_use(kind, name, value) if kind == "option" then if value then return string.format("instead, use '%s = %q'", name, value); else return string.format("instead, use '%s'", name); end elseif kind == "module" then return string.format("instead, add %q to '%s'", name, value or "modules_enabled"); elseif kind == "community" then return string.format("instead, add %q from %s", name, value or "prosody-modules"); end return kind end local deprecated_replacements = { anonymous_login = instead_use("option", "authentication", "anonymous"); daemonize = "instead, use the --daemonize/-D or --foreground/-F command line flags"; disallow_s2s = instead_use("module", "s2s", "modules_disabled"); no_daemonize = "instead, use the --daemonize/-D or --foreground/-F command line flags"; require_encryption = "instead, use 'c2s_require_encryption' and 's2s_require_encryption'"; vcard_compatibility = instead_use("community", "mod_compat_vcard"); use_libevent = instead_use("option", "network_backend", "event"); whitelist_registration_only = instead_use("option", "allowlist_registration_only"); registration_whitelist = instead_use("option", "registration_allowlist"); registration_blacklist = instead_use("option", "registration_blocklist"); blacklist_on_registration_throttle_overload = instead_use("blocklist_on_registration_throttle_overload"); cross_domain_bosh = "instead, use 'http_cors_override', see https://prosody.im/doc/http#cross-domain-cors-support"; cross_domain_websocket = "instead, use 'http_cors_override', see https://prosody.im/doc/http#cross-domain-cors-support"; }; -- FIXME all the singular _port and _interface options are supposed to be deprecated too local deprecated_ports = { bosh = "http", legacy_ssl = "c2s_direct_tls" }; local port_suffixes = set.new({ "port", "ports", "interface", "interfaces", "ssl" }); for port, replacement in pairs(deprecated_ports) do for suffix in port_suffixes do local rsuffix = (suffix == "port" or suffix == "interface") and suffix.."s" or suffix; deprecated_replacements[port.."_"..suffix] = "instead, use '"..replacement.."_"..rsuffix.."'" end end local deprecated = set.new(array.collect(it.keys(deprecated_replacements))); local known_global_options = set.new({ "access_control_allow_credentials", "access_control_allow_headers", "access_control_allow_methods", "access_control_max_age", "admin_socket", "body_size_limit", "bosh_max_inactivity", "bosh_max_polling", "bosh_max_wait", "buffer_size_limit", "c2s_close_timeout", "c2s_stanza_size_limit", "c2s_tcp_keepalives", "c2s_timeout", "component_stanza_size_limit", "component_tcp_keepalives", "consider_bosh_secure", "consider_websocket_secure", "console_banner", "console_prettyprint_settings", "daemonize", "gc", "http_default_host", "http_errors_always_show", "http_errors_default_message", "http_errors_detailed", "http_errors_messages", "http_max_buffer_size", "http_max_content_size", "installer_plugin_path", "limits", "limits_resolution", "log", "multiplex_buffer_size", "network_backend", "network_default_read_size", "network_settings", "openmetrics_allow_cidr", "openmetrics_allow_ips", "pidfile", "plugin_paths", "plugin_server", "prosodyctl_timeout", "prosody_group", "prosody_user", "run_as_root", "s2s_close_timeout", "s2s_insecure_domains", "s2s_require_encryption", "s2s_secure_auth", "s2s_secure_domains", "s2s_stanza_size_limit", "s2s_tcp_keepalives", "s2s_timeout", "statistics", "statistics_config", "statistics_interval", "tcp_keepalives", "tls_profile", "trusted_proxies", "umask", "use_dane", "use_ipv4", "use_ipv6", "websocket_frame_buffer_limit", "websocket_frame_fragment_limit", "websocket_get_response_body", "websocket_get_response_text", }); local config = configmanager.getconfig(); local global = api("*"); -- Check that we have any global options (caused by putting a host at the top) if it.count(it.filter("log", pairs(config["*"]))) == 0 then ok = false; print(""); print(" No global options defined. Perhaps you have put a host definition at the top") print(" of the config file? They should be at the bottom, see https://prosody.im/doc/configure#overview"); end if it.count(enabled_hosts()) == 0 then ok = false; print(""); if it.count(it.filter("*", pairs(config))) == 0 then print(" No hosts are defined, please add at least one VirtualHost section") elseif config["*"]["enabled"] == false then print(" No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section") else print(" All hosts are disabled. Remove enabled = false from at least one VirtualHost section") end end if not config["*"].modules_enabled then print(" No global modules_enabled is set?"); local suggested_global_modules; for host, options in enabled_hosts() do --luacheck: ignore 213/host if not options.component_module and options.modules_enabled then suggested_global_modules = set.intersection(suggested_global_modules or set.new(options.modules_enabled), set.new(options.modules_enabled)); end end if suggested_global_modules and not suggested_global_modules:empty() then print(" Consider moving these modules into modules_enabled in the global section:") print(" "..tostring(suggested_global_modules / function (x) return ("%q"):format(x) end)); end print(); end local function validate_module_list(host, name, modules) if modules == nil then return -- okay except for global section, checked separately end local t = type(modules) if t ~= "table" then print(" The " .. name .. " in the " .. host .. " section should not be a " .. t .. " but a list of strings, e.g."); print(" " .. name .. " = { \"name_of_module\", \"another_plugin\", }") print() ok = false return 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 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 end end for host, options in enabled_hosts() do validate_module_list(host, "modules_enabled", options.modules_enabled); validate_module_list(host, "modules_disabled", options.modules_disabled); end do -- Check for modules enabled both normally and as components local modules = global:get_option_set("modules_enabled"); for host, options in enabled_hosts() do local component_module = options.component_module; if component_module and modules:contains(component_module) then print((" mod_%s is enabled both in modules_enabled and as Component %q %q"):format(component_module, host, component_module)); print(" This means the service is enabled on all VirtualHosts as well as the Component."); print(" Are you sure this what you want? It may cause unexpected behaviour."); end end end -- Check for global options under hosts local global_options = set.new(it.to_array(it.keys(config["*"]))); local obsolete_global_options = set.intersection(global_options, obsolete); if not obsolete_global_options:empty() then print(""); print(" You have some obsolete options you can remove from the global section:"); print(" "..tostring(obsolete_global_options)) ok = false; end local deprecated_global_options = set.intersection(global_options, deprecated); if not deprecated_global_options:empty() then print(""); print(" You have some deprecated options in the global section:"); for option in deprecated_global_options do print((" '%s' -- %s"):format(option, deprecated_replacements[option])); end ok = false; end for host, options in it.filter(function (h) return h ~= "*" end, pairs(configmanager.getconfig())) do local host_options = set.new(it.to_array(it.keys(options))); local misplaced_options = set.intersection(host_options, known_global_options); for name in pairs(options) do if name:match("^interfaces?") or name:match("_ports?$") or name:match("_interfaces?$") or (name:match("_ssl$") and not name:match("^[cs]2s_ssl$")) then misplaced_options:add(name); end end -- FIXME These _could_ be misplaced, but we would have to check where the corresponding module is loaded to be sure misplaced_options:exclude(set.new({ "external_service_port", "turn_external_port" })); if not misplaced_options:empty() then ok = false; print(""); local n = it.count(misplaced_options); print(" You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be"); print(" in the global section of the config file, above any VirtualHost or Component definitions,") print(" see https://prosody.im/doc/configure#overview for more information.") print(""); print(" You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", ")); end end for host, options in enabled_hosts() do local host_options = set.new(it.to_array(it.keys(options))); local subdomain = host:match("^[^.]+"); if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp" or subdomain == "chat" or subdomain == "im") then print(""); print(" Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to"); print(" "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host.."."); print(" For more information see: https://prosody.im/doc/dns"); end end local all_modules = set.new(config["*"].modules_enabled); local all_options = set.new(it.to_array(it.keys(config["*"]))); for host in enabled_hosts() do all_options:include(set.new(it.to_array(it.keys(config[host])))); all_modules:include(set.new(config[host].modules_enabled)); end for mod in all_modules do if mod:match("^mod_") then print(""); print(" Modules in modules_enabled should not have the 'mod_' prefix included."); print(" Change '"..mod.."' to '"..mod:match("^mod_(.*)").."'."); elseif mod:match("^auth_") then print(""); print(" Authentication modules should not be added to modules_enabled,"); print(" but be specified in the 'authentication' option."); print(" Remove '"..mod.."' from modules_enabled and instead add"); print(" authentication = '"..mod:match("^auth_(.*)").."'"); print(" For more information see https://prosody.im/doc/authentication"); elseif mod:match("^storage_") then print(""); print(" storage modules should not be added to modules_enabled,"); print(" but be specified in the 'storage' option."); print(" Remove '"..mod.."' from modules_enabled and instead add"); print(" storage = '"..mod:match("^storage_(.*)").."'"); print(" For more information see https://prosody.im/doc/storage"); end end if all_modules:contains("vcard") and all_modules:contains("vcard_legacy") then print(""); print(" Both mod_vcard_legacy and mod_vcard are enabled but they conflict"); print(" with each other. Remove one."); end if all_modules:contains("pep") and all_modules:contains("pep_simple") then print(""); print(" Both mod_pep_simple and mod_pep are enabled but they conflict"); print(" with each other. Remove one."); end if all_modules:contains("posix") then print(""); print(" mod_posix is loaded in your configuration file, but it has"); print(" been deprecated. You can safely remove it."); end for host, host_config in pairs(config) do --luacheck: ignore 213/host if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then print(""); print(" The 'default_storage' option is not needed if 'storage' is set to a string."); break; end end for host, host_config in pairs(config) do --luacheck: ignore 213/host if type(rawget(host_config, "storage")) == "string" and rawget(host_config, "default_storage") then print(""); print(" The 'default_storage' option is not needed if 'storage' is set to a string."); break; end end local require_encryption = set.intersection(all_options, set.new({ "require_encryption", "c2s_require_encryption", "s2s_require_encryption" })):empty(); local ssl = dependencies.softreq"ssl"; if not ssl then if not require_encryption then print(""); print(" You require encryption but LuaSec is not available."); print(" Connections will fail."); ok = false; end elseif not ssl.loadcertificate then if all_options:contains("s2s_secure_auth") then print(""); print(" You have set s2s_secure_auth but your version of LuaSec does "); print(" not support certificate validation, so all s2s connections will"); print(" fail."); ok = false; elseif all_options:contains("s2s_secure_domains") then local secure_domains = set.new(); for host in enabled_hosts() do if api(host):get_option_boolean("s2s_secure_auth") then secure_domains:add("*"); else secure_domains:include(api(host):get_option_set("s2s_secure_domains", {})); end end if not secure_domains:empty() then print(""); print(" You have set s2s_secure_domains but your version of LuaSec does "); print(" not support certificate validation, so s2s connections to/from "); print(" these domains will fail."); ok = false; end end elseif require_encryption and not all_modules:contains("tls") then print(""); print(" You require encryption but mod_tls is not enabled."); print(" Connections will fail."); ok = false; end do local registration_enabled_hosts = {}; for host in enabled_hosts() do local host_modules, component = modulemanager.get_modules_for_host(host); local hostapi = api(host); local allow_registration = hostapi:get_option_boolean("allow_registration", false); local mod_register = host_modules:contains("register"); local mod_register_ibr = host_modules:contains("register_ibr"); local mod_invites_register = host_modules:contains("invites_register"); local registration_invite_only = hostapi:get_option_boolean("registration_invite_only", true); local is_vhost = not component; if is_vhost and (mod_register_ibr or (mod_register and allow_registration)) and not (mod_invites_register and registration_invite_only) then table.insert(registration_enabled_hosts, host); end end if #registration_enabled_hosts > 0 then table.sort(registration_enabled_hosts); print(""); print(" Public registration is enabled on:"); print(" "..table.concat(registration_enabled_hosts, ", ")); print(""); print(" If this is intentional, review our guidelines on running a public server"); print(" at https://prosody.im/doc/public_servers - otherwise, consider switching to"); print(" invite-based registration, which is more secure."); end end do local orphan_components = {}; local referenced_components = set.new(); local enabled_hosts_set = set.new(); local invalid_disco_items = {}; for host in it.filter("*", pairs(configmanager.getconfig())) do local hostapi = api(host); if hostapi:get_option_boolean("enabled", true) then enabled_hosts_set:add(host); for _, disco_item in ipairs(hostapi:get_option_array("disco_items", {})) do if type(disco_item[1]) == "string" then referenced_components:add(disco_item[1]); else invalid_disco_items[host] = true; end end end end for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do local is_component = not not select(2, modulemanager.get_modules_for_host(host)); if is_component then local parent_domain = host:match("^[^.]+%.(.+)$"); local is_orphan = not (enabled_hosts_set:contains(parent_domain) or referenced_components:contains(host)); if is_orphan then table.insert(orphan_components, host); end end end if next(invalid_disco_items) ~= nil then print(""); print(" Some hosts in your configuration file have an invalid 'disco_items' option."); print(" This may cause further errors, such as unreferenced components."); print(""); for host in it.sorted_pairs(invalid_disco_items) do print(" - "..host); end print(""); end if #orphan_components > 0 then table.sort(orphan_components); print(""); print(" Your configuration contains the following unreferenced components:\n"); print(" "..table.concat(orphan_components, "\n ")); print(""); print(" Clients may not be able to discover these services because they are not linked to"); print(" any VirtualHost. They are automatically linked if they are direct subdomains of a"); print(" VirtualHost. Alternatively, you can explicitly link them using the disco_items option."); print(" For more information see https://prosody.im/doc/modules/mod_disco#items"); end end -- Check hostname validity do local idna = require "prosody.util.encodings".idna; local invalid_hosts = {}; local alabel_hosts = {}; for host in it.filter("*", pairs(configmanager.getconfig())) do local _, h, _ = jid_split(host); if not h or not idna.to_ascii(h) then table.insert(invalid_hosts, host); else for label in h:gmatch("[^%.]+") do if label:match("^xn%-%-") then table.insert(alabel_hosts, host); break; end end end end if #invalid_hosts > 0 then table.sort(invalid_hosts); print(""); print(" Your configuration contains invalid host names:"); print(" "..table.concat(invalid_hosts, "\n ")); print(""); print(" Clients may not be able to log in to these hosts, or you may not be able to"); print(" communicate with remote servers."); print(" Use a valid domain name to correct this issue."); end if #alabel_hosts > 0 then table.sort(alabel_hosts); print(""); print(" Your configuration contains incorrectly-encoded hostnames:"); for _, ahost in ipairs(alabel_hosts) do print((" '%s' (should be '%s')"):format(ahost, idna.to_unicode(ahost))); end print(""); print(" Clients may not be able to log in to these hosts, or you may not be able to"); print(" communicate with remote servers."); print(" To correct this issue, use the Unicode version of the domain in Prosody's config file."); end if #invalid_hosts > 0 or #alabel_hosts > 0 then print(""); print(" WARNING: Changing the name of a VirtualHost in Prosody's config file"); print(" WILL NOT migrate any existing data (user accounts, etc.) to the new name."); ok = false; end end -- Check features do local missing_features = {}; for host in enabled_user_hosts() do local all_features = checks.features(host, true); if not all_features then table.insert(missing_features, host); end end if #missing_features > 0 then print(""); print(" Some of your hosts may be missing features due to a lack of configuration."); print(" For more details, use the 'prosodyctl check features' command."); end end print("Done.\n"); end function checks.dns() local dns = require "prosody.net.dns"; pcall(function () local unbound = require"prosody.net.unbound"; dns = unbound.dns; end) 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}); local c2s_tls_ports = global:get_option_set("c2s_direct_tls_ports", {}); local s2s_tls_ports = global:get_option_set("s2s_direct_tls_ports", {}); local global_enabled = set.new(); for host in enabled_hosts() do global_enabled:include(modulemanager.get_modules_for_host(host)); end if global_enabled:contains("net_multiplex") then local multiplex_ports = global:get_option_set("ports", {}); local multiplex_tls_ports = global:get_option_set("ssl_ports", {}); if not multiplex_ports:empty() then c2s_ports = c2s_ports + multiplex_ports; s2s_ports = s2s_ports + multiplex_ports; end if not multiplex_tls_ports:empty() then c2s_tls_ports = c2s_tls_ports + multiplex_tls_ports; s2s_tls_ports = s2s_tls_ports + multiplex_tls_ports; end end local c2s_srv_required, s2s_srv_required, c2s_tls_srv_required, s2s_tls_srv_required; if not c2s_ports:contains(5222) then c2s_srv_required = true; end if not s2s_ports:contains(5269) then s2s_srv_required = true; end if not c2s_tls_ports:empty() then c2s_tls_srv_required = true; end if not s2s_tls_ports:empty() then s2s_tls_srv_required = true; end local problem_hosts = set.new(); local external_addresses, internal_addresses = set.new(), set.new(); local fqdn = socket.dns.tohostname(socket.dns.gethostname()); if fqdn then local fqdn_a = idna.to_ascii(fqdn); if fqdn_a then local res = dns.lookup(fqdn_a, "A"); if res then for _, record in ipairs(res) do external_addresses:add(record.a); end end end if fqdn_a then local res = dns.lookup(fqdn_a, "AAAA"); if res then for _, record in ipairs(res) do external_addresses:add(record.aaaa); end end end end 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 external_addresses:add(addr); else internal_addresses:add(addr); end end -- Allow admin to specify additional (e.g. undiscoverable) IP addresses in the config for _, address in ipairs(global:get_option_array("external_addresses", {})) do external_addresses:add(address); end if external_addresses:empty() then print(""); print(" Failed to determine the external addresses of this server. Checks may be inaccurate."); print(" If you know the correct external addresses you can specify them in the config like:") print(" external_addresses = { \"192.0.2.34\", \"2001:db8::abcd:1234\" }") c2s_srv_required, s2s_srv_required = true, true; end local v6_supported = not not socket.tcp6; local use_ipv4 = global:get_option_boolean("use_ipv4", true); local use_ipv6 = global:get_option_boolean("use_ipv6", true); local function trim_dns_name(n) return (n:gsub("%.$", "")); end local unknown_addresses = set.new(); local function is_valid_domain(domain) return idna.to_ascii(domain) ~= nil; end for jid in it.filter(is_valid_domain, enabled_hosts()) do local all_targets_ok, some_targets_ok = true, false; local node, host = jid_split(jid); local modules, component_module = modulemanager.get_modules_for_host(host); if component_module then modules:add(component_module); end -- TODO Refactor these DNS SRV checks since they are very similar -- FIXME Suggest concrete actionable steps to correct issues so that -- users don't have to copy-paste the message into the support chat and -- ask what to do about it. local is_component = not not component_module; print("Checking DNS for "..(is_component and "component" or "host").." "..jid.."..."); if node then print("Only the domain part ("..host..") is used in DNS.") end local target_hosts = set.new(); if modules:contains("c2s") then local res = dns.lookup("_xmpp-client._tcp."..idna.to_ascii(host)..".", "SRV"); if res and #res > 0 then for _, record in ipairs(res) do if record.srv.target == "." then -- TODO is this an error if mod_c2s is enabled? print(" 'xmpp-client' service disabled by pointing to '.'"); -- FIXME Explain better what this is break; end local target = trim_dns_name(record.srv.target); target_hosts:add(target); if not c2s_ports:contains(record.srv.port) then print(" SRV target "..target.." contains unknown client port: "..record.srv.port); end end else if c2s_srv_required then print(" No _xmpp-client SRV record found for "..host..", but it looks like you need one."); all_targets_ok = false; else target_hosts:add(host); end end end if modules:contains("c2s") then local res = dns.lookup("_xmpps-client._tcp."..idna.to_ascii(host)..".", "SRV"); if res and #res > 0 then for _, record in ipairs(res) do if record.srv.target == "." then -- TODO is this an error if mod_c2s is enabled? print(" 'xmpps-client' service disabled by pointing to '.'"); -- FIXME Explain better what this is break; end local target = trim_dns_name(record.srv.target); target_hosts:add(target); if not c2s_tls_ports:contains(record.srv.port) then print(" SRV target "..target.." contains unknown Direct TLS client port: "..record.srv.port); end end elseif c2s_tls_srv_required then print(" No _xmpps-client SRV record found for "..host..", but it looks like you need one."); all_targets_ok = false; end end if modules:contains("s2s") then local res = dns.lookup("_xmpp-server._tcp."..idna.to_ascii(host)..".", "SRV"); if res and #res > 0 then for _, record in ipairs(res) do if record.srv.target == "." then -- TODO Is this an error if mod_s2s is enabled? print(" 'xmpp-server' service disabled by pointing to '.'"); -- FIXME Explain better what this is break; end local target = trim_dns_name(record.srv.target); target_hosts:add(target); if not s2s_ports:contains(record.srv.port) then print(" SRV target "..target.." contains unknown server port: "..record.srv.port); end end else if s2s_srv_required then print(" No _xmpp-server SRV record found for "..host..", but it looks like you need one."); all_targets_ok = false; else target_hosts:add(host); end end end if modules:contains("s2s") then local res = dns.lookup("_xmpps-server._tcp."..idna.to_ascii(host)..".", "SRV"); if res and #res > 0 then for _, record in ipairs(res) do if record.srv.target == "." then -- TODO is this an error if mod_s2s is enabled? print(" 'xmpps-server' service disabled by pointing to '.'"); -- FIXME Explain better what this is break; end local target = trim_dns_name(record.srv.target); target_hosts:add(target); if not s2s_tls_ports:contains(record.srv.port) then print(" SRV target "..target.." contains unknown Direct TLS server port: "..record.srv.port); end end elseif s2s_tls_srv_required then print(" No _xmpps-server SRV record found for "..host..", but it looks like you need one."); all_targets_ok = false; end end if target_hosts:empty() then target_hosts:add(host); end if target_hosts:contains("localhost") then print(" Target 'localhost' cannot be accessed from other servers"); target_hosts:remove("localhost"); end local function check_address(target) local A, AAAA = dns.lookup(idna.to_ascii(target), "A"), dns.lookup(idna.to_ascii(target), "AAAA"); local prob = {}; if use_ipv4 and not (A and #A > 0) then table.insert(prob, "A"); end if use_ipv6 and not (AAAA and #AAAA > 0) then table.insert(prob, "AAAA"); end return prob; end if modules:contains("proxy65") then local proxy65_target = api(host):get_option_string("proxy65_address", host); if type(proxy65_target) == "string" then local prob = check_address(proxy65_target); if #prob > 0 then print(" File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/") .." record. Create one or set 'proxy65_address' to the correct host/IP."); end else print(" proxy65_address for "..host.." should be set to a string, unable to perform DNS check"); end end local known_http_modules = set.new { "bosh"; "http_files"; "http_file_share"; "http_openmetrics"; "websocket" }; if modules:contains("http") or not set.intersection(modules, known_http_modules):empty() or contains_match(modules, "^http_") or contains_match(modules, "_web$") then local http_host = api(host):get_option_string("http_host", host); local http_internal_host = http_host; local http_url = api(host):get_option_string("http_external_url"); if http_url then local url_parse = require "socket.url".parse; local external_url_parts = url_parse(http_url); if external_url_parts then http_host = external_url_parts.host; else print(" The 'http_external_url' setting is not a valid URL"); end end local prob = check_address(http_host); if #prob > 1 then print(" HTTP service " .. http_host .. " has no " .. table.concat(prob, "/") .. " record. Create one or change " .. (http_url and "'http_external_url'" or "'http_host'").." to the correct host."); end if http_host ~= http_internal_host then print(" Ensure the reverse proxy sets the HTTP Host header to '" .. http_internal_host .. "'"); end end if not use_ipv4 and not use_ipv6 then print(" Both IPv6 and IPv4 are disabled, Prosody will not listen on any ports"); print(" nor be able to connect to any remote servers."); all_targets_ok = false; end for target_host in target_hosts do local host_ok_v4, host_ok_v6; do local res = dns.lookup(idna.to_ascii(target_host), "A"); if res then for _, record in ipairs(res) do if external_addresses:contains(record.a) then some_targets_ok = true; host_ok_v4 = true; elseif internal_addresses:contains(record.a) then host_ok_v4 = true; some_targets_ok = true; print(" "..target_host.." A record points to internal address, external connections might fail"); else print(" "..target_host.." A record points to unknown address "..record.a); unknown_addresses:add(record.a); all_targets_ok = false; end end end end do local res = dns.lookup(idna.to_ascii(target_host), "AAAA"); if res then for _, record in ipairs(res) do if external_addresses:contains(record.aaaa) then some_targets_ok = true; host_ok_v6 = true; elseif internal_addresses:contains(record.aaaa) then host_ok_v6 = true; some_targets_ok = true; print(" "..target_host.." AAAA record points to internal address, external connections might fail"); else print(" "..target_host.." AAAA record points to unknown address "..record.aaaa); unknown_addresses:add(record.aaaa); all_targets_ok = false; end end end end if host_ok_v4 and not use_ipv4 then print(" Host "..target_host.." does seem to resolve to this server but IPv4 has been disabled"); all_targets_ok = false; end if host_ok_v6 and not use_ipv6 then print(" Host "..target_host.." does seem to resolve to this server but IPv6 has been disabled"); all_targets_ok = false; end local bad_protos = {} if use_ipv4 and not host_ok_v4 then table.insert(bad_protos, "IPv4"); end if use_ipv6 and not host_ok_v6 then table.insert(bad_protos, "IPv6"); end if #bad_protos > 0 then print(" Host "..target_host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")"); end if host_ok_v6 and not v6_supported then print(" Host "..target_host.." has AAAA records, but your version of LuaSocket does not support IPv6."); print(" Please see https://prosody.im/doc/ipv6 for more information."); elseif host_ok_v6 and not use_ipv6 then print(" Host "..target_host.." has AAAA records, but IPv6 is disabled."); -- TODO Tell them to drop the AAAA records or enable IPv6? print(" Please see https://prosody.im/doc/ipv6 for more information."); end end if not all_targets_ok then print(" "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server."); if is_component then print(" DNS records are necessary if you want users on other servers to access this component."); end problem_hosts:add(host); end print(""); end if not problem_hosts:empty() then if not unknown_addresses:empty() then print(""); print("Some of your DNS records point to unknown IP addresses. This may be expected if your server"); print("is behind a NAT or proxy. The unrecognized addresses were:"); print(""); print(" Unrecognized: "..tostring(unknown_addresses)); print(""); print("The addresses we found on this system are:"); print(""); print(" Internal: "..tostring(internal_addresses)); print(" External: "..tostring(external_addresses)); print("") print("If the list of external external addresses is incorrect you can specify correct addresses in the config:") print(" external_addresses = { \"192.0.2.34\", \"2001:db8::abcd:1234\" }") end print(""); print("For more information about DNS configuration please see https://prosody.im/doc/dns"); print(""); ok = false; end end function checks.certs() local cert_ok; print"Checking certificates..." 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; -- or ssl.cert_from_pem if not ssl then print("LuaSec not available, can't perform certificate checks") if what == "certs" then cert_ok = false end elseif not load_cert then print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking"); 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 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 print(" No 'certificate' found for "..host) cert_ok = false elseif not ssl_config.key then print(" No 'key' found for "..host) cert_ok = false else local key, err = io.open(ssl_config.key); -- Permissions check only if not key then print(" Could not open "..ssl_config.key..": "..err); cert_ok = false else key:close(); end local cert_fh, err = io.open(ssl_config.certificate); -- Load the file. if not cert_fh then print(" Could not open "..ssl_config.certificate..": "..err); cert_ok = false else print(" Certificate: "..ssl_config.certificate) local cert = load_cert(cert_fh:read"*a"); cert_fh:close(); if not cert:validat(os.time()) then print(" Certificate has expired.") cert_ok = false elseif not cert:validat(os.time() + 86400) then print(" Certificate expires within one day.") cert_ok = false elseif not cert:validat(os.time() + 86400*7) then print(" Certificate expires within one week.") elseif not cert:validat(os.time() + 86400*31) then print(" Certificate expires within one month.") end 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 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 "..http_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 end if cert_ok == false then print("") print("For more information about certificates please see https://prosody.im/doc/certificates"); ok = false end print("") end -- intentionally not doing this by default 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."); print("Start it and then try again."); return 1; end local checker = "observe.jabber.network"; local probe_instance; local probe_modules = { ["xmpp-client"] = "c2s_normal_auth"; ["xmpp-server"] = "s2s_normal"; ["xmpps-client"] = nil; -- TODO ["xmpps-server"] = nil; -- TODO }; local probe_settings = api("*"):get_option_string("connectivity_probe"); if type(probe_settings) == "string" then probe_instance = probe_settings; elseif type(probe_settings) == "table" and type(probe_settings.url) == "string" then probe_instance = probe_settings.url; if type(probe_settings.modules) == "table" then probe_modules = probe_settings.modules; end elseif probe_settings ~= nil then print("The 'connectivity_probe' setting not understood."); print("Expected an URL or a table with 'url' and 'modules' fields"); print("See https://prosody.im/doc/prosodyctl#check for more information."); -- FIXME return 1; end local check_api; if probe_instance then local parsed_url = socket_url.parse(probe_instance); if not parsed_url then print(("'connectivity_probe' is not a valid URL: %q"):format(probe_instance)); print("Set it to the URL of an XMPP Blackbox Exporter instance and try again"); return 1; end checker = parsed_url.host; function check_api(protocol, host) local target = socket_url.build({scheme="xmpp",path=host}); local probe_module = probe_modules[protocol]; if not probe_module then return nil, "Checking protocol '"..protocol.."' is currently unsupported"; end return check_probe(probe_instance, probe_module, target); end else check_api = check_ojn; end for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do local modules, component_module = modulemanager.get_modules_for_host(host); if component_module then modules:add(component_module) end print("Checking external connectivity for "..host.." via "..checker) local function check_connectivity(protocol) local success, err = check_api(protocol, host); if not success and err ~= nil then print((" %s: Failed to request check at API: %s"):format(protocol, err)) elseif success then print((" %s: Works"):format(protocol)) else print((" %s: Check service failed to establish (secure) connection"):format(protocol)) ok = false end end if modules:contains("c2s") then check_connectivity("xmpp-client") if not api("*"):get_option_set("c2s_direct_tls_ports", {}):empty() then check_connectivity("xmpps-client"); end end if modules:contains("s2s") then check_connectivity("xmpp-server") if not api("*"):get_option_set("s2s_direct_tls_ports", {}):empty() then check_connectivity("xmpps-server"); end end print() end print("Note: The connectivity check only checks the reachability of the domain.") print("Note: It does not ensure that the check actually reaches this specific prosody instance.") end function checks.turn() local turn_enabled_hosts = {}; local turn_services = {}; for host in enabled_hosts() do local has_external_turn = modulemanager.get_modules_for_host(host):contains("turn_external"); if has_external_turn then local hostapi = api(host); table.insert(turn_enabled_hosts, host); local turn_host = hostapi:get_option_string("turn_external_host", host); local turn_port = hostapi:get_option_number("turn_external_port", 3478); local turn_secret = hostapi:get_option_string("turn_external_secret"); if not turn_secret then print("Error: Your configuration is missing a turn_external_secret for "..host); print("Error: TURN will not be advertised for this host."); ok = false; else local turn_id = ("%s:%d"):format(turn_host, turn_port); if turn_services[turn_id] and turn_services[turn_id].secret ~= turn_secret then print("Error: Your configuration contains multiple differing secrets"); print(" for the TURN service at "..turn_id.." - we will only test one."); elseif not turn_services[turn_id] then turn_services[turn_id] = { host = turn_host; port = turn_port; secret = turn_secret; }; end end end end if what == "turn" then local count = it.count(pairs(turn_services)); if count == 0 then print("Error: Unable to find any TURN services configured. Enable mod_turn_external!"); ok = false; else print("Identified "..tostring(count).." TURN services."); print(""); end end for turn_id, turn_service in pairs(turn_services) do print("Testing TURN service "..turn_id.."..."); local result = check_turn_service(turn_service, opts.ping); if #result.warnings > 0 then print(("%d warnings:\n"):format(#result.warnings)); print(" "..table.concat(result.warnings, "\n ")); print(""); end if opts.verbose then if result.external_ip then print(("External IP: %s"):format(result.external_ip.address)); end if result.relayed_addresses then for i, relayed_address in ipairs(result.relayed_addresses) do print(("Relayed address %d: %s:%d"):format(i, relayed_address.address, relayed_address.port)); end end if result.external_ip_pong then print(("TURN external address: %s:%d"):format(result.external_ip_pong.address, result.external_ip_pong.port)); end end if result.error then print("Error: "..result.error.."\n"); ok = false; else print("Success!\n"); end end end function checks.features(check_host, quiet) if not quiet then print("Feature report"); end local common_subdomains = { http_file_share = "share"; muc = "groups"; }; local recommended_component_modules = { muc = { "muc_mam" }; }; local function print_feature_status(feature, host) if quiet then return; end print("", feature.ok and "OK" or "(!)", feature.name); if feature.desc then print("", "", feature.desc); print(""); end if not feature.ok then if feature.lacking_modules then table.sort(feature.lacking_modules); print("", "", "Suggested modules: "); for _, module in ipairs(feature.lacking_modules) do print("", "", (" - %s: https://prosody.im/doc/modules/mod_%s"):format(module, module)); end end if feature.lacking_components then table.sort(feature.lacking_components); for _, component_module in ipairs(feature.lacking_components) do local subdomain = common_subdomains[component_module]; local recommended_mods = recommended_component_modules[component_module]; if subdomain then print("", "", "Suggested component:"); print(""); print("", "", "", ("-- Documentation: https://prosody.im/doc/modules/mod_%s"):format(component_module)); print("", "", "", ("Component %q %q"):format(subdomain.."."..host, component_module)); if recommended_mods then print("", "", "", " modules_enabled = {"); table.sort(recommended_mods); for _, mod in ipairs(recommended_mods) do print("", "", "", (" %q;"):format(mod)); end print("", "", "", " }"); end else print("", "", ("Suggested component: %s"):format(component_module)); end end print(""); print("", "", "If you have already configured any of these components, they may not be"); print("", "", "linked correctly to "..host..". For more info see https://prosody.im/doc/components"); end if feature.lacking_component_modules then table.sort(feature.lacking_component_modules, function (a, b) return a.host < b.host; end); for _, problem in ipairs(feature.lacking_component_modules) do local hostapi = api(problem.host); local current_modules_enabled = hostapi:get_option_array("modules_enabled", {}); print("", "", ("Component %q is missing the following modules: %s"):format(problem.host, table.concat(problem.missing_mods))); print(""); print("","", "Add the missing modules to your modules_enabled under the Component, like this:"); print(""); print(""); print("", "", "", ("-- Documentation: https://prosody.im/doc/modules/mod_%s"):format(problem.component_module)); print("", "", "", ("Component %q %q"):format(problem.host, problem.component_module)); print("", "", "", (" modules_enabled = {")); for _, mod in ipairs(current_modules_enabled) do print("", "", "", (" %q;"):format(mod)); end for _, mod in ipairs(problem.missing_mods) do print("", "", "", (" %q; -- Add this!"):format(mod)); end print("", "", "", (" }")); end end end if feature.meta then for k, v in it.sorted_pairs(feature.meta) do print("", "", (" - %s: %s"):format(k, v)); end end print(""); end local all_ok = true; local config = configmanager.getconfig(); local f, s, v; if check_host then f, s, v = it.values({ check_host }); else f, s, v = enabled_user_hosts(); end for host in f, s, v do local modules_enabled = set.new(config["*"].modules_enabled); modules_enabled:include(set.new(config[host].modules_enabled)); -- { [component_module] = { hostname1, hostname2, ... } } local host_components = setmetatable({}, { __index = function (t, k) return rawset(t, k, {})[k]; end }); do local hostapi = api(host); -- Find implicitly linked components for other_host in enabled_components() do local parent_host = other_host:match("^[^.]+%.(.+)$"); if parent_host == host then local component_module = configmanager.get(other_host, "component_module"); if component_module then table.insert(host_components[component_module], other_host); end end end -- And components linked explicitly for _, disco_item in ipairs(hostapi:get_option_array("disco_items", {})) do local other_host = disco_item[1]; if type(other_host) == "string" then local component_module = configmanager.get(other_host, "component_module"); if component_module then table.insert(host_components[component_module], other_host); end end end end local current_feature; local function check_module(suggested, alternate, ...) if set.intersection(modules_enabled, set.new({suggested, alternate, ...})):empty() then current_feature.lacking_modules = current_feature.lacking_modules or {}; table.insert(current_feature.lacking_modules, suggested); end end local function check_component(suggested, alternate, ...) local found; for _, component_module in ipairs({ suggested, alternate, ... }) do found = host_components[component_module][1]; if found then local enabled_component_modules = api(found):get_option_inherited_set("modules_enabled"); local recommended_mods = recommended_component_modules[component_module]; if recommended_mods then local missing_mods = {}; for _, mod in ipairs(recommended_mods) do if not enabled_component_modules:contains(mod) then table.insert(missing_mods, mod); end end if #missing_mods > 0 then if not current_feature.lacking_component_modules then current_feature.lacking_component_modules = {}; end table.insert(current_feature.lacking_component_modules, { host = found; component_module = component_module; missing_mods = missing_mods; }); end end break; end end if not found then current_feature.lacking_components = current_feature.lacking_components or {}; table.insert(current_feature.lacking_components, suggested); end return found; end local features = { { name = "Basic functionality"; desc = "Support for secure connections, authentication and messaging"; check = function () check_module("disco"); check_module("roster"); check_module("saslauth"); check_module("tls"); end; }; { name = "Multi-device messaging and data synchronization"; desc = "Multiple clients connected to the same account stay in sync"; check = function () check_module("carbons"); check_module("mam"); check_module("bookmarks"); check_module("pep"); end; }; { name = "Mobile optimizations"; desc = "Help mobile clients reduce battery and data usage"; check = function () check_module("smacks"); check_module("csi_simple", "csi_battery_saver"); end; }; { name = "Web connections"; desc = "Allow connections from browser-based web clients"; check = function () check_module("bosh"); check_module("websocket"); end; }; { name = "User profiles"; desc = "Enable users to publish profile information"; check = function () check_module("vcard_legacy", "vcard"); end; }; { name = "Blocking"; desc = "Block communication with chosen entities"; check = function () check_module("blocklist"); end; }; { name = "Push notifications"; desc = "Receive notifications on platforms that don't support persistent connections"; check = function () check_module("cloud_notify"); end; }; { name = "Audio/video calls and P2P"; desc = "Assist clients in setting up connections between each other"; check = function () check_module( "turn_external", "external_services", "turncredentials", "extdisco" ); end; }; { name = "File sharing"; desc = "Sharing of files to groups and offline users"; check = function (self) local service = check_component("http_file_share", "http_upload", "http_upload_external"); if service then local size_limit; if api(service):get_option("component_module") == "http_file_share" then size_limit = api(service):get_option_number("http_file_share_size_limit", 10*1024*1024); end if size_limit then self.meta = { ["Size limit"] = human_units.format(size_limit, "b", "b"); }; end end end; }; { name = "Group chats"; desc = "Create group chats and channels"; check = function () check_component("muc"); end; }; }; if not quiet then print(host); end for _, feature in ipairs(features) do current_feature = feature; feature:check(); feature.ok = ( not feature.lacking_modules and not feature.lacking_components and not feature.lacking_component_modules ); -- For improved presentation, we group the (ok) and (not ok) features if feature.ok then print_feature_status(feature, host); end end for _, feature in ipairs(features) do if not feature.ok then all_ok = false; print_feature_status(feature, host); end end if not quiet then print(""); end end return all_ok; 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."); else print("All checks passed, congratulations!"); end return ok and 0 or 2; end return { check = check; }; prosody-13.0.1/util/prosodyctl/PaxHeaders/shell.lua0000644000000000000000000000011714773555365017371 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.875711667 prosody-13.0.1/util/prosodyctl/shell.lua0000644000175000017500000001271114773555365021572 0ustar00prosodyprosody00000000000000local config = require "prosody.core.configmanager"; local human_io = require "prosody.util.human.io"; 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 "prosody.util.adminstream"; if have_readline then readline.set_readline_name("prosody"); readline.set_options({ histfile = path.join(prosody.paths.data, ".shell_history"); ignoredups = true; }); end local function read_line(prompt_string) if have_readline then return readline.readline(prompt_string); else io.write(prompt_string); return io.read("*line"); end end local function send_line(client, line, interactive) client.send(st.stanza("repl-input", { width = tostring(term_width()), repl = interactive == false and "0" or "1" }):text(line)); end local function repl(client) local line = read_line(client.prompt_string or "prosody> "); if not line or line == "quit" or line == "exit" or line == "bye" then if not line then print(""); end if have_readline then readline.save_history(); end os.exit(0, true); end send_line(client, line); end local function printbanner() local banner = config.get("*", "console_banner"); if banner then return print(banner); end print([[ ____ \ / _ | _ \ _ __ ___ ___ _-_ __| |_ _ | |_) | '__/ _ \/ __|/ _ \ / _` | | | | | __/| | | (_) \__ \ |_| | (_| | |_| | |_| |_| \___/|___/\___/ \__,_|\__, | A study in simplicity |___/ ]]); print("Welcome to the Prosody administration console. For a list of commands, type: help"); print("You may find more help on using this console in our online documentation at "); print("https://prosody.im/doc/console\n"); end local function check() local lfs = require "lfs"; local socket_path = path.resolve_relative_path(prosody.paths.data, config.get("*", "admin_socket") or "prosody.sock"); local state = lfs.attributes(socket_path, "mode"); return state == "socket"; 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 print("Unknown command-line option: "..tostring(where)); elseif err == "missing-value" then print("Expected a value to follow command-line option: "..where); end os.exit(1); end if arg[1] then if arg[2] then arg[1] = ("{"..string.rep("%q", #arg, ", ").."}"):format(table.unpack(arg, 1, #arg)); end client.events.add_handler("connected", function() send_line(client, arg[1], false); 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; dest = io.stderr; end if stanza.attr.eol == "0" then dest:write(stanza:get_text()); else dest:write(stanza:get_text(), "\n"); end end if stanza.name == "repl-result" then os.exit(errors); end return true; end, 1); end client.events.add_handler("connected", function () if not opts.quiet then printbanner(); end repl(client); end); client.events.add_handler("disconnected", function () print("--- session closed ---"); os.exit(0, true); end); client.events.add_handler("received", function (stanza) if stanza.name ~= "repl-request-input" then return; end if stanza.attr.type == "password" then local password = human_io.read_password(); client.send(st.stanza("repl-requested-input", { type = stanza.attr.type; id = stanza.attr.id; status = password and "submit" or "cancel"; }):text(password or "")); else io.stderr:write("Internal error - unexpected input request type "..tostring(stanza.attr.type).."\n"); os.exit(1); end return true; end, 2); 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 "|"; 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); end end); client.prompt_string = config.get("*", "admin_shell_prompt"); local socket_path = path.resolve_relative_path(prosody.paths.data, opts.socket or config.get("*", "admin_socket") or "prosody.sock"); local conn = adminstream.connection(socket_path, client.listeners); local ok, err = conn:connect(); if not ok then if err == "no unix socket support" then print("** LuaSocket unix socket support not available or incompatible, ensure your"); print("** version is up to date."); else print("** Unable to connect to server - is it running? Is mod_admin_shell enabled?"); print("** Connection error: "..err); end os.exit(1); end server.loop(); end return { shell = start; available = check; }; prosody-13.0.1/util/PaxHeaders/prosodyctl.lua0000644000000000000000000000011714773555365016262 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.871711651 prosody-13.0.1/util/prosodyctl.lua0000644000175000017500000001761614773555365020474 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local config = require "prosody.core.configmanager"; local encodings = require "prosody.util.encodings"; local stringprep = encodings.stringprep; 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; local print = print; local tonumber = tonumber; local _G = _G; local prosody = prosody; local error_messages = setmetatable({ ["invalid-username"] = "The given username is invalid in a Jabber ID"; ["invalid-hostname"] = "The given hostname is invalid"; ["no-password"] = "No password was supplied"; ["no-such-user"] = "The given user does not exist on the server"; ["no-such-host"] = "The given hostname does not exist in the config"; ["unable-to-save-data"] = "Unable to store, perhaps you don't have permission?"; ["no-pidfile"] = "There is no 'pidfile' option in the configuration file, see https://prosody.im/doc/prosodyctl#pidfile for help"; ["invalid-pidfile"] = "The 'pidfile' option in the configuration file is not a string, see https://prosody.im/doc/prosodyctl#pidfile for help"; ["pidfile-not-locked"] = "Stale pidfile found. Prosody is probably not running."; ["no-posix"] = "The mod_posix module is not enabled in the Prosody config file, see https://prosody.im/doc/prosodyctl for more info"; ["no-such-method"] = "This module has no commands"; ["not-running"] = "Prosody is not running"; }, { __index = function (_,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end }); -- UI helpers local show_message = require "prosody.util.human.io".printf; local function show_usage(usage, desc) print("Usage: ".._G.arg[0].." "..usage); if desc then print(" "..desc); end end local function show_module_configuration_help(mod_name) print("Done.") print("If you installed a prosody plugin, don't forget to add its name under the 'modules_enabled' section inside your configuration file.") print("Depending on the module, there might be further configuration steps required.") print("") print("More info about: ") print(" modules_enabled: https://prosody.im/doc/modules_enabled") print(" "..mod_name..": https://modules.prosody.im/"..mod_name..".html") end -- Server control local function adduser(params) local user, host, password = nodeprep(params.user, true), nameprep(params.host), params.password; if not user then return false, "invalid-username"; elseif not host then return false, "invalid-hostname"; end local host_session = prosody.hosts[host]; if not host_session then return false, "no-such-host"; end storagemanager.initialize_host(host); local provider = host_session.users; if not(provider) or provider.name == "null" then usermanager.initialize_host(host); end local ok, errmsg = usermanager.create_user(user, password, host); if not ok then return false, errmsg or "creating-user-failed"; end return true; end local function user_exists(params) local user, host = nodeprep(params.user), nameprep(params.host); storagemanager.initialize_host(host); local provider = prosody.hosts[host].users; if not(provider) or provider.name == "null" then usermanager.initialize_host(host); end return usermanager.user_exists(user, host); end local function passwd(params) if not user_exists(params) then return false, "no-such-user"; end return adduser(params); end local function deluser(params) if not user_exists(params) then return false, "no-such-user"; end local user, host = nodeprep(params.user), nameprep(params.host); return usermanager.delete_user(user, host); end local function getpid() local pidfile = config.get("*", "pidfile"); if not pidfile then return false, "no-pidfile"; end if type(pidfile) ~= "string" then return false, "invalid-pidfile"; end pidfile = config.resolve_relative_path(prosody.paths.data, pidfile); local modules_disabled = set.new(config.get("*", "modules_disabled")); if prosody.platform ~= "posix" or modules_disabled:contains("posix") then return false, "no-posix"; end local file, err = io.open(pidfile, "r+"); if not file then return false, "pidfile-read-failed", err; end -- Check for a lock on the file local locked, err = lfs.lock(file, "w"); -- luacheck: ignore 211/err if locked then -- Prosody keeps the pidfile locked while it is running. -- We successfully locked the file, which means Prosody is not -- running and the pidfile is stale (somehow it was not -- cleaned up). We'll abort here, to avoid sending signals to -- a non-Prosody PID. file:close(); return false, "pidfile-not-locked"; end local pid = tonumber(file:read("*a")); file:close(); if not pid then return false, "invalid-pid"; end return true, pid; end local function isrunning() local ok, pid, err = getpid(); -- luacheck: ignore 211/err if not ok then if pid == "pidfile-read-failed" or pid == "pidfile-not-locked" then -- Report as not running, since we can't open the pidfile -- (it probably doesn't exist) return true, false; end return ok, pid; end return true, signal.kill(pid, 0) == 0; end local function start(source_dir, lua) lua = lua and lua .. " " or ""; local ok, ret = isrunning(); if not ok then return ok, ret; end 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 local function stop() local ok, ret = isrunning(); if not ok then return ok, ret; end if not ret then return false, "not-running"; end local ok, pid = getpid() if not ok then return false, pid; end signal.kill(pid, signal.SIGTERM); return true; end local function reload() local ok, ret = isrunning(); if not ok then return ok, ret; end if not ret then return false, "not-running"; end local ok, pid = getpid() if not ok then return false, pid; end signal.kill(pid, signal.SIGHUP); return true; end local render_cli = interpolation.new("%b{}", function (s) return "'"..s:gsub("'","'\\''").."'" end) local function call_luarocks(operation, mod, server) local dir = prosody.paths.installer; 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"); })); return ok and code; end return { show_message = show_message; show_warning = show_message; show_usage = show_usage; show_module_configuration_help = show_module_configuration_help; adduser = adduser; user_exists = user_exists; passwd = passwd; deluser = deluser; getpid = getpid; isrunning = isrunning; start = start; stop = stop; reload = reload; call_luarocks = call_luarocks; error_messages = error_messages; }; prosody-13.0.1/util/PaxHeaders/pubsub.lua0000644000000000000000000000011714773555365015360 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.879711682 prosody-13.0.1/util/pubsub.lua0000644000175000017500000005624014773555365017566 0ustar00prosodyprosody00000000000000local events = require "prosody.util.events"; local cache = require "prosody.util.cache"; local service_mt = {}; local default_config = { max_items = 256; itemstore = function (config, _) return cache.new(config["max_items"]) end; broadcaster = function () end; subscriber_filter = function (subs) return subs end; itemcheck = function () return true; end; get_affiliation = function () end; normalize_jid = function (jid) return jid; end; metadata_subset = {}; capabilities = { outcast = { create = false; publish = false; retract = false; get_nodes = false; subscribe = false; unsubscribe = false; get_subscription = true; get_subscriptions = true; get_items = false; subscribe_other = false; unsubscribe_other = false; get_subscription_other = false; get_subscriptions_other = false; be_subscribed = false; be_unsubscribed = true; set_affiliation = false; }; none = { create = false; publish = false; retract = false; get_nodes = true; subscribe = true; unsubscribe = true; get_subscription = true; get_subscriptions = true; get_items = false; get_metadata = true; subscribe_other = false; unsubscribe_other = false; get_subscription_other = false; get_subscriptions_other = false; be_subscribed = true; be_unsubscribed = true; set_affiliation = false; }; member = { create = false; publish = false; retract = false; get_nodes = true; subscribe = true; unsubscribe = true; get_subscription = true; get_subscriptions = true; get_items = true; get_metadata = true; subscribe_other = false; unsubscribe_other = false; get_subscription_other = false; get_subscriptions_other = false; be_subscribed = true; be_unsubscribed = true; set_affiliation = false; }; publisher = { create = false; publish = true; retract = true; get_nodes = true; get_configuration = true; subscribe = true; unsubscribe = true; get_subscription = true; get_subscriptions = true; get_items = true; get_metadata = true; subscribe_other = false; unsubscribe_other = false; get_subscription_other = false; get_subscriptions_other = false; be_subscribed = true; be_unsubscribed = true; set_affiliation = false; }; owner = { create = true; publish = true; retract = true; delete = true; get_nodes = true; configure = true; get_configuration = true; subscribe = true; unsubscribe = true; get_subscription = true; get_subscriptions = true; get_items = true; get_metadata = true; subscribe_other = true; unsubscribe_other = true; get_subscription_other = true; get_subscriptions_other = true; be_subscribed = true; be_unsubscribed = true; set_affiliation = true; }; }; }; local default_config_mt = { __index = default_config }; local default_node_config = { ["persist_items"] = true; ["max_items"] = 20; ["access_model"] = "open"; ["publish_model"] = "publishers"; ["send_last_published_item"] = "never"; }; local default_node_config_mt = { __index = default_node_config }; -- Storage helper functions local function load_node_from_store(service, node_name) local node = service.config.nodestore:get(node_name); node.config = setmetatable(node.config or {}, {__index=service.node_defaults}); return node; end local function save_node_to_store(service, node) return service.config.nodestore:set(node.name, { name = node.name; config = node.config; subscribers = node.subscribers; affiliations = node.affiliations; }); end local function delete_node_in_store(service, node_name) return service.config.nodestore:set(node_name, nil); end -- Create and return a new service object local function new(config) config = config or {}; local service = setmetatable({ config = setmetatable(config, default_config_mt); node_defaults = setmetatable(config.node_defaults or {}, default_node_config_mt); affiliations = {}; subscriptions = {}; nodes = {}; data = {}; events = events.new(); }, service_mt); -- Load nodes from storage, if we have a store and it supports iterating over stored items if config.nodestore and config.nodestore.users then for node_name in config.nodestore:users() do local node = load_node_from_store(service, node_name); service.nodes[node_name] = node; if node.config.persist_items then service.data[node_name] = config.itemstore(service.nodes[node_name].config, node_name); end for jid in pairs(service.nodes[node_name].subscribers) do local normal_jid = service.config.normalize_jid(jid); local subs = service.subscriptions[normal_jid]; if subs then if not subs[jid] then subs[jid] = { [node_name] = true }; else subs[jid][node_name] = true; end else service.subscriptions[normal_jid] = { [jid] = { [node_name] = true } }; end end end end return service; end --- Service methods local service = {}; service_mt.__index = service; function service:jids_equal(jid1, jid2) --> boolean local normalize = self.config.normalize_jid; return normalize(jid1) == normalize(jid2); end function service:may(node, actor, action) --> boolean if actor == true then return true; end local node_obj = self.nodes[node]; local node_aff = node_obj and (node_obj.affiliations[actor] or node_obj.affiliations[self.config.normalize_jid(actor)]); local service_aff = self.affiliations[actor] or self.config.get_affiliation(actor, node, action); local default_aff = self:get_default_affiliation(node, actor) or "none"; -- Check if node allows/forbids it local node_capabilities = node_obj and node_obj.capabilities; if node_capabilities then local caps = node_capabilities[node_aff or service_aff or default_aff]; if caps then local can = caps[action]; if can ~= nil then return can; end end end -- Check service-wide capabilities instead local service_capabilities = self.config.capabilities; local caps = service_capabilities[node_aff or service_aff or default_aff]; if caps then local can = caps[action]; if can ~= nil then return can; end end return false; end function service:get_default_affiliation(node, actor) --> affiliation local node_obj = self.nodes[node]; local access_model = node_obj and node_obj.config.access_model or self.node_defaults.access_model; if access_model == "open" then return "member"; elseif access_model == "whitelist" then return "outcast"; end if self.config.access_models then local check = self.config.access_models[access_model]; if check then local aff = check(actor, node_obj); if aff then return aff; end end end end function service:set_affiliation(node, actor, jid, affiliation) --> ok, err -- Access checking if not self:may(node, actor, "set_affiliation") then return false, "forbidden"; end -- local node_obj = self.nodes[node]; if not node_obj then return false, "item-not-found"; end jid = self.config.normalize_jid(jid); local old_affiliation = node_obj.affiliations[jid]; node_obj.affiliations[jid] = affiliation; if self.config.nodestore then -- TODO pass the error from storage to caller eg wrapped in an util.error local ok, err = save_node_to_store(self, node_obj); -- luacheck: ignore 211/err if not ok then node_obj.affiliations[jid] = old_affiliation; return ok, "internal-server-error"; end end local _, jid_sub = self:get_subscription(node, true, jid); if not jid_sub and not self:may(node, jid, "be_unsubscribed") then local ok, err = self:add_subscription(node, true, jid); if not ok then return ok, err; end elseif jid_sub and not self:may(node, jid, "be_subscribed") then local ok, err = self:add_subscription(node, true, jid); if not ok then return ok, err; end end return true; end function service:add_subscription(node, actor, jid, options) --> ok, err -- Access checking local cap; if actor == true or jid == actor or self:jids_equal(actor, jid) then cap = "subscribe"; else cap = "subscribe_other"; end if not self:may(node, actor, cap) then return false, "forbidden"; end if not self:may(node, jid, "be_subscribed") then return false, "forbidden"; end -- local node_obj = self.nodes[node]; if not node_obj then if not self.config.autocreate_on_subscribe then return false, "item-not-found"; else local ok, err = self:create(node, true); if not ok then return ok, err; end node_obj = self.nodes[node]; end end local old_subscription = node_obj.subscribers[jid]; node_obj.subscribers[jid] = options or true; local normal_jid = self.config.normalize_jid(jid); local subs = self.subscriptions[normal_jid]; if subs then if not subs[jid] then subs[jid] = { [node] = true }; else subs[jid][node] = true; end else self.subscriptions[normal_jid] = { [jid] = { [node] = true } }; end if self.config.nodestore then -- TODO pass the error from storage to caller eg wrapped in an util.error local ok, err = save_node_to_store(self, node_obj); -- luacheck: ignore 211/err if not ok then node_obj.subscribers[jid] = old_subscription; self.subscriptions[normal_jid][jid][node] = old_subscription and true or nil; return ok, "internal-server-error"; end end self.events.fire_event("subscription-added", { service = self, node = node, jid = jid, normalized_jid = normal_jid, options = options }); return true; end function service:remove_subscription(node, actor, jid) --> ok, err -- Access checking local cap; if actor == true or jid == actor or self:jids_equal(actor, jid) then cap = "unsubscribe"; else cap = "unsubscribe_other"; end if not self:may(node, actor, cap) then return false, "forbidden"; end if not self:may(node, jid, "be_unsubscribed") then return false, "forbidden"; end -- local node_obj = self.nodes[node]; if not node_obj then return false, "item-not-found"; end if not node_obj.subscribers[jid] then return false, "not-subscribed"; end local old_subscription = node_obj.subscribers[jid]; node_obj.subscribers[jid] = nil; local normal_jid = self.config.normalize_jid(jid); local subs = self.subscriptions[normal_jid]; if subs then local jid_subs = subs[jid]; if jid_subs then jid_subs[node] = nil; if next(jid_subs) == nil then subs[jid] = nil; end end if next(subs) == nil then self.subscriptions[normal_jid] = nil; end end if self.config.nodestore then -- TODO pass the error from storage to caller eg wrapped in an util.error local ok, err = save_node_to_store(self, node_obj); -- luacheck: ignore 211/err if not ok then node_obj.subscribers[jid] = old_subscription; self.subscriptions[normal_jid][jid][node] = old_subscription and true or nil; return ok, "internal-server-error"; end end self.events.fire_event("subscription-removed", { service = self, node = node, jid = jid, normalized_jid = normal_jid }); return true; end function service:get_subscription(node, actor, jid) --> (true, subscription) or (false, err) -- Access checking local cap; if actor == true or jid == actor or self:jids_equal(actor, jid) then cap = "get_subscription"; else cap = "get_subscription_other"; end if not self:may(node, actor, cap) then return false, "forbidden"; end -- local node_obj = self.nodes[node]; if not node_obj then return false, "item-not-found"; end return true, node_obj.subscribers[jid]; end function service:create(node, actor, options) --> ok, err -- Access checking if not self:may(node, actor, "create") then return false, "forbidden"; end -- if self.nodes[node] then return false, "conflict"; end local config = setmetatable(options or {}, {__index=self.node_defaults}); if self.config.check_node_config then local ok = self.config.check_node_config(node, actor, config); if not ok then return false, "not-acceptable"; end end self.nodes[node] = { name = node; subscribers = {}; config = config; affiliations = {}; }; if self.config.nodestore then -- TODO pass the error from storage to caller eg wrapped in an util.error local ok, err = save_node_to_store(self, self.nodes[node]); -- luacheck: ignore 211/err if not ok then self.nodes[node] = nil; return ok, "internal-server-error"; end end if config.persist_items then self.data[node] = self.config.itemstore(self.nodes[node].config, node); end self.events.fire_event("node-created", { service = self, node = node, actor = actor }); if actor ~= true then local ok, err = self:set_affiliation(node, true, actor, "owner"); if not ok then self.nodes[node] = nil; self.data[node] = nil; return ok, err; end end return true; end function service:delete(node, actor) --> ok, err -- Access checking if not self:may(node, actor, "delete") then return false, "forbidden"; end -- local node_obj = self.nodes[node]; if not node_obj then return false, "item-not-found"; end self.nodes[node] = nil; if self.data[node] and self.data[node].clear then self.data[node]:clear(); end self.data[node] = nil; if self.config.nodestore then local ok, err = delete_node_in_store(self, node); if not ok then self.nodes[node] = nil; return ok, err; end end self.events.fire_event("node-deleted", { service = self, node = node, actor = actor }); self:broadcast("delete", node, node_obj.subscribers, nil, actor, node_obj); return true; end -- Used to check that the config of a node is as expected (i.e. 'publish-options') local function check_preconditions(node_config, required_config) if not (node_config and required_config) then return false; end for config_field, value in pairs(required_config) do if node_config[config_field] ~= value then return false, config_field; end end return true; end function service:publish(node, actor, id, item, requested_config) --> ok, err -- Access checking local may_publish = false; if self:may(node, actor, "publish") then may_publish = true; else local node_obj = self.nodes[node]; local publish_model = node_obj and node_obj.config.publish_model; if publish_model == "open" or (publish_model == "subscribers" and node_obj.subscribers[actor]) then may_publish = true; end end if not may_publish then return false, "forbidden"; end -- local node_obj = self.nodes[node]; if not node_obj then if not self.config.autocreate_on_publish then return false, "item-not-found"; end local ok, err = self:create(node, true, requested_config); if not ok then return ok, err; end node_obj = self.nodes[node]; elseif requested_config and not requested_config._defaults_only then -- Check that node has the requested config before we publish local ok, field = check_preconditions(node_obj.config, requested_config); if not ok then return false, "precondition-not-met", { field = field }; end end if not self.config.itemcheck(item) then return nil, "invalid-item"; end if node_obj.config.persist_items then if not self.data[node] then self.data[node] = self.config.itemstore(self.nodes[node].config, node); end local ok = self.data[node]:set(id, item); if not ok then return nil, "internal-server-error"; end if type(ok) == "string" then id = ok; end end local event_data = { service = self, node = node, actor = actor, id = id, item = item }; self.events.fire_event("item-published/"..node, event_data); self.events.fire_event("item-published", event_data); self:broadcast("items", node, node_obj.subscribers, item, actor, node_obj); return true; end function service:broadcast(event, node, subscribers, item, actor, node_obj) subscribers = self.config.subscriber_filter(subscribers, node, event); return self.config.broadcaster(event, node, subscribers, item, actor, node_obj, self); end function service:retract(node, actor, id, retract) --> ok, err -- Access checking if not self:may(node, actor, "retract") then return false, "forbidden"; end -- local node_obj = self.nodes[node]; if not node_obj then return false, "item-not-found"; end if self.data[node] then if not self.data[node]:get(id) then return false, "item-not-found"; end local ok = self.data[node]:set(id, nil); if not ok then return nil, "internal-server-error"; end end self.events.fire_event("item-retracted", { service = self, node = node, actor = actor, id = id }); if retract then self:broadcast("retract", node, node_obj.subscribers, retract, actor, node_obj); end return true end function service:purge(node, actor, notify) --> ok, err -- Access checking if not self:may(node, actor, "retract") then return false, "forbidden"; end -- local node_obj = self.nodes[node]; if not node_obj then return false, "item-not-found"; end if self.data[node] then if self.data[node].clear then self.data[node]:clear() else self.data[node] = self.config.itemstore(self.nodes[node].config, node); end end self.events.fire_event("node-purged", { service = self, node = node, actor = actor }); if notify then self:broadcast("purge", node, node_obj.subscribers, nil, actor, node_obj); end return true end function service:get_items(node, actor, ids, resultspec) --> (true, { id, [id] = node }) or (false, err) -- Access checking if not self:may(node, actor, "get_items") then return false, "forbidden"; end -- local node_obj = self.nodes[node]; if not node_obj then return false, "item-not-found"; end if not self.data[node] then -- Disabled rather than unsupported, but close enough. return false, "persistent-items-unsupported"; end if type(ids) == "string" then -- COMPAT see #1305 ids = { ids }; end local data = {}; local limit = resultspec and resultspec.max; if ids then for _, key in ipairs(ids) do local value = self.data[node]:get(key); if value then data[#data+1] = key; data[key] = value; -- Limits and ids seem like a problematic combination. if limit and #data >= limit then break end end end else for key, value in self.data[node]:items() do data[#data+1] = key; data[key] = value; if limit and #data >= limit then break end end end return true, data; end function service:get_last_item(node, actor) --> (true, id, node) or (false, err) -- Access checking if not self:may(node, actor, "get_items") then return false, "forbidden"; end -- -- Check node exists if not self.nodes[node] then return false, "item-not-found"; end if not self.data[node] then -- FIXME Should this be a success or failure? return true, nil; end -- Returns success, id, item return true, self.data[node]:head(); end function service:get_nodes(actor) --> (true, map) or (false, err) -- Access checking if not self:may(nil, actor, "get_nodes") then return false, "forbidden"; end -- return true, self.nodes; end local function flatten_subscriptions(ret, serv, subs, node, node_obj) for subscribed_jid, subscribed_nodes in pairs(subs) do if node then -- Return only subscriptions to this node if subscribed_nodes[node] then ret[#ret+1] = { node = node; jid = subscribed_jid; subscription = node_obj.subscribers[subscribed_jid]; }; end else -- Return subscriptions to all nodes local nodes = serv.nodes; for subscribed_node in pairs(subscribed_nodes) do ret[#ret+1] = { node = subscribed_node; jid = subscribed_jid; subscription = nodes[subscribed_node].subscribers[subscribed_jid]; }; end end end end function service:get_subscriptions(node, actor, jid) --> (true, array) or (false, err) -- Access checking local cap; if actor == true or jid == actor or self:jids_equal(actor, jid) then cap = "get_subscriptions"; else cap = "get_subscriptions_other"; end if not self:may(node, actor, cap) then return false, "forbidden"; end -- local node_obj; if node then node_obj = self.nodes[node]; if not node_obj then return false, "item-not-found"; end end local ret = {}; if jid == nil then for _, subs in pairs(self.subscriptions) do flatten_subscriptions(ret, self, subs, node, node_obj) end return true, ret; end local normal_jid = self.config.normalize_jid(jid); local subs = self.subscriptions[normal_jid]; -- We return the subscription object from the node to save -- a get_subscription() call for each node. if subs then flatten_subscriptions(ret, self, subs, node, node_obj) end return true, ret; end -- Access models only affect 'none' affiliation caps, service/default access level... function service:set_node_capabilities(node, actor, capabilities) --> ok, err -- Access checking if not self:may(node, actor, "configure") then return false, "forbidden"; end -- local node_obj = self.nodes[node]; if not node_obj then return false, "item-not-found"; end node_obj.capabilities = capabilities; return true; end function service:set_node_config(node, actor, new_config) --> ok, err if not self:may(node, actor, "configure") then return false, "forbidden"; end local node_obj = self.nodes[node]; if not node_obj then return false, "item-not-found"; end setmetatable(new_config, {__index=self.node_defaults}) if self.config.check_node_config then local ok = self.config.check_node_config(node, actor, new_config); if not ok then return false, "not-acceptable"; end end local old_config = node_obj.config; node_obj.config = new_config; if self.config.nodestore then -- TODO pass the error from storage to caller eg wrapped in an util.error local ok, err = save_node_to_store(self, node_obj); -- luacheck: ignore 211/err if not ok then node_obj.config = old_config; return ok, "internal-server-error"; end end if old_config["access_model"] ~= node_obj.config["access_model"] then for subscriber in pairs(node_obj.subscribers) do if not self:may(node, subscriber, "be_subscribed") then local ok, err = self:remove_subscription(node, true, subscriber); if not ok then node_obj.config = old_config; return ok, err; end end end end if old_config["persist_items"] ~= node_obj.config["persist_items"] then if node_obj.config["persist_items"] then self.data[node] = self.config.itemstore(self.nodes[node].config, node); elseif self.data[node] then if self.data[node].clear then self.data[node]:clear() end self.data[node] = nil; end elseif old_config["max_items"] ~= node_obj.config["max_items"] then if self.data[node] then local max_items = self.nodes[node].config["max_items"]; if max_items == "max" then max_items = self.config.max_items; end self.data[node]:resize(max_items); end end return true; end function service:get_node_config(node, actor) --> (true, config) or (false, err) if not self:may(node, actor, "get_configuration") then return false, "forbidden"; end local node_obj = self.nodes[node]; if not node_obj then return false, "item-not-found"; end local config_table = {}; for k, v in pairs(default_node_config) do config_table[k] = v; end for k, v in pairs(self.node_defaults) do config_table[k] = v; end for k, v in pairs(node_obj.config) do config_table[k] = v; end 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; }; prosody-13.0.1/util/PaxHeaders/queue.lua0000644000000000000000000000011714773555365015204 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.879711682 prosody-13.0.1/util/queue.lua0000644000175000017500000000354214773555365017407 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2015 Matthew Wild -- Copyright (C) 2008-2015 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- 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, "prosody.util.table"); -- For pre-allocation of table local function new(size, allow_wrapping) -- Head is next insert, tail is next read local head, tail = 1, 1; local items = 0; -- Number of stored items local t = have_utable and utable.create(size, 0) or {}; -- Table to hold items --luacheck: ignore 212/self return { _items = t; size = size; count = function (self) return items; end; push = function (self, item) if items >= size then if allow_wrapping then tail = (tail%size)+1; -- Advance to next oldest item items = items - 1; else return nil, "queue full"; end end t[head] = item; items = items + 1; head = (head%size)+1; return true; end; pop = function (self) if items == 0 then return nil; end local item; item, t[tail] = t[tail], 0; tail = (tail%size)+1; items = items - 1; return item; end; peek = function (self) if items == 0 then return nil; end return t[tail]; end; replace = function (self, data) if items == 0 then return self:push(data); end t[tail] = data; return true; end; items = function (self) return function (_, pos) if pos >= items then return nil; end local read_pos = tail + pos; if read_pos > self.size then read_pos = (read_pos%size); end return pos+1, t[read_pos]; end, self, 0; end; consume = function (self) return self.pop, self; end; }; end return { new = new; }; prosody-13.0.1/util/PaxHeaders/random.lua0000644000000000000000000000011714773555365015340 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.879711682 prosody-13.0.1/util/random.lua0000644000175000017500000000165414773555365017545 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2014 Matthew Wild -- Copyright (C) 2008-2014 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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"); local function bytes(n) local data, err = urandom:read(n); if not data then if err then error("Unable to retrieve data from secure random number generator (/dev/urandom): "..tostring(err)); else error("Secure random number generator (/dev/urandom) returned an end-of-file condition"); end end return data; end if not urandom then function bytes() error("Unable to obtain a secure random number generator, please see https://prosody.im/doc/random ("..urandom_err..")"); end end return { bytes = bytes; _source = "/dev/urandom"; }; prosody-13.0.1/util/PaxHeaders/roles.lua0000644000000000000000000000011714773555365015204 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.879711682 prosody-13.0.1/util/roles.lua0000644000175000017500000000570314773555365017410 0ustar00prosodyprosody00000000000000local 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; }; prosody-13.0.1/util/PaxHeaders/rsm.lua0000644000000000000000000000011714773555365014661 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.883711699 prosody-13.0.1/util/rsm.lua0000644000175000017500000000451314773555365017063 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2017 Matthew Wild -- Copyright (C) 2008-2017 Waqas Hussain -- Copyright (C) 2011-2017 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- XEP-0313: Message Archive Management for Prosody -- local stanza = require"prosody.util.stanza".stanza; local tonumber = tonumber; local s_format = string.format; local type = type; local pairs = pairs; local function inttostr(n) return s_format("%d", n); end local xmlns_rsm = 'http://jabber.org/protocol/rsm'; local element_parsers = {}; do local parsers = element_parsers; local function xs_int(st) return tonumber((st:get_text())); end local function xs_string(st) return st:get_text(); end parsers.after = xs_string; parsers.before = function(st) local text = st:get_text(); return text == "" or text; end; parsers.max = xs_int; parsers.index = xs_int; parsers.first = function(st) return { index = tonumber(st.attr.index); st:get_text() }; end; parsers.last = xs_string; parsers.count = xs_int; end local element_generators = setmetatable({ first = function(st, data) if type(data) == "table" then st:tag("first", { index = inttostr(data.index) }):text(data[1]):up(); else st:text_tag("first", data); end end; before = function(st, data) if data == true then st:tag("before"):up(); else st:text_tag("before", data); end end; max = function (st, data) st:text_tag("max", inttostr(data)); end; index = function (st, data) st:text_tag("index", inttostr(data)); end; count = function (st, data) st:text_tag("count", inttostr(data)); end; }, { __index = function(_, name) return function(st, data) st:text_tag(name, data); end end; }); local function parse(set) local rs = {}; for tag in set:childtags() do local name = tag.name; local parser = name and element_parsers[name]; if parser then rs[name] = parser(tag); end end return rs; end local function generate(t) local st = stanza("set", { xmlns = xmlns_rsm }); for k,v in pairs(t) do if element_parsers[k] then element_generators[k](st, v); end end return st; end local function get(st) local set = st:get_child("set", xmlns_rsm); if set and #set.tags > 0 then return parse(set); end end return { parse = parse, generate = generate, get = get }; prosody-13.0.1/util/PaxHeaders/sasl0000644000000000000000000000013114773555365014236 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.883711699 30 ctime=1743706869.887711715 prosody-13.0.1/util/sasl/0000755000175000017500000000000014773555365016516 5ustar00prosodyprosody00000000000000prosody-13.0.1/util/sasl/PaxHeaders/anonymous.lua0000644000000000000000000000011714773555365017052 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.883711699 prosody-13.0.1/util/sasl/anonymous.lua0000644000175000017500000000441514773555365021255 0ustar00prosodyprosody00000000000000-- sasl.lua v0.4 -- Copyright (C) 2008-2010 Tobias Markmann -- -- All rights reserved. -- -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -- -- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -- -- 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 "prosody.util.id".medium; local _ENV = nil; -- luacheck: std none --========================= --SASL ANONYMOUS according to RFC 4505 --[[ Supported Authentication Backends anonymous: function(username, realm) return true; --for normal usage just return true; if you don't like the supplied username you can return false. end ]] local function anonymous(self, message) -- luacheck: ignore 212/message local username; repeat username = generate_random_id():lower(); self.username = username; until self.profile.anonymous(self, username, self.realm, message); return "success" end local function init(registerMechanism) registerMechanism("ANONYMOUS", {"anonymous"}, anonymous); end return { init = init; } prosody-13.0.1/util/sasl/PaxHeaders/external.lua0000644000000000000000000000011714773555365016644 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.883711699 prosody-13.0.1/util/sasl/external.lua0000644000175000017500000000114114773555365021040 0ustar00prosodyprosody00000000000000local saslprep = require "prosody.util.encodings".stringprep.saslprep; local _ENV = nil; -- luacheck: std none local function external(self, message) message = saslprep(message); local state self.username, state = self.profile.external(message); if state == false then return "failure", "account-disabled"; elseif state == nil then return "failure", "not-authorized"; elseif state == "expired" then return "false", "credentials-expired"; end return "success"; end local function init(registerMechanism) registerMechanism("EXTERNAL", {"external"}, external); end return { init = init; } prosody-13.0.1/util/sasl/PaxHeaders/oauthbearer.lua0000644000000000000000000000011714773555365017323 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.887711715 prosody-13.0.1/util/sasl/oauthbearer.lua0000644000175000017500000000313714773555365021526 0ustar00prosodyprosody00000000000000local 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; } prosody-13.0.1/util/sasl/PaxHeaders/plain.lua0000644000000000000000000000011714773555365016125 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.887711715 prosody-13.0.1/util/sasl/plain.lua0000644000175000017500000000732314773555365020331 0ustar00prosodyprosody00000000000000-- sasl.lua v0.4 -- Copyright (C) 2008-2010 Tobias Markmann -- -- All rights reserved. -- -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -- -- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -- -- 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 "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 -- ================================ -- SASL PLAIN according to RFC 4616 --[[ Supported Authentication Backends plain: function(username, realm) return password, state; end plain_test: function(username, password, realm) return true or false, state; end ]] local function plain(self, message) if not message then return "failure", "malformed-request"; end local authorization, authentication, password = s_match(message, "^([^%z]*)%z([^%z]+)%z([^%z]+)"); if not authorization then return "failure", "malformed-request"; end -- SASLprep password and authentication authentication = saslprep(authentication); password = saslprep(password); if (not password) or (password == "") or (not authentication) or (authentication == "") then log("debug", "Username or password violates SASLprep."); return "failure", "malformed-request", "Invalid username or password."; end local _nodeprep = self.profile.nodeprep; if _nodeprep ~= false then authentication = (_nodeprep or nodeprep)(authentication); if not authentication or authentication == "" then return "failure", "malformed-request", "Invalid username or password." end end self.username = authentication local correct, state = false, false; if self.profile.plain then local correct_password; 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, authorization); end if state == false then return "failure", "account-disabled"; elseif state == nil or not correct then return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent."; end return "success"; end local function init(registerMechanism) registerMechanism("PLAIN", {"plain", "plain_test"}, plain); end return { init = init; } prosody-13.0.1/util/sasl/PaxHeaders/scram.lua0000644000000000000000000000011714773555365016127 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.887711715 prosody-13.0.1/util/sasl/scram.lua0000644000175000017500000002412114773555365020326 0ustar00prosodyprosody00000000000000-- sasl.lua v0.4 -- Copyright (C) 2008-2010 Tobias Markmann -- -- All rights reserved. -- -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -- -- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -- -- 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 type = type 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 --========================= --SASL SCRAM-SHA-1 according to RFC 5802 --[[ Supported Authentication Backends scram_{MECH}: -- MECH being a standard hash name (like those at IANA's hash registry) with '-' replaced with '_' function(username, realm) return stored_key, server_key, iteration_count, salt, state; end Supported Channel Binding Backends 'tls-unique' according to RFC 5929 ]] local default_i = 10000 local function validate_username(username, _nodeprep) -- check for forbidden char sequences for eq in username:gmatch("=(.?.?)") do if eq ~= "2C" and eq ~= "3D" then return false end end -- replace =2C with , and =3D with = username = username:gsub("=2C", ","); username = username:gsub("=3D", "="); -- apply SASLprep username = saslprep(username); if username and _nodeprep ~= false then username = (_nodeprep or nodeprep)(username); end return username and #username>0 and username; end local function hashprep(hashname) return hashname:lower():gsub("-", "_"); end local function get_scram_hasher(H, HMAC, Hi) return function (password, salt, iteration_count) if type(password) ~= "string" or type(salt) ~= "string" or type(iteration_count) ~= "number" then return false, "inappropriate argument types" end if iteration_count < 4096 then log("warn", "Iteration count < 4096 which is the suggested minimum according to RFC 5802.") end password = saslprep(password); if not password then return false, "password fails SASLprep"; end local salted_password = Hi(password, salt, iteration_count); local stored_key = H(HMAC(salted_password, "Client Key")) local server_key = HMAC(salted_password, "Server Key"); return true, stored_key, server_key end end local function scram_gen(hash_name, H_f, HMAC_f, get_auth_db, expect_cb) local profile_name = "scram_" .. hashprep(hash_name); local function scram_hash(self, message) local support_channel_binding = false; if self.profile.cb then support_channel_binding = true; end if type(message) ~= "string" or #message == 0 then return "failure", "malformed-request" end local state = self.state; if not state then -- we are processing client_first_message local client_first_message = message; -- TODO: fail if authzid is provided, since we don't support them yet 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=([^,]*),?.*)$"); if not gs2_cbind_flag then return "failure", "malformed-request"; end 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 if gs2_cbind_flag == "n" then -- "n" -> client doesn't support channel binding. if expect_cb then log("debug", "Client unexpectedly doesn't support channel binding"); -- XXX Is it sensible to abort if the client starts -PLUS but doesn't use channel binding? end support_channel_binding = false; end if support_channel_binding and gs2_cbind_flag == "p" then -- check whether we support the proposed channel binding type if not self.profile.cb[gs2_cbind_name] then return "failure", "malformed-request", "Proposed channel binding type isn't supported."; end else -- no channel binding, gs2_cbind_name = nil; end username = validate_username(username, self.profile.nodeprep); if not username then log("debug", "Username violates either SASLprep or contains forbidden character sequences.") return "failure", "malformed-request", "Invalid username."; end self.username = username; -- 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, authzid) if status == nil then return "failure", "not-authorized" elseif status == false then return "failure", "account-disabled" end password = saslprep(password); if not password then log("debug", "Password violates SASLprep."); return "failure", "not-authorized", "Invalid password." end salt = generate_uuid(); iteration_count = default_i; local succ; succ, stored_key, server_key = get_auth_db(password, salt, iteration_count); if not succ then log("error", "Generating authentication database failed. Reason: %s", stored_key); return "failure", "temporary-auth-failure"; 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, authzid); if status == nil then return "failure", "not-authorized" elseif status == false then return "failure", "account-disabled" end end local nonce = clientnonce .. generate_uuid(); local server_first_message = ("r=%s,s=%s,i=%d"):format(nonce, base64.encode(salt), iteration_count); self.state = { gs2_header = gs2_header; gs2_cbind_name = gs2_cbind_name; username = self.username; -- Reference property instead of local, in case it was modified by the profile nonce = nonce; server_key = server_key; stored_key = stored_key; client_first_message_bare = client_first_message_bare; server_first_message = server_first_message; } return "challenge", server_first_message else -- we are processing client_final_message local client_final_message = message; local client_final_message_without_proof, channelbinding, nonce, proof = s_match(client_final_message, "(c=([^,]*),r=([^,]*),?.-),p=(.*)$"); if not proof or not nonce or not channelbinding then return "failure", "malformed-request", "Missing an attribute(p, r or c) in SASL message."; end local client_gs2_header = base64.decode(channelbinding) local our_client_gs2_header = state["gs2_header"] if state.gs2_cbind_name then -- we support channelbinding, so check if the value is valid our_client_gs2_header = our_client_gs2_header .. self.profile.cb[state.gs2_cbind_name](self); end if client_gs2_header ~= our_client_gs2_header then return "failure", "malformed-request", "Invalid channel binding value."; end if nonce ~= state.nonce then return "failure", "malformed-request", "Wrong nonce in client-final-message."; end local ServerKey = state.server_key; local StoredKey = state.stored_key; local AuthMessage = state.client_first_message_bare .. "," .. state.server_first_message .. "," .. client_final_message_without_proof local ClientSignature = HMAC_f(StoredKey, AuthMessage) local ClientKey = binaryXOR(ClientSignature, base64.decode(proof)) local ServerSignature = HMAC_f(ServerKey, AuthMessage) if StoredKey == H_f(ClientKey) then local server_final_message = "v="..base64.encode(ServerSignature); return "success", server_final_message; else return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."; end end end return scram_hash; end local auth_db_getters = {} local function init(registerMechanism) local function registerSCRAMMechanism(hash_name, hash, hmac_hash, pbkdf2) local get_auth_db = get_scram_hasher(hash, hmac_hash, pbkdf2); auth_db_getters[hash_name] = get_auth_db; registerMechanism("SCRAM-"..hash_name, {"plain", "scram_"..(hashprep(hash_name))}, scram_gen(hash_name:lower(), hash, hmac_hash, get_auth_db)); -- 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", "tls-exporter"}); end registerSCRAMMechanism("SHA-1", hashes.sha1, hashes.hmac_sha1, hashes.pbkdf2_hmac_sha1); registerSCRAMMechanism("SHA-256", hashes.sha256, hashes.hmac_sha256, hashes.pbkdf2_hmac_sha256); end return { get_hash = get_scram_hasher; hashers = auth_db_getters; getAuthenticationDatabaseSHA1 = get_scram_hasher(hashes.sha1, hashes.hmac_sha1, hashes.pbkdf2_hmac_sha1); -- COMPAT init = init; } prosody-13.0.1/util/PaxHeaders/sasl.lua0000644000000000000000000000011714773555365015022 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.883711699 prosody-13.0.1/util/sasl.lua0000644000175000017500000001224714773555365017227 0ustar00prosodyprosody00000000000000-- sasl.lua v0.4 -- Copyright (C) 2008-2010 Tobias Markmann -- -- All rights reserved. -- -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -- -- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -- -- 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 pairs, ipairs = pairs, ipairs; local t_insert = table.insert; local type = type local setmetatable = setmetatable; local assert = assert; local require = require; local _ENV = nil; -- luacheck: std none --[[ Authentication Backend Prototypes: state = false : disabled state = true : enabled state = nil : non-existent Channel Binding: To enable support of channel binding in some mechanisms you need to provide appropriate callbacks in a table at profile.cb. Example: profile.cb["tls-unique"] = function(self) return self.user end ]] local method = {}; method.__index = method; local registered_mechanisms = {}; local backend_mechanism = {}; local mechanism_channelbindings = {}; -- 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."); assert(type(f) == "function", "Parameter f MUST be a function."); if cb_backends then assert(type(cb_backends) == "table"); end registered_mechanisms[name] = f if cb_backends then mechanism_channelbindings[name] = {}; for _, cb_name in ipairs(cb_backends) do mechanism_channelbindings[name][cb_name] = true; end end for _, backend_name in ipairs(backends) do if backend_mechanism[backend_name] == nil then backend_mechanism[backend_name] = {}; end t_insert(backend_mechanism[backend_name], name); end end -- create a new SASL object which can be used to authenticate clients local function new(realm, profile, userdata) local mechanisms = profile.mechanisms; if not mechanisms then mechanisms = {}; for backend in pairs(profile) do if backend_mechanism[backend] then for _, mechanism in ipairs(backend_mechanism[backend]) do mechanisms[mechanism] = true; end end end profile.mechanisms = mechanisms; end return setmetatable({ profile = profile, realm = realm, mechs = mechanisms, userdata = userdata }, method); end -- add a channel binding handler function method:add_cb_handler(name, f) if type(self.profile.cb) ~= "table" then self.profile.cb = {}; end self.profile.cb[name] = f; return self; end -- get a fresh clone with the same realm and profile function method:clean_clone() return new(self.realm, self.profile, self.userdata) end -- get a list of possible SASL mechanisms to use function method:mechanisms() local current_mechs = {}; for mech, _ in pairs(self.mechs) do if mechanism_channelbindings[mech] then if self.profile.cb then local ok = false; for cb_name, _ in pairs(self.profile.cb) do if mechanism_channelbindings[mech][cb_name] then ok = true; end end if ok == true then current_mechs[mech] = true; end end else current_mechs[mech] = true; end end return current_mechs; end -- select a mechanism to use function method:select(mechanism) if not self.selected and self.mechs[mechanism] then self.selected = mechanism; return true; end end -- feed new messages to process into the library function method:process(message) --if message == "" or message == nil then return "failure", "malformed-request" end return registered_mechanisms[self.selected](self, message); end -- load the mechanisms 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; new = new; }; prosody-13.0.1/util/PaxHeaders/serialization.lua0000644000000000000000000000011714773555365016735 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.887711715 prosody-13.0.1/util/serialization.lua0000644000175000017500000001554314773555365021144 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2018 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local getmetatable = getmetatable; local next, type = next, type; local s_format = string.format; local s_gsub = string.gsub; local s_rep = string.rep; local s_char = string.char; local s_match = string.match; local t_concat = table.concat; local to_hex = require "prosody.util.hex".to; local pcall = pcall; 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; local function rawpairs(t) return next, t, nil; end local function fatal_error(obj, why) error("Can't serialize "..type(obj) .. (why and ": ".. why or "")); end local function nonfatal_fallback(x, why) return s_format("{__type=%q,__error=%q}", type(x), why or "fail"); end local string_escapes = { ['\a'] = [[\a]]; ['\b'] = [[\b]]; ['\f'] = [[\f]]; ['\n'] = [[\n]]; ['\r'] = [[\r]]; ['\t'] = [[\t]]; ['\v'] = [[\v]]; ['\\'] = [[\\]]; ['\"'] = [[\"]]; ['\''] = [[\']]; } for i = 0, 255 do local c = s_char(i); if not string_escapes[c] then string_escapes[c] = s_format("\\%03d", i); end end local default_keywords = { ["do"] = true; ["and"] = true; ["else"] = true; ["break"] = true; ["if"] = true; ["end"] = true; ["goto"] = true; ["false"] = true; ["in"] = true; ["for"] = true; ["then"] = true; ["local"] = true; ["or"] = true; ["nil"] = true; ["true"] = true; ["until"] = true; ["elseif"] = true; ["function"] = true; ["not"] = true; ["repeat"] = true; ["return"] = true; ["while"] = true; }; local function new(opt) if type(opt) ~= "table" then opt = { preset = opt }; end local types = { table = true; string = true; number = true; boolean = true; ["nil"] = true; }; -- presets if opt.preset == "debug" then opt.preset = "oneline"; opt.freeze = true; opt.fatal = false; opt.fallback = nonfatal_fallback; opt.unquoted = true; end if opt.preset == "oneline" then opt.indentwith = opt.indentwith or ""; opt.itemstart = opt.itemstart or " "; opt.itemlast = opt.itemlast or ""; opt.tend = opt.tend or " }"; elseif opt.preset == "compact" then opt.indentwith = opt.indentwith or ""; opt.itemstart = opt.itemstart or ""; 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; local function ser(v) return (types[type(v)] or fallback)(v); end local keywords = opt.keywords or default_keywords; -- indented local indentwith = opt.indentwith or "\t"; local itemstart = opt.itemstart or "\n"; local itemsep = opt.itemsep or ";"; local itemlast = opt.itemlast or ";\n"; local tstart = opt.tstart or "{"; local tend = opt.tend or "}"; local kstart = opt.kstart or "["; local kend = opt.kend or "]"; local equals = opt.equals or " = "; local unquoted = opt.unquoted == true and "^[%a_][%w_]*$" or opt.unquoted; local hex = opt.hex; local freeze = opt.freeze; local maxdepth = opt.maxdepth or 127; local multirefs = opt.multiref; local table_pairs = opt.table_iterator or rawpairs; -- serialize one table, recursively -- t - table being serialized -- o - array where tokens are added, concatenate to get final result -- - also used to detect cycles -- l - position in o of where to insert next token -- d - depth, used for indentation local function serialize_table(t, o, l, d) if o[t] then o[l], l = fallback(t, "table has multiple references"), l + 1; return l; elseif d > maxdepth then o[l], l = fallback(t, "max table depth reached"), l + 1; return l; end -- Keep track of table loops local ot = t; -- reference pre-freeze o[t] = true; o[ot] = true; if freeze == true then -- opportunity to do pre-serialization local mt = getmetatable(t); if type(mt) == "table" then local tag = mt.__name; local fr = mt.__freeze; if type(fr) == "function" then t = fr(t); if type(t) == "string" then o[l], l = t, l + 1; return l; end if type(tag) == "string" then o[l], l = tag, l + 1; end end end end o[l], l = tstart, l + 1; local indent = s_rep(indentwith, d); local numkey = 1; local ktyp, vtyp; local had_items = false; for k,v in table_pairs(t) do had_items = true; o[l], l = itemstart, l + 1; o[l], l = indent, l + 1; ktyp, vtyp = type(k), type(v); if k == numkey then -- next index in array part -- assuming that these are found in order numkey = numkey + 1; elseif unquoted and ktyp == "string" and not keywords[k] and s_match(k, unquoted) then -- unquoted keys o[l], l = k, l + 1; o[l], l = equals, l + 1; else -- quoted keys o[l], l = kstart, l + 1; if ktyp == "table" then l = serialize_table(k, o, l, d+1); else o[l], l = ser(k), l + 1; end -- = o[l], o[l+1], l = kend, equals, l + 2; end -- the value if vtyp == "table" then l = serialize_table(v, o, l, d+1); else o[l], l = ser(v), l + 1; end o[l], l = itemsep, l + 1; end if had_items then o[l - 1] = itemlast; o[l], l = s_rep(indentwith, d-1), l + 1; end o[l], l = tend, l +1; if multirefs then o[t] = nil; o[ot] = nil; end return l; end function types.table(t) local o = {}; serialize_table(t, o, 1, 1); return t_concat(o); end local function serialize_string(s) return '"' .. s_gsub(s, "[%z\1-\31\"\'\\\127-\255]", string_escapes) .. '"'; end if type(hex) == "string" then function types.string(s) local esc = serialize_string(s); if #esc > (#s*2+2+#hex) then return hex .. '"' .. to_hex(s) .. '"'; end return esc; end else types.string = serialize_string; end function types.number(t) if m_type(t) == "integer" then return s_format("%d", t); elseif t == pos_inf then return "(1/0)"; elseif t == neg_inf then return "(-1/0)"; elseif t ~= t then return "(0/0)"; end return s_format("%.18g", t); end -- Are these faster than tostring? types["nil"] = function() return "nil"; end function types.boolean(t) return t and "true" or "false"; end return ser; end local function deserialize(str) if type(str) ~= "string" then return nil; end str = "return "..str; local f, err = envload(str, "=serialized data", {}); if not f then return nil, err; end local success, ret = pcall(f); if not success then return nil, ret; end return ret; end local default = new(); return { new = new; serialize = function (x, opt) if opt == nil then return default(x); else return new(opt)(x); end end; deserialize = deserialize; }; prosody-13.0.1/util/PaxHeaders/session.lua0000644000000000000000000000011614773555365015542 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.89171173 prosody-13.0.1/util/session.lua0000644000175000017500000000301114773555365017735 0ustar00prosodyprosody00000000000000local 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 local function set_id(session) local id = session.base_type .. tostring(session):match("%x+$"):lower(); session.id = id; return session; end local function set_logger(session) local log = logger.init(session.id); session.log = log; return session; end local function set_conn(session, conn) session.conn = conn; session.ip = conn:ip(); return session; end local function set_send(session) local conn = session.conn; if not conn then function session.send(data) session.log("debug", "Discarding data sent to unconnected session: %s", data); return false; end return session; end local filter = initialize_filters(session); local w = conn.write; session.send = function (t) if t.name then t = filter("stanzas/out", t); end if t then t = filter("bytes/out", tostring(t)); if t then local ret, err = w(conn, t); if not ret then session.log("debug", "Error writing to connection: %s", err); return false, err; end end end return true; end 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; } prosody-13.0.1/util/PaxHeaders/set.lua0000644000000000000000000000011614773555365014652 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.89171173 prosody-13.0.1/util/set.lua0000644000175000017500000000720114773555365017052 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local ipairs, pairs, setmetatable, next, tostring = ipairs, pairs, setmetatable, next, tostring; local getmetatable = getmetatable; local t_concat = table.concat; local _ENV = nil; -- luacheck: std none local set_mt = { __name = "set" }; function set_mt.__call(set, _, k) return next(set._items, k); end local items_mt = {}; function items_mt.__call(items, _, k) return next(items, k); end function set_mt:__freeze() local a, i = {}, 1; for item in self._items do a[i], i = item, i+1; end return a; end local function is_set(o) local mt = getmetatable(o); return mt == set_mt; end local function new(list) local items = setmetatable({}, items_mt); local set = { _items = items }; -- We access the set through an upvalue in these methods, so ignore 'self' being unused --luacheck: ignore 212/self function set:add(item) items[item] = true; end function set:contains(item) return items[item]; end function set:contains_set(other_set) for item in other_set do if not self:contains(item) then return false; end end return true; end function set:items() return next, items; end function set:remove(item) items[item] = nil; end function set:add_list(item_list) if item_list then for _, item in ipairs(item_list) do items[item] = true; end end end function set:include(otherset) for item in otherset do items[item] = true; end end function set:exclude(otherset) for item in otherset do items[item] = nil; end end function set:empty() return not next(items); end if list then set:add_list(list); end return setmetatable(set, set_mt); end local function union(set1, set2) local set = new(); local items = set._items; for item in pairs(set1._items) do items[item] = true; end for item in pairs(set2._items) do items[item] = true; end return set; end local function difference(set1, set2) local set = new(); local items = set._items; for item in pairs(set1._items) do items[item] = (not set2._items[item]) or nil; end return set; end local function intersection(set1, set2) local set = new(); local items = set._items; set1, set2 = set1._items, set2._items; for item in pairs(set1) do items[item] = (not not set2[item]) or nil; end return set; end local function xor(set1, set2) return union(set1, set2) - intersection(set1, set2); end function set_mt.__add(set1, set2) return union(set1, set2); end function set_mt.__sub(set1, set2) return difference(set1, set2); end function set_mt.__div(set, func) local new_set = new(); local items, new_items = set._items, new_set._items; for item in pairs(items) do local new_item = func(item); if new_item ~= nil then new_items[new_item] = true; end end return new_set; end function set_mt.__eq(set1, set2) if getmetatable(set1) ~= set_mt or getmetatable(set2) ~= set_mt then -- Lua 5.3+ calls this if both operands are tables, even if metatables differ return false; end set1, set2 = set1._items, set2._items; for item in pairs(set1) do if not set2[item] then return false; end end for item in pairs(set2) do if not set1[item] then return false; end end return true; end function set_mt.__tostring(set) local s, items = { }, set._items; for item in pairs(items) do s[#s+1] = tostring(item); end return "{"..t_concat(s, ", ").."}"; end return { new = new; is_set = is_set; union = union; difference = difference; intersection = intersection; xor = xor; }; prosody-13.0.1/util/PaxHeaders/smqueue.lua0000644000000000000000000000011614773555365015543 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.89171173 prosody-13.0.1/util/smqueue.lua0000644000175000017500000000244414773555365017747 0ustar00prosodyprosody00000000000000local queue = require("prosody.util.queue"); local lib = { smqueue = {} } local smqueue = lib.smqueue; function smqueue:push(v) self._head = self._head + 1; assert(self._queue:push(v)); end function smqueue:ack(h) if h < self._tail then return nil, "tail" elseif h > self._head then return nil, "head" end local acked = {}; self._tail = h; local expect = self._head - self._tail; while expect < self._queue:count() do local v = self._queue:pop(); if not v then return nil, "pop" end table.insert(acked, v); end return acked end function smqueue:count_unacked() return self._head - self._tail end function smqueue:count_acked() return self._tail end function smqueue:resumable() return self._queue:count() >= (self._head - self._tail) end function smqueue:resume() return self._queue:items() end function smqueue:consume() return self._queue:consume() end function smqueue:table() local t = {}; for i, v in self:resume() do t[i] = v; end return t end local function freeze(q) return { head = q._head; tail = q._tail } end local queue_mt = { __name = "smqueue"; __index = smqueue; __len = smqueue.count_unacked; __freeze = freeze } function lib.new(size) assert(size > 0); return setmetatable({ _head = 0; _tail = 0; _queue = queue.new(size, true) }, queue_mt) end return lib prosody-13.0.1/util/PaxHeaders/sql.lua0000644000000000000000000000011614773555365014656 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.89171173 prosody-13.0.1/util/sql.lua0000644000175000017500000002610214773555365017057 0ustar00prosodyprosody00000000000000 local setmetatable, getmetatable = setmetatable, getmetatable; local ipairs = ipairs; local tostring = tostring; local type = type; local assert, pcall, debug_traceback = assert, pcall, debug.traceback; local xpcall = require "prosody.util.xpcall".xpcall; local t_concat = table.concat; local log = require "prosody.util.logger".init("sql"); local DBI = require "DBI"; -- This loads all available drivers while globals are unlocked -- LuaDBI should be fixed to not set globals. DBI.Drivers(); local build_url = require "socket.url".build; 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, "no driver") log("debug", "Connecting to [%s] %s...", params.driver, params.database); local ok, dbh, err = pcall(DBI.Connect, params.driver, params.database, params.username, params.password, params.host, params.port ); if not ok then return ok, dbh; end if not dbh then return nil, err; end dbh:autocommit(false); -- don't commit automatically self.conn = dbh; self.prepared = {}; if params.driver == "SQLite3" and params.password then local ok, err = self:execute(("PRAGMA key='%s'"):format(dbh:quote(params.password))); if not ok then return ok, err; end end 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:prepquery(sql) if self.params.driver == "MySQL" then sql = sql:gsub("\"", "`"); end return sql; end function engine:execute(sql, ...) local success, err = self:connect(); if not success then return success, err; end local prepared = self.prepared; sql = self:prepquery(sql); local stmt = prepared[sql]; if not stmt then local err; stmt, err = self.conn:prepare(sql); if not stmt then return stmt, err; end prepared[sql] = stmt; end -- luacheck: ignore 411/success local success, err = stmt:execute(...); if not success then return success, err; end return stmt; end local result_mt = { __index = { affected = function(self) return self.__stmt:affected(); end; rowcount = function(self) return self.__stmt:rowcount(); 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_query(sql, ...) sql = self:prepquery(sql); local stmt = assert(self.conn:prepare(sql)); assert(stmt:execute(...)); local result = {}; for row in stmt:rows() do result[#result + 1] = row; end stmt:close(); local i = 0; return function() i=i+1; return result[i]; end; end function engine:execute_update(sql, ...) sql = self:prepquery(sql); local prepared = self.prepared; local stmt = prepared[sql]; if not stmt then stmt = assert(self.conn:prepare(sql)); prepared[sql] = stmt; end assert(stmt:execute(...)); return setmetatable({ __stmt = stmt }, result_mt); 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 local function handleerr(err) local trace = debug_traceback(err, 3); log("debug", "Error in SQL transaction: %s", trace); return { err = err, traceback = trace }; end function engine:_transaction(func, ...) if not self.conn then local ok, err = self:connect(); if not ok then return ok, err; end end --assert(not self.__transaction, "Recursive transactions not allowed"); log("debug", "SQL transaction begin [%s]", func); self.__transaction = true; local success, a, b, c = xpcall(func, handleerr, ...); self.__transaction = nil; if success then log("debug", "SQL transaction success [%s]", func); local ok, err = self.conn:commit(); -- LuaDBI doesn't actually return an error message here, just a boolean if not ok then return ok, err or "commit failed"; end return success, a, b, c; else log("debug", "SQL transaction failure [%s]: %s", func, a.err); if self.conn then self.conn:rollback(); end return success, a.err; end end function engine:transaction(...) local ok, ret, b, c = self:_transaction(...); if not ok then local conn = self.conn; 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"); else log("debug", "SQL connection is up, so not retrying"); end if not ok then log("error", "Error in SQL transaction: %s", ret); end end return ok, ret, b, c; end function engine:_create_index(index) local sql = "CREATE INDEX \""..index.name.."\" ON \""..index.table.."\" ("; if self.params.driver ~= "MySQL" then sql = sql:gsub("^CREATE INDEX", "%1 IF NOT EXISTS"); end for i=1,#index do sql = sql.."\""..index[i].."\""; if i ~= #index then sql = sql..", "; end end sql = sql..");" if self.params.driver == "MySQL" then sql = sql:gsub("\"([,)])", "\"(20)%1"); end 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 \""..table.name.."\" ("; do sql = sql:gsub("^CREATE TABLE", "%1 IF NOT EXISTS"); end for i,col in ipairs(table.c) do local col_type = col.type; if col_type == "MEDIUMTEXT" and self.params.driver ~= "MySQL" then col_type = "TEXT"; -- MEDIUMTEXT is MySQL-specific end if col.auto_increment == true and self.params.driver == "PostgreSQL" then col_type = "BIGSERIAL"; end 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 if self.params.driver == "MySQL" then sql = sql.." AUTO_INCREMENT"; elseif self.params.driver == "SQLite3" then sql = sql.." AUTOINCREMENT"; end end if i ~= #table.c then sql = sql..", "; end end sql = sql.. ");" if self.params.driver == "MySQL" then sql = sql:gsub(";$", (" CHARACTER SET '%s' COLLATE '%s_bin';"):format(self.charset, self.charset)); end 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 local driver = self.params.driver; if driver == "SQLite3" then 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 set_names_query = "SET NAMES '%s';" local charset = "utf8"; if driver == "MySQL" then self:transaction(function() for row in self:select[[ SELECT "CHARACTER_SET_NAME" FROM "information_schema"."CHARACTER_SETS" WHERE "CHARACTER_SET_NAME" LIKE 'utf8%' ORDER BY MAXLEN DESC LIMIT 1; ]] do charset = row and row[1] or charset; end end); set_names_query = set_names_query:gsub(";$", (" COLLATE '%s';"):format(charset.."_bin")); end self.charset = charset; log("debug", "Using encoding '%s' for database connection", charset); local ok, err = self:transaction(function() return self:execute(set_names_query:format(charset)); end); if not ok then return ok, err; end if driver == "MySQL" then local ok, actual_charset = self:transaction(function () return self:select"SHOW SESSION VARIABLES LIKE 'character_set_client'"; end); if not ok then return false, "Failed to detect connection encoding"; end local charset_ok = true; for row in actual_charset do if row[2] ~= charset then log("error", "MySQL %s is actually %q (expected %q)", row[1], row[2], charset); charset_ok = false; end end if not charset_ok then return false, "Failed to set connection encoding"; end end return true; 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) 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; }; prosody-13.0.1/util/PaxHeaders/sqlite3.lua0000644000000000000000000000011614773555365015443 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.89171173 prosody-13.0.1/util/sqlite3.lua0000644000175000017500000003052114773555365017644 0ustar00prosodyprosody00000000000000 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 = {}; if params.password then local ok, err = self:execute(("PRAGMA key='%s'"):format((params.password:gsub("'", "''")))); if not ok then return ok, err; end end 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; }; prosody-13.0.1/util/PaxHeaders/sslconfig.lua0000644000000000000000000000011714773555365016047 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.895711746 prosody-13.0.1/util/sslconfig.lua0000644000175000017500000001145414773555365020253 0ustar00prosodyprosody00000000000000-- util to easily merge multiple sets of LuaSec context options 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 local handlers = { }; local finalisers = { }; local id = function (v) return v end -- All "handlers" behave like extended rawset(table, key, value) with extra -- processing usually merging the new value with the old in some reasonable -- way -- If a field does not have a defined handler then a new value simply -- replaces the old. -- Convert either a list or a set into a special type of set where each -- item is either positive or negative in order for a later set of options -- to be able to remove options from this set by filtering out the negative ones function handlers.options(config, field, new) local options = config[field] or { }; if type(new) ~= "table" then new = { new } end for key, value in pairs(new) do if value == true or value == false then options[key] = value; else -- list item options[value] = true; end end rawset(config, field, options) end handlers.verifyext = handlers.options; -- finalisers take something produced by handlers and return what luasec -- expects it to be -- Produce a list of "positive" options from the set function finalisers.options(options) local output = {}; for opt, enable in pairs(options) do if enable then output[#output+1] = opt; end end return output; end finalisers.verifyext = finalisers.options; -- We allow ciphers to be a list function finalisers.ciphers(cipherlist) if type(cipherlist) == "table" then return t_concat(cipherlist, ":"); end return cipherlist; end -- Curve list too 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 local protocols = { "sslv2", "sslv3", "tlsv1", "tlsv1_1", "tlsv1_2", "tlsv1_3" }; for i = 1, #protocols do protocols[protocols[i] .. "+"] = i - 1; end -- this interacts with ssl.options as well to add no_x local function protocol(config) local min_protocol = protocols[config.protocol]; if min_protocol then config.protocol = "sslv23"; for i = 1, min_protocol do t_insert(config.options, "no_"..protocols[i]); end end 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 -- 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 -- 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; }; -- 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; }; prosody-13.0.1/util/PaxHeaders/stanza.lua0000644000000000000000000000011714773555365015360 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.895711746 prosody-13.0.1/util/stanza.lua0000644000175000017500000004132114773555365017560 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local error = error; local t_insert = table.insert; local t_remove = table.remove; local t_concat = table.concat; local s_match = string.match; local tostring = tostring; local setmetatable = setmetatable; local getmetatable = getmetatable; local pairs = pairs; local ipairs = ipairs; 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 "prosody.util.encodings".utf8.valid; 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 local stanza_mt = { __name = "stanza" }; stanza_mt.__index = stanza_mt; -- Basic check for valid XML character data. -- Disallow control characters. -- Tab U+09 and newline U+0A are allowed. -- For attributes, allow the \1 separator between namespace and name. local function valid_xml_cdata(str, attr) return not s_find(str, attr and "[^\1\9\10\13\20-\255]" or "[^\9\10\13\20-\255]"); end local function check_name(name, name_type) if type(name) ~= "string" then error("invalid "..name_type.." name: expected string, got "..type(name)); elseif #name == 0 then error("invalid "..name_type.." name: empty string"); elseif s_find(name, "[<>& '\"]") then error("invalid "..name_type.." name: contains invalid characters"); elseif not valid_xml_cdata(name, name_type == "attribute") then error("invalid "..name_type.." name: contains control characters"); elseif not valid_utf8(name) then error("invalid "..name_type.." name: contains invalid utf8"); end end local function check_text(text, text_type) if type(text) ~= "string" then error("invalid "..text_type.." value: expected string, got "..type(text)); elseif not valid_xml_cdata(text, false) then error("invalid "..text_type.." value: contains control characters"); elseif not valid_utf8(text) then error("invalid "..text_type.." value: contains invalid utf8"); end end local function check_attr(attr) if attr ~= nil then if type(attr) ~= "table" then error("invalid attributes: expected table, got "..type(attr)); end for k, v in pairs(attr) do check_name(k, "attribute"); check_text(v, "attribute"); end end end local function new_stanza(name, attr, namespaces) check_name(name, "tag"); check_attr(attr); local stanza = { name = name, attr = attr or {}, namespaces = namespaces, tags = {} }; return setmetatable(stanza, stanza_mt); end local function is_stanza(s) return getmetatable(s) == stanza_mt; end function stanza_mt:query(xmlns) return self:tag("query", { xmlns = xmlns }); end function stanza_mt:body(text, attr) return self:text_tag("body", text, attr); end function stanza_mt:text_tag(name, text, attr, namespaces) return self:tag(name, attr, namespaces):text(text):up(); end function stanza_mt:tag(name, attr, namespaces) local s = new_stanza(name, attr, namespaces); local last_add = self.last_add; if not last_add then last_add = {}; self.last_add = last_add; end (last_add[#last_add] or self):add_direct_child(s); t_insert(last_add, s); return self; end function stanza_mt:text(text) if text ~= nil and text ~= "" then local last_add = self.last_add; (last_add and last_add[#last_add] or self):add_direct_child(text); end return self; end function stanza_mt:up() local last_add = self.last_add; if last_add then t_remove(last_add); end return self; end function stanza_mt:at_top() return self.last_add == nil or #self.last_add == 0 end function stanza_mt:reset() self.last_add = nil; return self; end function stanza_mt:add_direct_child(child) if is_stanza(child) then t_insert(self.tags, child); t_insert(self, child); else check_text(child, "text"); t_insert(self, child); end end function stanza_mt:add_child(child) local last_add = self.last_add; (last_add and last_add[#last_add] or self):add_direct_child(child); return self; end function stanza_mt:remove_children(name, xmlns) xmlns = xmlns or self.attr.xmlns; return self:maptags(function (tag) if (not name or tag.name == name) and tag.attr.xmlns == xmlns then return nil; end return tag; end); end function stanza_mt:get_child(name, xmlns) for _, child in ipairs(self.tags) do if (not name or child.name == name) and ((not xmlns and self.attr.xmlns == child.attr.xmlns) or child.attr.xmlns == xmlns) then return child; end end return nil; end function stanza_mt:get_child_text(name, xmlns) local tag = self:get_child(name, xmlns); if tag then return tag:get_text(); end 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 end return nil; end function stanza_mt:child_with_ns(ns) for _, child in ipairs(self.tags) do if child.attr.xmlns == ns then return child; end end return nil; end function stanza_mt:get_child_with_attr(name, xmlns, attr_name, attr_value, normalize) for tag in self:childtags(name, xmlns) do if (normalize and normalize(tag.attr[attr_name]) or tag.attr[attr_name]) == attr_value then return tag; end end return nil; end function stanza_mt:children() local i = 0; return function (a) i = i + 1 return a[i]; end, self, i; end function stanza_mt:childtags(name, xmlns) local tags = self.tags; local start_i, max_i = 1, #tags; return function () for i = start_i, max_i do local v = tags[i]; if (not name or v.name == name) and ((not xmlns and self.attr.xmlns == v.attr.xmlns) or v.attr.xmlns == xmlns) then start_i = i+1; return v; end end end; end function stanza_mt:maptags(callback) local tags, curr_tag = self.tags, 1; local n_children, n_tags = #self, #tags; local max_iterations = n_children + 1; local i = 1; while curr_tag <= n_tags and n_tags > 0 do if self[i] == tags[curr_tag] then local ret = callback(self[i]); if ret == nil then t_remove(self, i); t_remove(tags, curr_tag); n_children = n_children - 1; n_tags = n_tags - 1; i = i - 1; curr_tag = curr_tag - 1; else self[i] = ret; tags[curr_tag] = ret; end curr_tag = curr_tag + 1; end i = i + 1; if i > max_iterations then -- COMPAT: Hopefully temporary guard against #981 while we -- figure out the root cause error("Invalid stanza state! Please report this error."); end end return self; end function stanza_mt:find(path) local pos = 1; local len = #path + 1; repeat 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); end name, text, pos = s_match(path, "^([^@/#]*)([/#]?)()", pos); name = name ~= "" and name or nil; if pos == len then if text == "#" then return self:get_child_text(name, xmlns); end return self:get_child(name, xmlns); end self = self:get_child(name, xmlns); until not self end local function _clone(stanza, only_top) 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 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 t_move(stanza, 1, #stanza, 1, new); t_move(stanza.tags, 1, #stanza.tags, 1, tags); new:maptags(_clone); end return new; end local function clone(stanza, only_top) if not is_stanza(stanza) then error("bad argument to clone: expected stanza, got "..type(stanza)); end return _clone(stanza, only_top); end local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; local function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end local function _dostring(t, buf, self, _xml_escape, parentns) local nsid = 0; local name = t.name t_insert(buf, "<"..name); for k, v in pairs(t.attr) do if s_find(k, "\1", 1, true) then local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$"); nsid = nsid + 1; t_insert(buf, " xmlns:ns"..nsid.."='".._xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='".._xml_escape(v).."'"); elseif not(k == "xmlns" and v == parentns) then t_insert(buf, " "..k.."='".._xml_escape(v).."'"); end end local len = #t; if len == 0 then t_insert(buf, "/>"); else t_insert(buf, ">"); for n=1,len do local child = t[n]; if child.name then self(child, buf, self, _xml_escape, t.attr.xmlns); else t_insert(buf, _xml_escape(child)); end end t_insert(buf, ""); end end function stanza_mt.__tostring(t) local buf = {}; _dostring(t, buf, _dostring, xml_escape, nil); return t_concat(buf); end function stanza_mt.top_tag(t) local top_tag_clone = clone(t, true); return tostring(top_tag_clone):sub(1,-3)..">"; end function stanza_mt.get_text(t) if #t.tags == 0 then return t_concat(t); end return nil; end function stanza_mt.get_error(stanza) local error_type, condition, text, extra_tag; local error_tag = stanza:get_child("error"); if not error_tag then return nil, nil, nil, nil; end error_type = error_tag.attr.type; for _, child in ipairs(error_tag.tags) do if child.attr.xmlns == xmlns_stanzas then if not text and child.name == "text" then text = child:get_text(); elseif not condition then condition = child.name; end else extra_tag = child; end if condition and text and extra_tag then break; end end 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 if type(child) == "table" then t_insert(s, preserialize(child)); else t_insert(s, child); end end return s; end stanza_mt.__freeze = preserialize; local function deserialize(serialized) -- Set metatable if serialized then local attr = serialized.attr; local attrx = {}; for att, val in pairs(attr) do if type(att) == "string" then if s_find(att, "|", 1, true) and not s_find(att, "\1", 1, true) then local ns,na = s_match(att, "^([^|]+)|(.+)$"); attrx[ns.."\1"..na] = val; else attrx[att] = val; end end end local stanza = new_stanza(serialized.name, attrx); for _, child in ipairs(serialized) do if type(child) == "table" then stanza:add_direct_child(deserialize(child)); elseif type(child) == "string" then stanza:add_direct_child(child); end end return stanza; end end local function message(attr, body) if not body then return new_stanza("message", attr); else return new_stanza("message", attr):text_tag("body", body); end end local function iq(attr) if not attr then error("iq stanzas require id and type attributes"); end if not attr.id then error("iq stanzas require an id attribute"); end if not attr.type then error("iq stanzas require a type attribute"); end return new_stanza("iq", attr); end local function reply(orig) if not is_stanza(orig) then error("bad argument to reply: expected stanza, got "..type(orig)); end return new_stanza(orig.name, { to = orig.attr.from, from = orig.attr.to, id = orig.attr.id, type = ((orig.name == "iq" and "result") or orig.attr.type) }); end 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)); elseif orig.attr.type == "error" then error("bad argument to error_reply: got stanza of type error which must not be replied to"); end local t = reply(orig); t.attr.type = "error"; 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) return new_stanza("presence", attr); end local pretty; if do_pretty_printing then local getstyle, getstring = termcolours.getstyle, termcolours.getstring; local blue1 = getstyle("1b3967"); local blue2 = getstyle("13b5ea"); local green1 = getstyle("439639"); local green2 = getstyle("a0ce67"); local orange1 = getstyle("d9541e"); local orange2 = getstyle("e96d1f"); local attr_replace = ( getstring(green2, "%1") .. -- attr name getstring(green1, "%2") .. -- equal getstring(orange1, "%3") .. -- quote getstring(orange2, "%4") .. -- attr value getstring(orange1, "%5") -- quote ); local text_replace = ( getstring(green1, "%1") .. -- & getstring(green2, "%2") .. -- amp getstring(green1, "%3") -- ; ); function pretty(s) -- Tag soup color -- Outer gsub call takes each , applies colour to the brackets, the -- tag name, then applies one inner gsub call to colour the attributes and -- another for any text content. return (s:gsub("(<[?/]?)([^ >/?]*)(.-)([?/]?>)([^<]*)", function(opening_bracket, tag_name, attrs, closing_bracket, content) return getstring(blue1, opening_bracket)..getstring(blue2, tag_name).. attrs:gsub("([^=]+)(=)([\"'])(.-)([\"'])", attr_replace) .. getstring(blue1, closing_bracket) .. content:gsub("(&#?)(%w+)(;)", text_replace); end, 100)); end function stanza_mt.pretty_print(t) return pretty(tostring(t)); end function stanza_mt.pretty_top_tag(t) return pretty(t:top_tag()); end else -- Sorry, fresh out of colours for you guys ;) stanza_mt.pretty_print = stanza_mt.__tostring; stanza_mt.pretty_top_tag = stanza_mt.top_tag; end function stanza_mt.indent(t, level, indent) if #t == 0 or (#t == 1 and type(t[1]) == "string") then -- Empty nodes wouldn't have any indentation -- Text-only nodes are preserved as to not alter the text content -- Optimization: Skip clone of these since we don't alter them return t; end indent = indent or "\t"; level = level or 1; local tag = clone(t, true); for child in t:children() do if type(child) == "string" then -- Already indented text would look weird but let's ignore that for now. if child:find("%S") then tag:text("\n" .. indent:rep(level)); tag:text(child); end elseif is_stanza(child) then tag:text("\n" .. indent:rep(level)); tag:add_direct_child(child:indent(level+1, indent)); end end -- before the closing tag tag:text("\n" .. indent:rep((level-1))); return tag; end return { stanza_mt = stanza_mt; stanza = new_stanza; is_stanza = is_stanza; preserialize = preserialize; deserialize = deserialize; clone = clone; message = message; iq = iq; reply = reply; error_reply = error_reply; presence = presence; xml_escape = xml_escape; pretty_print = pretty; }; prosody-13.0.1/util/PaxHeaders/startup.lua0000644000000000000000000000011714773555365015562 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.899711762 prosody-13.0.1/util/startup.lua0000644000175000017500000007165414773555365017776 0ustar00prosodyprosody00000000000000-- Ignore the CFG_* variables -- luacheck: ignore 113/CFG_CONFIGDIR 113/CFG_SOURCEDIR 113/CFG_DATADIR 113/CFG_PLUGINDIR local startup = {}; local prosody = { events = require "prosody.util.events".new() }; local logger = require "prosody.util.logger"; local log = logger.init("startup"); local parse_args = require "prosody.util.argparse".parse; local config = require "prosody.core.configmanager"; local config_warnings; local dependencies = require "prosody.util.dependencies"; local original_logging_config; local default_gc_params = { mode = "incremental"; -- Incremental mode defaults threshold = 105, speed = 500; -- Generational mode defaults minor_threshold = 20, major_threshold = 50; }; local arg_settigs = { prosody = { short_params = { D = "daemonize"; F = "no-daemonize", h = "help", ["?"] = "help" }; value_params = { config = true }; }; prosodyctl = { short_params = { v = "verbose", h = "help", ["?"] = "help" }; value_params = { config = true }; }; } function startup.parse_args(profile) local opts, err, where = parse_args(arg, arg_settigs[profile or prosody.process_type] or profile); if not opts then if err == "param-not-found" then print("Unknown command-line option: "..tostring(where)); if prosody.process_type == "prosody" then print("Perhaps you meant to use prosodyctl instead?"); end elseif err == "missing-value" then print("Expected a value to follow command-line option: "..where); end os.exit(1); end if prosody.process_type == "prosody" then if #arg > 0 then print("Unrecognized option: "..arg[1]); print("(Did you mean 'prosodyctl "..arg[1].."'?)"); print(""); end if opts.help or #arg > 0 then print("prosody [ -D | -F ] [ --config /path/to/prosody.cfg.lua ]"); print(" -D, --daemonize Run in the background") print(" -F, --no-daemonize Run in the foreground") print(" --config FILE Specify config file") os.exit(0); end end prosody.opts = opts; end function startup.read_config() local filenames = {}; local filename; if prosody.opts.config then table.insert(filenames, prosody.opts.config); if CFG_CONFIGDIR then table.insert(filenames, CFG_CONFIGDIR.."/"..prosody.opts.config); end elseif os.getenv("PROSODY_CONFIG") then -- Passed by prosodyctl table.insert(filenames, os.getenv("PROSODY_CONFIG")); else table.insert(filenames, (CFG_CONFIGDIR or ".").."/prosody.cfg.lua"); end for _,_filename in ipairs(filenames) do filename = _filename; local file = io.open(filename); if file then file:close(); prosody.config_file = filename; prosody.paths.config = filename:match("^(.*)[\\/][^\\/]*$"); CFG_CONFIGDIR = prosody.paths.config; -- luacheck: ignore 111 break; end end prosody.config_file = filename local credentials_directory = os.getenv("CREDENTIALS_DIRECTORY"); if credentials_directory then config.set_credentials_directory(credentials_directory); elseif prosody.process_type == "prosody" then config.set_credential_fallback_mode("error"); else config.set_credential_fallback_mode("warn"); end local ok, level, err = config.load(filename); if not ok then print("\n"); print("**************************"); if level == "parser" then print("A problem occurred while reading the config file "..filename); print(""); local err_line, err_message = tostring(err):match("%[string .-%]:(%d*): (.*)"); if err:match("chunk has too many syntax levels$") then print("An Include statement in a config file is including an already-included"); print("file and causing an infinite loop. An Include statement in a config file is..."); else print("Error"..(err_line and (" on line "..err_line) or "")..": "..(err_message or tostring(err))); end print(""); elseif level == "file" then print("Prosody was unable to find the configuration file."); print("We looked for: "..filename); print("A sample config file is included in the Prosody download called prosody.cfg.lua.dist"); print("Copy or rename it to prosody.cfg.lua and edit as necessary."); end print("More help on configuring Prosody can be found at https://prosody.im/doc/configure"); print("Good luck!"); print("**************************"); print(""); os.exit(1); elseif err and #err > 0 then config_warnings = err; end prosody.config_loaded = true; end function startup.check_dependencies() if not dependencies.check_dependencies() then os.exit(1); end end -- luacheck: globals socket server function startup.load_libraries() -- Load socket framework -- luacheck: ignore 111/server 111/socket require "prosody.util.import" socket = require "socket"; server = require "prosody.net.server" end function startup.init_logging() -- Initialize logging local loggingmanager = require "prosody.core.loggingmanager" loggingmanager.reload_logging(); prosody.events.add_handler("config-reloaded", function () prosody.events.fire_event("reopen-log-files"); end); prosody.events.add_handler("reopen-log-files", function () loggingmanager.reload_logging(); prosody.events.fire_event("logging-reloaded"); end); end function startup.log_startup_warnings() dependencies.log_warnings(); if config_warnings then for _, warning in ipairs(config_warnings) do log("warn", "Configuration warning: %s", warning); end end end function startup.sanity_check() for host, host_config in pairs(config.getconfig()) do if host ~= "*" and host_config.enabled ~= false and not host_config.component_module then return; end end log("error", "No enabled VirtualHost entries found in the config file."); log("error", "At least one active host is required for Prosody to function. Exiting..."); os.exit(1); end function startup.sandbox_require() -- Replace require() with one that doesn't pollute _G, required -- for neat sandboxing of modules -- luacheck: ignore 113/getfenv 111/require local _realG = _G; local _real_require = require; local getfenv = getfenv or function (f) -- FIXME: This is a hack to replace getfenv() in Lua 5.2 local name, env = debug.getupvalue(debug.getinfo(f or 1).func, 1); if name == "_ENV" then return env; end end function require(...) -- luacheck: ignore 121 local curr_env = getfenv(2); local curr_env_mt = getmetatable(curr_env); local _realG_mt = getmetatable(_realG); if curr_env_mt and curr_env_mt.__index and not curr_env_mt.__newindex and _realG_mt then local old_newindex, old_index; old_newindex, _realG_mt.__newindex = _realG_mt.__newindex, curr_env; old_index, _realG_mt.__index = _realG_mt.__index, function (_G, k) -- luacheck: ignore 212/_G return rawget(curr_env, k); end; local ret = _real_require(...); _realG_mt.__newindex = old_newindex; _realG_mt.__index = old_index; return ret; end return _real_require(...); end end function startup.set_function_metatable() local mt = {}; function mt.__index(f, upvalue) local i, name, value = 0; repeat i = i + 1; name, value = debug.getupvalue(f, i); until name == upvalue or name == nil; return value; end function mt.__newindex(f, upvalue, value) local i, name = 0; repeat i = i + 1; name = debug.getupvalue(f, i); until name == upvalue or name == nil; if name then debug.setupvalue(f, i, value); end end function mt.__tostring(f) local info = debug.getinfo(f, "Su"); local n_params = info.nparams or 0; for i = 1, n_params do info[i] = debug.getlocal(f, i); end if info.isvararg then info[n_params+1] = "..."; end return ("function @%s:%d(%s)"):format(info.short_src:match("[^\\/]*$"), info.linedefined, table.concat(info, ", ")); end debug.setmetatable(function() end, mt); end function startup.detect_platform() prosody.platform = "unknown"; if os.getenv("WINDIR") then prosody.platform = "windows"; elseif package.config:sub(1,1) == "/" then prosody.platform = "posix"; end end function startup.detect_installed() prosody.installed = nil; if CFG_SOURCEDIR and (prosody.platform == "windows" or CFG_SOURCEDIR:match("^/")) then prosody.installed = true; end end function startup.init_global_state() -- luacheck: ignore 121 prosody.bare_sessions = {}; prosody.full_sessions = {}; prosody.hosts = {}; -- COMPAT: These globals are deprecated -- luacheck: ignore 111/bare_sessions 111/full_sessions 111/hosts bare_sessions = prosody.bare_sessions; full_sessions = prosody.full_sessions; hosts = prosody.hosts; prosody.paths = { source = CFG_SOURCEDIR; config = CFG_CONFIGDIR or "."; plugins = CFG_PLUGINDIR or "plugins"; data = "data"; }; prosody.arg = _G.arg; _G.log = logger.init("general"); prosody.log = logger.init("general"); 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() prosody.paths.data = config.get("*", "data_path") or CFG_DATADIR or "data"; end function startup.setup_plugindir() local custom_plugin_paths = config.get("*", "plugin_paths"); local path_sep = package.config:sub(3,3); if custom_plugin_paths then -- path1;path2;path3;defaultpath... -- luacheck: ignore 111 CFG_PLUGINDIR = table.concat(custom_plugin_paths, path_sep)..path_sep..(CFG_PLUGINDIR or "plugins"); prosody.paths.plugins = CFG_PLUGINDIR; end end 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"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; prosody.paths.plugins = CFG_PLUGINDIR; end function startup.chdir() if prosody.installed then local lfs = require "lfs"; -- Ensure paths are absolute, not relative to the working directory which we're about to change local cwd = lfs.currentdir(); prosody.paths.source = config.resolve_relative_path(cwd, prosody.paths.source); prosody.paths.config = config.resolve_relative_path(cwd, prosody.paths.config); prosody.paths.data = config.resolve_relative_path(cwd, prosody.paths.data); -- Change working directory to data path. lfs.chdir(prosody.paths.data); end end function startup.add_global_prosody_functions() -- Function to reload the config file function prosody.reload_config() log("info", "Reloading configuration file"); prosody.events.fire_event("reloading-config"); local ok, level, err = config.load(prosody.config_file); if not ok then if level == "parser" then log("error", "There was an error parsing the configuration file: %s", err); elseif level == "file" then log("error", "Couldn't read the config file when trying to reload: %s", err); end else prosody.events.fire_event("config-reloaded", { filename = prosody.config_file, config = config.getconfig(), }); end return ok, (err and tostring(level)..": "..tostring(err)) or nil; end -- Function to reopen logfiles function prosody.reopen_logfiles() log("info", "Re-opening log files"); prosody.events.fire_event("reopen-log-files"); end -- Function to initiate prosody shutdown function prosody.shutdown(reason, code) log("info", "Shutting down: %s", reason or "unknown reason"); prosody.shutdown_reason = reason; prosody.shutdown_code = code; prosody.events.fire_event("server-stopping", { reason = reason; code = code; }); prosody.main_thread:run(startup.shutdown); end end function startup.load_secondary_libraries() --- Load and initialise core modules 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 --[[ pcall(require, "remdebug.engine"); if remdebug then remdebug.engine.start() end ]] require "prosody.util.stanza" require "prosody.util.jid" prosody.features = require "prosody.core.features".available; end function startup.init_http_client() 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 "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 "prosody.util.promise"; 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 "prosody.util.async"; 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.instrument() local statsmanager = require "prosody.core.statsmanager"; local timed = require"prosody.util.openmetrics".timed; local adns = require "prosody.net.adns"; if adns.instrument then local m = statsmanager.metric("histogram", "prosody_dns", "seconds", "DNS lookups", { "qclass"; "qtype" }, { buckets = { 1 / 1024; 1 / 256; 1 / 64; 1 / 16; 1 / 4; 1; 4 }; }); adns.instrument(function(qclass, qtype) return timed(m:with_labels(qclass, qtype)); end); end end function startup.init_data_store() 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.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() -- Catch global accesses -- luacheck: ignore 212/t local locked_globals_mt = { __index = function (t, k) log("warn", "%s", debug.traceback("Attempt to read a non-existent global '"..tostring(k).."'", 2)); end; __newindex = function (t, k, v) error("Attempt to set a global: "..tostring(k).." = "..tostring(v), 2); end; }; function prosody.unlock_globals() setmetatable(_G, nil); end function prosody.lock_globals() setmetatable(_G, locked_globals_mt); end -- And lock now... prosody.lock_globals(); end function startup.read_version() -- Try to determine version local version_file = io.open((CFG_SOURCEDIR or ".").."/prosody.version"); prosody.version = "unknown"; if version_file then prosody.version = version_file:read("*a"):gsub("%s*$", ""); version_file:close(); if #prosody.version == 12 and prosody.version:match("^[a-f0-9]+$") then prosody.version = "hg:"..prosody.version; end else local hg = require"prosody.util.mercurial"; local hgid = hg.check_id(CFG_SOURCEDIR or "."); if hgid then prosody.version = "hg:" .. hgid; end end end function startup.log_greeting() log("info", "Hello and welcome to Prosody version %s", prosody.version); end function startup.notify_started() running_state:finish_startup(); end -- Override logging config (used by prosodyctl) function startup.force_console_logging() original_logging_config = config.get("*", "log"); local log_level = os.getenv("PROSODYCTL_LOG_LEVEL"); if not log_level then if prosody.opts.verbose then log_level = "debug"; elseif prosody.opts.quiet then log_level = "error"; elseif prosody.opts.silent then config.set("*", "log", {}); -- ssssshush! return end end 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 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 -- We haz root! local desired_user = config.get("*", "prosody_user") or "prosody"; local desired_group = config.get("*", "prosody_group") or desired_user; local ok, err = pposix.setgid(desired_group); if ok then ok, err = pposix.initgroups(desired_user); end if ok then ok, err = pposix.setuid(desired_user); if ok then -- Yay! prosody.switched_user = true; end end if not prosody.switched_user then -- Boo! print("Warning: Couldn't switch to Prosody user/group '"..tostring(desired_user).."'/'"..tostring(desired_group).."': "..tostring(err)); elseif prosody.config_file then -- Make sure the Prosody user can read the config local conf, err, errno = io.open(prosody.config_file); --luacheck: ignore 211/errno if conf then conf:close(); else print("The config file is not readable by the '"..desired_user.."' user."); print("Prosody will not be able to read it."); print("Error was "..err); os.exit(1); end end end -- Set our umask to protect data files pposix.umask(config.get("*", "umask") or "027"); pposix.setenv("HOME", prosody.paths.data); pposix.setenv("PROSODY_CONFIG", prosody.config_file); else print("Error: Unable to load pposix module. Check that Prosody is installed correctly.") print("For more help send the below error to us through https://prosody.im/discuss"); print(tostring(pposix)) os.exit(1); end end function startup.check_unwriteable() local function test_writeable(filename) local f, err = io.open(filename, "a"); if not f then return false, err; end f:close(); return true; end local unwriteable_files = {}; if type(original_logging_config) == "string" and original_logging_config:sub(1,1) ~= "*" then local ok, err = test_writeable(original_logging_config); if not ok then table.insert(unwriteable_files, err); end elseif type(original_logging_config) == "table" then for _, rule in ipairs(original_logging_config) do if rule.filename then local ok, err = test_writeable(rule.filename); if not ok then table.insert(unwriteable_files, err); end end end end if #unwriteable_files > 0 then print("One of more of the Prosody log files are not"); print("writeable, please correct the errors and try"); print("starting prosodyctl again."); print(""); for _, err in ipairs(unwriteable_files) do print(err); end print(""); os.exit(1); end end function startup.init_gc() -- Apply garbage collector settings from the config file 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); if not ok then log("error", "Failed to apply GC configuration: %s", err); return nil, err; end return true; end function startup.init_errors() require "prosody.util.error".configure(config.get("*", "error_library") or {}); end function startup.make_host(hostname) return { type = "local", events = prosody.events, modules = {}, sessions = {}, users = require "prosody.core.usermanager".new_null_provider(hostname) }; end function startup.make_dummy_hosts() -- When running under prosodyctl, we don't want to -- fully initialize the server, so we populate prosody.hosts -- with just enough things for most code to work correctly -- luacheck: ignore 122/hosts prosody.core_post_stanza = function () end; -- TODO: mod_router! for hostname in pairs(config.getconfig()) do prosody.hosts[hostname] = startup.make_host(hostname); 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.notification_socket() 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 process manager.") 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 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"); 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); end function startup.exit() os.exit(prosody.shutdown_code, true); end -- prosodyctl only function startup.prosodyctl() prosody.process_type = "prosodyctl"; startup.parse_args(); startup.init_global_state(); startup.read_config(); startup.force_console_logging(); startup.init_logging(); startup.init_gc(); startup.init_errors(); startup.setup_plugindir(); startup.setup_plugin_install_path(); startup.setup_datadir(); startup.chdir(); startup.read_version(); startup.switch_user(); startup.check_dependencies(); startup.log_startup_warnings(); startup.check_unwriteable(); startup.load_libraries(); startup.init_http_client(); startup.make_dummy_hosts(); end function startup.prosody() -- These actions are in a strict order, as many depend on -- previous steps to have already been performed prosody.process_type = "prosody"; startup.parse_args(); startup.init_global_state(); startup.read_config(); startup.check_user(); startup.init_logging(); startup.init_gc(); startup.init_errors(); startup.sanity_check(); startup.sandbox_require(); startup.set_function_metatable(); startup.check_dependencies(); startup.load_libraries(); startup.setup_plugindir(); startup.setup_plugin_install_path(); startup.setup_datadir(); startup.chdir(); startup.add_global_prosody_functions(); startup.read_version(); startup.log_greeting(); startup.log_startup_warnings(); startup.load_secondary_libraries(); startup.init_promise(); startup.init_async(); startup.instrument(); startup.init_http_client(); startup.init_data_store(); startup.init_global_protection(); startup.posix_daemonize(); startup.write_pidfile(); startup.hook_posix_signals(); startup.notification_socket(); startup.prepare_to_start(); startup.notify_started(); end return startup; prosody-13.0.1/util/PaxHeaders/statistics.lua0000644000000000000000000000011714773555365016252 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.899711762 prosody-13.0.1/util/statistics.lua0000644000175000017500000000744114773555365020457 0ustar00prosodyprosody00000000000000local 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 -- Gauges local gauge_metric_mt = {} gauge_metric_mt.__index = gauge_metric_mt local function new_gauge_metric() local metric = { value = 0 } setmetatable(metric, gauge_metric_mt) return metric end function gauge_metric_mt:set(value) self.value = value end function gauge_metric_mt:add(delta) self.value = self.value + delta end function gauge_metric_mt:reset() self.value = 0 end function gauge_metric_mt:iter_samples() local done = false return function(_s) if done then return nil, true end done = true return "", nil, _s.value end, self end -- Counters local counter_metric_mt = {} counter_metric_mt.__index = counter_metric_mt local function new_counter_metric() local metric = { _created = time(), value = 0, } setmetatable(metric, counter_metric_mt) return metric end function counter_metric_mt:set(value) self.value = value end function counter_metric_mt:add(value) self.value = (self.value or 0) + value end function counter_metric_mt:iter_samples() local step = 0 return function(_s) step = step + 1 if step == 1 then return "_created", nil, _s._created elseif step == 2 then return "_total", nil, _s.value else return nil, nil, true end end, self end function counter_metric_mt:reset() self.value = 0 end -- Histograms local histogram_metric_mt = {} histogram_metric_mt.__index = histogram_metric_mt local function new_histogram_metric(buckets) local metric = { _created = time(), _sum = 0, _count = 0, } -- the order of buckets matters unfortunately, so we cannot directly use -- the thresholds as table keys for i, threshold in ipairs(buckets) do metric[i] = { threshold = threshold, threshold_s = render_histogram_le(threshold), count = 0 } end setmetatable(metric, histogram_metric_mt) return metric end function histogram_metric_mt:sample(value) -- According to the I-D, values must be part of all buckets for i, bucket in pairs(self) do if "number" == type(i) and value <= bucket.threshold then bucket.count = bucket.count + 1 end end self._sum = self._sum + value self._count = self._count + 1 end function histogram_metric_mt:iter_samples() local key = nil return function (_s) local data key, data = next(_s, key) if key == "_created" or key == "_sum" or key == "_count" then return key, nil, data elseif key ~= nil then return "_bucket", {["le"] = data.threshold_s}, data.count else return nil, nil, nil end end, self end function histogram_metric_mt:reset() self._created = time() self._count = 0 self._sum = 0 for i, bucket in pairs(self) do if "number" == type(i) then bucket.count = 0 end end end -- Summary local summary_metric_mt = {} summary_metric_mt.__index = summary_metric_mt local function new_summary_metric() -- quantiles are not supported yet local metric = { _created = time(), _sum = 0, _count = 0, } setmetatable(metric, summary_metric_mt) return metric end function summary_metric_mt:sample(value) self._sum = self._sum + value self._count = self._count + 1 end function summary_metric_mt:iter_samples() local key = nil return function (_s) local data key, data = next(_s, key) return key, nil, data end, self end function summary_metric_mt:reset() self._created = time() self._count = 0 self._sum = 0 end local pull_backend = { gauge = new_gauge_metric, counter = new_counter_metric, histogram = new_histogram_metric, summary = new_summary_metric, } -- END of Metric implementations local function new() return { metric_registry = new_metric_registry(pull_backend), } end return { new = new; } prosody-13.0.1/util/PaxHeaders/statsd.lua0000644000000000000000000000011714773555365015362 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.899711762 prosody-13.0.1/util/statsd.lua0000644000175000017500000001463714773555365017574 0ustar00prosodyprosody00000000000000local socket = require "socket"; local time = require "prosody.util.time".now; local array = require "prosody.util.array"; local t_concat = table.concat; 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 -- Gauges local gauge_metric_mt = {} gauge_metric_mt.__index = gauge_metric_mt local function new_gauge_metric(full_name, impl) local metric = { _full_name = full_name; _impl = impl; value = 0; } setmetatable(metric, gauge_metric_mt) return metric end function gauge_metric_mt:set(value) self.value = value self._impl:push_gauge(self._full_name, value) end function gauge_metric_mt:add(delta) self.value = self.value + delta self._impl:push_gauge(self._full_name, self.value) end function gauge_metric_mt:reset() self.value = 0 self._impl:push_gauge(self._full_name, 0) end function gauge_metric_mt.iter_samples() -- statsd backend does not support iteration. return function() return nil end end -- Counters local counter_metric_mt = {} counter_metric_mt.__index = counter_metric_mt local function new_counter_metric(full_name, impl) local metric = { _full_name = full_name, _impl = impl, value = 0, } setmetatable(metric, counter_metric_mt) return metric end function counter_metric_mt:set(value) local delta = value - self.value self.value = value self._impl:push_counter_delta(self._full_name, delta) end function counter_metric_mt:add(value) self.value = (self.value or 0) + value self._impl:push_counter_delta(self._full_name, value) end function counter_metric_mt.iter_samples() -- statsd backend does not support iteration. return function() return nil end end function counter_metric_mt:reset() self.value = 0 end -- Histograms local histogram_metric_mt = {} histogram_metric_mt.__index = histogram_metric_mt local function new_histogram_metric(buckets, full_name, impl) -- NOTE: even though the more or less proprietary dogstatsd has Its own -- histogram implementation, we push the individual buckets in this statsd -- backend for both consistency and compatibility across statsd -- implementations. local metric = { _sum_name = full_name..".sum", _count_name = full_name..".count", _impl = impl, _created = time(), _sum = 0, _count = 0, } -- the order of buckets matters unfortunately, so we cannot directly use -- the thresholds as table keys for i, threshold in ipairs(buckets) do local threshold_s = render_histogram_le(threshold) metric[i] = { threshold = threshold, threshold_s = threshold_s, count = 0, _full_name = full_name..".bucket."..(threshold_s:gsub("%.", "_")), } end setmetatable(metric, histogram_metric_mt) return metric end function histogram_metric_mt:sample(value) -- According to the I-D, values must be part of all buckets for i, bucket in pairs(self) do if "number" == type(i) and value <= bucket.threshold then bucket.count = bucket.count + 1 self._impl:push_counter_delta(bucket._full_name, 1) end end self._sum = self._sum + value self._count = self._count + 1 self._impl:push_gauge(self._sum_name, self._sum) self._impl:push_counter_delta(self._count_name, 1) end function histogram_metric_mt.iter_samples() -- statsd backend does not support iteration. return function() return nil end end function histogram_metric_mt:reset() self._created = time() self._count = 0 self._sum = 0 for i, bucket in pairs(self) do if "number" == type(i) then bucket.count = 0 end end self._impl:push_gauge(self._sum_name, self._sum) end -- Summaries local summary_metric_mt = {} summary_metric_mt.__index = summary_metric_mt local function new_summary_metric(full_name, impl) local metric = { _sum_name = full_name..".sum", _count_name = full_name..".count", _impl = impl, } setmetatable(metric, summary_metric_mt) return metric end function summary_metric_mt:sample(value) self._impl:push_counter_delta(self._sum_name, value) self._impl:push_counter_delta(self._count_name, 1) end function summary_metric_mt.iter_samples() -- statsd backend does not support iteration. return function() return nil end end function summary_metric_mt.reset() end -- BEGIN of statsd client implementation local statsd_mt = {} statsd_mt.__index = statsd_mt function statsd_mt:cork() self.corked = true self.cork_buffer = self.cork_buffer or {} end function statsd_mt:uncork() self.corked = false self:_flush_cork_buffer() end function statsd_mt:_flush_cork_buffer() local buffer = self.cork_buffer for metric_name, value in pairs(buffer) do self:_send_gauge(metric_name, value) buffer[metric_name] = nil end end function statsd_mt:push_gauge(metric_name, value) if self.corked then self.cork_buffer[metric_name] = value else self:_send_gauge(metric_name, value) end end function statsd_mt:_send_gauge(metric_name, value) self:_send(self.prefix..metric_name..":"..tostring(value).."|g") end function statsd_mt:push_counter_delta(metric_name, delta) self:_send(self.prefix..metric_name..":"..tostring(delta).."|c") end function statsd_mt:_send(s) return self.sock:send(s) end -- END of statsd client implementation local function build_metric_name(family_name, labels) local parts = array { family_name } if labels then parts:append(labels) end return t_concat(parts, "/"):gsub("%.", "_"):gsub("/", ".") end local function new(config) if not config or not config.statsd_server then return nil, "No statsd server specified in the config, please see https://prosody.im/doc/statistics"; end local sock = socket.udp(); sock:setpeername(config.statsd_server, config.statsd_port or 8125); local prefix = (config.prefix or "prosody").."."; local impl = { metric_registry = nil; sock = sock; prefix = prefix; }; setmetatable(impl, statsd_mt) local backend = { gauge = function(family_name, labels) return new_gauge_metric(build_metric_name(family_name, labels), impl) end; counter = function(family_name, labels) return new_counter_metric(build_metric_name(family_name, labels), impl) end; histogram = function(buckets, family_name, labels) return new_histogram_metric(buckets, build_metric_name(family_name, labels), impl) end; summary = function(family_name, labels, extra) return new_summary_metric(build_metric_name(family_name, labels), impl, extra) end; }; impl.metric_registry = new_metric_registry(backend); return impl; end return { new = new; } prosody-13.0.1/util/PaxHeaders/template.lua0000644000000000000000000000011714773555365015673 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.899711762 prosody-13.0.1/util/template.lua0000644000175000017500000000556414773555365020104 0ustar00prosodyprosody00000000000000-- luacheck: ignore 213/i local stanza_mt = require "prosody.util.stanza".stanza_mt; local setmetatable = setmetatable; local pairs = pairs; local ipairs = ipairs; local error = error; local envload = require "prosody.util.envload".envload; local debug = debug; local t_remove = table.remove; local parse_xml = require "prosody.util.xml".parse; local _ENV = nil; -- luacheck: std none local function trim_xml(stanza) for i=#stanza,1,-1 do local child = stanza[i]; if child.name then trim_xml(child); else child = child:gsub("^%s*", ""):gsub("%s*$", ""); stanza[i] = child; if child == "" then t_remove(stanza, i); end end end end local function create_string_string(str) str = ("%q"):format(str); str = str:gsub("{([^}]*)}", function(s) return '"..(data["'..s..'"]or"").."'; end); return str; end local function create_attr_string(attr, xmlns) local str = '{'; for name,value in pairs(attr) do if name ~= "xmlns" or value ~= xmlns then str = str..("[%q]=%s;"):format(name, create_string_string(value)); end end return str..'}'; end local function create_clone_string(stanza, lookup, xmlns) if not lookup[stanza] then local s = ('setmetatable({name=%q,attr=%s,tags={'):format(stanza.name, create_attr_string(stanza.attr, xmlns)); -- add tags for i,tag in ipairs(stanza.tags) do s = s..create_clone_string(tag, lookup, stanza.attr.xmlns)..";"; end s = s..'};'; -- add children for i,child in ipairs(stanza) do if child.name then s = s..create_clone_string(child, lookup, stanza.attr.xmlns)..";"; else s = s..create_string_string(child)..";" end end s = s..'}, stanza_mt)'; s = s:gsub('%.%.""', ""):gsub('([=;])""%.%.', "%1"):gsub(';"";', ";"); -- strip empty strings local n = #lookup + 1; lookup[n] = s; lookup[stanza] = "_"..n; end return lookup[stanza]; end local function create_cloner(stanza, chunkname) local lookup = {}; local name = create_clone_string(stanza, lookup, ""); local src = "local setmetatable,stanza_mt=...;return function(data)"; for i=1,#lookup do src = src.."local _"..i.."="..lookup[i]..";"; end src = src.."return "..name..";end"; local f,err = envload(src, chunkname); if not f then error(err); end return f(setmetatable, stanza_mt); end local template_mt = { __tostring = function(t) return t.name end }; local function create_template(templates, text) local stanza, err = parse_xml(text); if not stanza then error(err); end trim_xml(stanza); local info = debug.getinfo(3, "Sl"); info = info and ("template(%s:%d)"):format(info.short_src:match("[^\\/]*$"), info.currentline) or "template(unknown)"; local template = setmetatable({ apply = create_cloner(stanza, info), name = info, text = text }, template_mt); templates[text] = template; return template; end local templates = setmetatable({}, { __mode = 'k', __index = create_template }); return function(text) return templates[text]; end; prosody-13.0.1/util/PaxHeaders/termcolours.lua0000644000000000000000000000011714773555365016436 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.903711778 prosody-13.0.1/util/termcolours.lua0000644000175000017500000001047314773555365020642 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- -- luacheck: ignore 213/i local t_concat, t_insert = table.concat, table.insert; local char, format = string.char, string.format; local tonumber = tonumber; local ipairs = ipairs; local io_write = io.write; local m_floor = math.floor; local type = type; local setmetatable = setmetatable; local pairs = pairs; local windows; if os.getenv("WINDIR") then windows = require "prosody.util.windows"; end local orig_color = windows and windows.get_consolecolor and windows.get_consolecolor(); local _ENV = nil; -- luacheck: std none local stylemap = { reset = 0; bright = 1, dim = 2, underscore = 4, blink = 5, reverse = 7, hidden = 8; black = 30; red = 31; green = 32; yellow = 33; blue = 34; magenta = 35; cyan = 36; white = 37; ["black background"] = 40; ["red background"] = 41; ["green background"] = 42; ["yellow background"] = 43; ["blue background"] = 44; ["magenta background"] = 45; ["cyan background"] = 46; ["white background"] = 47; bold = 1, dark = 2, underline = 4, underlined = 4, normal = 0; } local winstylemap = { ["0"] = orig_color, -- reset ["1"] = 7+8, -- bold ["1;33"] = 2+4+8, -- bold yellow ["1;31"] = 4+8 -- bold red } local cssmap = { [1] = "font-weight: bold", [2] = "opacity: 0.5", [4] = "text-decoration: underline", [8] = "visibility: hidden", [30] = "color:black", [31] = "color:red", [32]="color:green", [33]="color:#FFD700", [34] = "color:blue", [35] = "color: magenta", [36] = "color:cyan", [37] = "color: white", [40] = "background-color:black", [41] = "background-color:red", [42]="background-color:green", [43]="background-color:yellow", [44] = "background-color:blue", [45] = "background-color: magenta", [46] = "background-color:cyan", [47] = "background-color: white"; }; local fmt_string = char(0x1B).."[%sm%s"..char(0x1B).."[0m"; local function getstring(style, text) if style then return format(fmt_string, style, text); else return text; end end local function gray(n) return m_floor(n*3/32)+0xe8; end local function color(r,g,b) if r == g and g == b then return gray(r); end r = m_floor(r*3/128); g = m_floor(g*3/128); b = m_floor(b*3/128); return 0x10 + ( r * 36 ) + ( g * 6 ) + ( b ); end local function hex2rgb(hex) local r = tonumber(hex:sub(1,2),16); local g = tonumber(hex:sub(3,4),16); local b = tonumber(hex:sub(5,6),16); return r,g,b; end setmetatable(stylemap, { __index = function(_, style) if type(style) == "string" and style:find("%x%x%x%x%x%x") == 1 then local g = style:sub(7) == " background" and "48;5;" or "38;5;"; return format("%s%d", g, color(hex2rgb(style))); end end } ); local csscolors = { red = "ff0000"; fuchsia = "ff00ff"; green = "008000"; white = "ffffff"; lime = "00ff00"; yellow = "ffff00"; purple = "800080"; blue = "0000ff"; aqua = "00ffff"; olive = "808000"; black = "000000"; navy = "000080"; teal = "008080"; silver = "c0c0c0"; maroon = "800000"; gray = "808080"; } for colorname, rgb in pairs(csscolors) do stylemap[colorname] = stylemap[colorname] or stylemap[rgb]; colorname, rgb = colorname .. " background", rgb .. " background" stylemap[colorname] = stylemap[colorname] or stylemap[rgb]; end local function getstyle(...) local styles, result = { ... }, {}; for i, style in ipairs(styles) do style = stylemap[style]; if style then t_insert(result, style); end end return t_concat(result, ";"); end local last = "0"; local function setstyle(style) style = style or "0"; if style ~= last then io_write("\27["..style.."m"); last = style; end end if windows then function setstyle(style) style = style or "0"; if style ~= last then windows.set_consolecolor(winstylemap[style] or orig_color); last = style; end end if not orig_color then function setstyle() end end end local function ansi2css(ansi_codes) if ansi_codes == "0" then return ""; end local css = {}; for code in ansi_codes:gmatch("[^;]+") do t_insert(css, cssmap[tonumber(code)]); end return ""; end local function tohtml(input) return input:gsub("\027%[(.-)m", ansi2css); end return { getstring = getstring; getstyle = getstyle; setstyle = setstyle; tohtml = tohtml; }; prosody-13.0.1/util/PaxHeaders/throttle.lua0000644000000000000000000000011714773555365015725 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.903711778 prosody-13.0.1/util/throttle.lua0000644000175000017500000000173714773555365020134 0ustar00prosodyprosody00000000000000 local gettime = require "prosody.util.time".now local setmetatable = setmetatable; local _ENV = nil; -- luacheck: std none local throttle = {}; local throttle_mt = { __index = throttle }; function throttle:update() local newt = gettime(); local elapsed = newt - self.t; self.t = newt; local balance = (self.rate * elapsed) + self.balance; if balance > self.max then self.balance = self.max; else self.balance = balance; end return self.balance; end function throttle:peek(cost) cost = cost or 1; return self.balance >= cost or self:update() >= cost; end function throttle:poll(cost, split) if self:peek(cost) then self.balance = self.balance - cost; return true; else local balance = self.balance; if split then self.balance = 0; end return false, balance, (cost-balance); end end local function create(max, period) return setmetatable({ rate = max / period, max = max, t = gettime(), balance = max }, throttle_mt); end return { create = create; }; prosody-13.0.1/util/PaxHeaders/timer.lua0000644000000000000000000000011714773555365015200 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.903711778 prosody-13.0.1/util/timer.lua0000644000175000017500000000627214773555365017406 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- 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 "prosody.util.xpcall".xpcall; local math_max = math.max; local pairs = pairs; if server.timer then -- The selected net.server implements this API, so defer to that return server.timer; end local _ENV = nil; -- luacheck: std none local _add_task = server.add_task; local _server_timer; local _active_timers = 0; local h = indexedbheap.create(); local params = {}; local next_time = nil; local function _traceback_handler(err) log("error", "Traceback[timer]: %s", debug_traceback(tostring(err), 2)); end local function _on_timer(now) local peek; local readd; while true do peek = h:peek(); if peek == nil or peek > now then break; end local _, callback, id = h:pop(); local param = params[id]; params[id] = nil; --item(now, id, _param); local success, err = xpcall(callback, _traceback_handler, now, id, param); if success and type(err) == "number" then if readd then readd[id] = { callback, err + now }; else readd = { [id] = { callback, err + now } }; end params[id] = param; end end if readd then for id,timer in pairs(readd) do h:insert(timer[1], timer[2], id); end peek = h:peek(); end if peek ~= nil and _active_timers > 1 and peek == next_time then -- Another instance of _on_timer already set next_time to the same value, -- so it should be safe to not renew this timer event peek = nil; else next_time = peek; end if peek then -- peek is the time of the next event return peek - now; end _active_timers = _active_timers - 1; end local function add_task(delay, callback, param) local current_time = get_time(); local event_time = current_time + delay; local id = h:insert(callback, event_time); params[id] = param; if next_time == nil or event_time < next_time then next_time = event_time; if _server_timer then _server_timer:close(); _server_timer = nil; else _active_timers = _active_timers + 1; end _server_timer = _add_task(next_time - current_time, _on_timer); end return id; end local function stop(id) params[id] = nil; local result, item, result_sync = h:remove(id); local peek = h:peek(); if peek ~= next_time and _server_timer then next_time = peek; _server_timer:close(); if next_time ~= nil then _server_timer = _add_task(math_max(next_time - get_time(), 0), _on_timer); end end return result, item, result_sync; end local function reschedule(id, delay) local current_time = get_time(); local event_time = current_time + delay; h:reprioritize(id, delay); if next_time == nil or event_time < next_time then next_time = event_time; _add_task(next_time - current_time, _on_timer); end return id; end return { add_task = add_task; stop = stop; reschedule = reschedule; }; prosody-13.0.1/util/PaxHeaders/uuid.lua0000644000000000000000000000011714773555365015026 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.903711778 prosody-13.0.1/util/uuid.lua0000644000175000017500000000275514773555365017236 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local random = require "prosody.util.random"; local random_bytes = random.bytes; 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); end local function get_twobits() return ("%x"):format(random_bytes(1):byte() % 4 + 8); end local function generate() -- generate RFC 4122 complaint UUIDs (version 4 - random) 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 seed = random.seed; }; prosody-13.0.1/util/PaxHeaders/watchdog.lua0000644000000000000000000000011714773555365015660 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.903711778 prosody-13.0.1/util/watchdog.lua0000644000175000017500000000152014773555365020055 0ustar00prosodyprosody00000000000000local timer = require "prosody.util.timer"; local setmetatable = setmetatable; local _ENV = nil; -- luacheck: std none local watchdog_methods = {}; local watchdog_mt = { __index = watchdog_methods }; local function new(timeout, callback) local watchdog = setmetatable({ timeout = timeout; callback = callback; timer_id = nil; }, watchdog_mt); watchdog:reset(); -- Kick things off return watchdog; end 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() if self.timer_id then timer.stop(self.timer_id); self.timer_id = nil; end end return { new = new; }; prosody-13.0.1/util/PaxHeaders/x509.lua0000644000000000000000000000011714773555365014565 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.907711794 prosody-13.0.1/util/x509.lua0000644000175000017500000001750114773555365016770 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2010 Matthew Wild -- Copyright (C) 2010 Paul Aurich -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- TODO: I feel a fair amount of this logic should be integrated into Luasec, -- so that everyone isn't re-inventing the wheel. Dependencies on -- IDN libraries complicate that. -- [TLS-CERTS] - https://www.rfc-editor.org/rfc/rfc6125.html -- Obsolete -- [TLS-IDENT] - https://www.rfc-editor.org/rfc/rfc9525.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; local _ENV = nil; -- luacheck: std none local oid_commonname = "2.5.4.3"; -- [LDAP] 2.3 local oid_subjectaltname = "2.5.29.17"; -- [PKIX] 4.2.1.6 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] -- Compare a hostname (possibly international) with asserted names extracted from a certificate. -- This function follows the rules laid out in section 6.3 of [TLS-IDENT] -- -- A wildcard ("*") all by itself is allowed only as the left-most label local function compare_dnsname(host, asserted_names) -- TODO: Sufficient normalization? Review relevant specs. local norm_host = idna_to_ascii(host) if norm_host == nil then log("info", "Host %s failed IDNA ToASCII operation", host) return false end norm_host = norm_host:lower() local host_chopped = norm_host:gsub("^[^.]+%.", "") -- everything after the first label for i=1,#asserted_names do local name = asserted_names[i] if norm_host == name:lower() then log("debug", "Cert dNSName %s matched hostname", name); return true end -- Allow the left most label to be a "*" if name:match("^%*%.") then local rest_name = name:gsub("^[^.]+%.", "") if host_chopped == rest_name:lower() then log("debug", "Cert dNSName %s matched hostname", name); return true end end end return false end -- Compare an XMPP domain name with the asserted id-on-xmppAddr -- identities extracted from a certificate. Both are UTF8 strings. -- -- Per [XMPP-CORE], matches against asserted identities don't include -- wildcards, so we just do a normalize on both and then a string comparison -- -- TODO: Support for full JIDs? local function compare_xmppaddr(host, asserted_names) local norm_host = nameprep(host) for i=1,#asserted_names do local name = asserted_names[i] -- We only want to match against bare domains right now, not -- those crazy full-er JIDs. if name:match("[@/]") then log("debug", "Ignoring xmppAddr %s because it's not a bare domain", name) else local norm_name = nameprep(name) if norm_name == nil then log("info", "Ignoring xmppAddr %s, failed nameprep!", name) else if norm_host == norm_name then log("debug", "Cert xmppAddr %s matched hostname", name) return true end end end end return false end -- Compare a host + service against the asserted id-on-dnsSRV (SRV-ID) -- identities extracted from a certificate. -- -- Per [SRV-ID], the asserted identities will be encoded in ASCII via ToASCII. -- Comparison is done case-insensitively, and a wildcard ("*") all by itself -- is allowed only as the left-most non-service label. local function compare_srvname(host, service, asserted_names) local norm_host = idna_to_ascii(host) if norm_host == nil then log("info", "Host %s failed IDNA ToASCII operation", host); return false end -- Service names start with a "_" if service:match("^_") == nil then service = "_"..service end norm_host = norm_host:lower(); local host_chopped = norm_host:gsub("^[^.]+%.", "") -- everything after the first label for i=1,#asserted_names do local asserted_service, name = asserted_names[i]:match("^(_[^.]+)%.(.*)"); if service == asserted_service then if norm_host == name:lower() then log("debug", "Cert SRVName %s matched hostname", name); return true; end -- Allow the left most label to be a "*" if name:match("^%*%.") then local rest_name = name:gsub("^[^.]+%.", "") if host_chopped == rest_name:lower() then log("debug", "Cert SRVName %s matched hostname", name) return true end end if norm_host == name:lower() then log("debug", "Cert SRVName %s matched hostname", name); return true end end end return false end local function verify_identity(host, service, cert) if cert.setencode then cert:setencode("utf8"); end local ext = cert:extensions() if ext[oid_subjectaltname] then local sans = ext[oid_subjectaltname]; if sans[oid_xmppaddr] then if service == "_xmpp-client" or service == "_xmpp-server" then if compare_xmppaddr(host, sans[oid_xmppaddr]) then return true end end end if sans[oid_dnssrv] then -- Only check srvNames if the caller specified a service if service and compare_srvname(host, service, sans[oid_dnssrv]) then return true end end if sans["dNSName"] then if compare_dnsname(host, sans["dNSName"]) then return true end end end -- Per [TLS-IDENT] ignore the Common Name -- The server identity can only be expressed in the subjectAltNames extension; -- it is no longer valid to use the commonName RDN, known as CN-ID in [TLS-CERTS]. -- If all else fails, well, why should we be any different? return false end -- TODO Support other SANs local function get_identities(cert) --> map of names to sets of services if cert.setencode then cert:setencode("utf8"); end local names = mt.new(); local ext = cert:extensions(); local sans = ext[oid_subjectaltname]; if sans then if sans["dNSName"] then -- Valid for any service for _, name in ipairs(sans["dNSName"]) do local is_wildcard = name:sub(1, 2) == "*."; if is_wildcard then name = name:sub(3); end name = idna_to_unicode(nameprep(name)); if name then if is_wildcard then name = "*." .. name; end names:set(name, "*", true); end end end if sans[oid_xmppaddr] then for _, name in ipairs(sans[oid_xmppaddr]) do name = nameprep(name); if name then names:set(name, "xmpp-client", true); names:set(name, "xmpp-server", true); end end end if sans[oid_dnssrv] then for _, srvname in ipairs(sans[oid_dnssrv]) do local srv, name = srvname:match("^_([^.]+)%.(.*)"); if srv then name = nameprep(name); if name then names:set(name, srv, true); end end end end end local subject = cert:subject(); for i = 1, #subject do local dn = subject[i]; if dn.oid == oid_commonname then local name = nameprep(dn.value); if name and idna_to_ascii(name) then names:set(name, "*", true); end end end return names.data; end local pat = "%-%-%-%-%-BEGIN ([A-Z ]+)%-%-%-%-%-\r?\n".. "([0-9A-Za-z+/=\r\n]*)\r?\n%-%-%-%-%-END %1%-%-%-%-%-"; local function pem2der(pem) local typ, data = pem:match(pat); if typ and data then return base64.decode(data), typ; end end local wrap = ('.'):rep(64); local envelope = "-----BEGIN %s-----\n%s\n-----END %s-----\n" local function der2pem(data, typ) typ = typ and typ:upper() or "CERTIFICATE"; data = base64.encode(data); return s_format(envelope, typ, data:gsub(wrap, '%0\n', (#data-1)/64), typ); end return { verify_identity = verify_identity; get_identities = get_identities; pem2der = pem2der; der2pem = der2pem; }; prosody-13.0.1/util/PaxHeaders/xml.lua0000644000000000000000000000011714773555365014660 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.907711794 prosody-13.0.1/util/xml.lua0000644000175000017500000000523414773555365017063 0ustar00prosodyprosody00000000000000 local st = require "prosody.util.stanza"; local lxp = require "lxp"; local t_insert = table.insert; local t_remove = table.remove; local error = error; local _ENV = nil; -- luacheck: std none local parse_xml = (function() local ns_prefixes = { ["http://www.w3.org/XML/1998/namespace"] = "xml"; }; local ns_separator = "\1"; local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$"; return function(xml, options) --luacheck: ignore 212/self local handler = {}; local stanza = st.stanza("root"); local namespaces = {}; local prefixes = {}; function handler:StartNamespaceDecl(prefix, url) if prefix ~= nil then t_insert(namespaces, url); t_insert(prefixes, prefix); end end function handler:EndNamespaceDecl(prefix) if prefix ~= nil then -- we depend on each StartNamespaceDecl having a paired EndNamespaceDecl t_remove(namespaces); t_remove(prefixes); end end function handler:StartElement(tagname, attr) local curr_ns,name = tagname:match(ns_pattern); if name == "" then curr_ns, name = "", curr_ns; end if curr_ns ~= "" then attr.xmlns = curr_ns; end for i=1,#attr do local k = attr[i]; attr[i] = nil; local ns, nm = k:match(ns_pattern); if nm ~= "" then ns = ns_prefixes[ns]; if ns then attr[ns..":"..nm] = attr[k]; attr[k] = nil; end end end local n = {} for i=1,#namespaces do n[prefixes[i]] = namespaces[i]; end stanza:tag(name, attr, n); end function handler:CharacterData(data) stanza:text(data); end function handler:EndElement() stanza:up(); end -- SECURITY: These two handlers, especially the Doctype one, are required to prevent exploits such as Billion Laughs. local function restricted_handler(parser) if not parser.stop or not parser:stop() then error("Failed to abort parsing"); end end handler.StartDoctypeDecl = restricted_handler; if not options or not options.allow_comments then -- NOTE: comments are generally harmless and can be useful when parsing configuration files or other data, even user-provided data handler.Comment = restricted_handler; end if not options or not options.allow_processing_instructions then -- Processing instructions should generally be safe to just ignore handler.ProcessingInstruction = restricted_handler; end local parser = lxp.new(handler, ns_separator); local ok, err, line, col = parser:parse(xml); if ok then ok, err, line, col = parser:parse(); end --parser:close(); if ok then return stanza.tags[1]; else return ok, ("%s (line %d, col %d))"):format(err, line, col); end end; end)(); return { parse = parse_xml; }; prosody-13.0.1/util/PaxHeaders/xmppstream.lua0000644000000000000000000000011714773555365016260 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.907711794 prosody-13.0.1/util/xmppstream.lua0000644000175000017500000002161014773555365020457 0ustar00prosodyprosody00000000000000-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local lxp = require "lxp"; local st = require "prosody.util.stanza"; local stanza_mt = st.stanza_mt; local error = error; local tostring = tostring; local t_insert = table.insert; local t_concat = table.concat; local t_remove = table.remove; local setmetatable = setmetatable; -- COMPAT: w/LuaExpat 1.1.0 local lxp_supports_doctype = pcall(lxp.new, { StartDoctypeDecl = false }); local lxp_supports_xmldecl = pcall(lxp.new, { XmlDecl = false }); local lxp_supports_bytecount = not not lxp.new({}).getcurrentbytecount; local default_stanza_size_limit = 1024*1024*1; -- 1MB local _ENV = nil; -- luacheck: std none local new_parser = lxp.new; local xml_namespace = { ["http://www.w3.org/XML/1998/namespace\1lang"] = "xml:lang"; ["http://www.w3.org/XML/1998/namespace\1space"] = "xml:space"; ["http://www.w3.org/XML/1998/namespace\1base"] = "xml:base"; ["http://www.w3.org/XML/1998/namespace\1id"] = "xml:id"; }; local xmlns_streams = "http://etherx.jabber.org/streams"; local ns_separator = "\1"; local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$"; local function dummy_cb() end local function new_sax_handlers(session, stream_callbacks, cb_handleprogress) local xml_handlers = {}; local cb_streamopened = stream_callbacks.streamopened; local cb_streamclosed = stream_callbacks.streamclosed; local cb_error = stream_callbacks.error or function(_, e, stanza) error("XML stream error: "..tostring(e)..(stanza and ": "..tostring(stanza) or ""),2); end; local cb_handlestanza = stream_callbacks.handlestanza; cb_handleprogress = cb_handleprogress or dummy_cb; local stream_ns = stream_callbacks.stream_ns or xmlns_streams; local stream_tag = stream_callbacks.stream_tag or "stream"; if stream_ns ~= "" then stream_tag = stream_ns..ns_separator..stream_tag; end local stream_error_tag = stream_ns..ns_separator..(stream_callbacks.error_tag or "error"); local stream_default_ns = stream_callbacks.default_ns; local stream_lang = "en"; local stack = {}; local chardata, stanza = {}; local stanza_size = 0; local non_streamns_depth = 0; function xml_handlers:StartElement(tagname, attr) if stanza and #chardata > 0 then -- We have some character data in the buffer t_insert(stanza, t_concat(chardata)); chardata = {}; end local curr_ns,name = tagname:match(ns_pattern); if name == "" then curr_ns, name = "", curr_ns; end if curr_ns ~= stream_default_ns or non_streamns_depth > 0 then attr.xmlns = curr_ns; non_streamns_depth = non_streamns_depth + 1; end for i=1,#attr do local k = attr[i]; attr[i] = nil; local xmlk = xml_namespace[k]; if xmlk then attr[xmlk] = attr[k]; attr[k] = nil; end end if not stanza then --if we are not currently inside a stanza if lxp_supports_bytecount then stanza_size = self:getcurrentbytecount(); end if session.notopen then if tagname == stream_tag then non_streamns_depth = 0; stream_lang = attr["xml:lang"] or stream_lang; if cb_streamopened then if lxp_supports_bytecount then cb_handleprogress(stanza_size); stanza_size = 0; end cb_streamopened(session, attr); end else -- Garbage before stream? cb_error(session, "no-stream", tagname); end return; end if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then cb_error(session, "invalid-top-level-element"); end stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt); else -- we are inside a stanza, so add a tag if lxp_supports_bytecount then stanza_size = stanza_size + self:getcurrentbytecount(); end t_insert(stack, stanza); local oldstanza = stanza; stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt); t_insert(oldstanza, stanza); t_insert(oldstanza.tags, stanza); end end function xml_handlers:StartCdataSection() if lxp_supports_bytecount then if stanza then stanza_size = stanza_size + self:getcurrentbytecount(); else cb_handleprogress(self:getcurrentbytecount()); end end end function xml_handlers:EndCdataSection() if lxp_supports_bytecount then if stanza then stanza_size = stanza_size + self:getcurrentbytecount(); else cb_handleprogress(self:getcurrentbytecount()); end end end function xml_handlers:CharacterData(data) if stanza then if lxp_supports_bytecount then stanza_size = stanza_size + self:getcurrentbytecount(); end t_insert(chardata, data); elseif lxp_supports_bytecount then cb_handleprogress(self:getcurrentbytecount()); end end function xml_handlers:EndElement(tagname) if lxp_supports_bytecount then stanza_size = stanza_size + self:getcurrentbytecount() end if non_streamns_depth > 0 then non_streamns_depth = non_streamns_depth - 1; end if stanza then if #chardata > 0 then -- We have some character data in the buffer t_insert(stanza, t_concat(chardata)); chardata = {}; end -- Complete stanza if #stack == 0 then if lxp_supports_bytecount then cb_handleprogress(stanza_size); end stanza_size = 0; if stanza.attr["xml:lang"] == nil then stanza.attr["xml:lang"] = stream_lang; end if tagname ~= stream_error_tag then cb_handlestanza(session, stanza); else cb_error(session, "stream-error", stanza); end stanza = nil; else stanza = t_remove(stack); end else if lxp_supports_bytecount then cb_handleprogress(stanza_size); end if cb_streamclosed then cb_streamclosed(session); end end end local function restricted_handler(parser) cb_error(session, "parse-error", "restricted-xml", "Restricted XML, see RFC 6120 section 11.1."); if not parser.stop or not parser:stop() then error("Failed to abort parsing"); end end if lxp_supports_xmldecl then function xml_handlers:XmlDecl(version, encoding, standalone) if lxp_supports_bytecount then cb_handleprogress(self:getcurrentbytecount()); end if (encoding and encoding:lower() ~= "utf-8") or (standalone == "no") or (version and version ~= "1.0") then return restricted_handler(self); end end end if lxp_supports_doctype then xml_handlers.StartDoctypeDecl = restricted_handler; end xml_handlers.Comment = restricted_handler; xml_handlers.ProcessingInstruction = restricted_handler; local function reset() stanza, chardata, stanza_size = nil, {}, 0; stack = {}; end local function set_session(stream, new_session) -- luacheck: ignore 212/stream session = new_session; end return xml_handlers, { reset = reset, set_session = set_session }; end local function new(session, stream_callbacks, stanza_size_limit) -- Used to track parser progress (e.g. to enforce size limits) local n_outstanding_bytes = 0; local handle_progress; if lxp_supports_bytecount then function handle_progress(n_parsed_bytes) n_outstanding_bytes = n_outstanding_bytes - n_parsed_bytes; end stanza_size_limit = stanza_size_limit or default_stanza_size_limit; elseif stanza_size_limit then error("Stanza size limits are not supported on this version of LuaExpat") end local handlers, meta = new_sax_handlers(session, stream_callbacks, handle_progress); local parser = new_parser(handlers, ns_separator, false); local parse = parser.parse; function session.open_stream(session, from, to) -- luacheck: ignore 432/session local send = session.sends2s or session.send; local attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", ["xml:lang"] = "en", xmlns = stream_callbacks.default_ns, version = session.version and (session.version > 0 and "1.0" or nil), id = session.streamid or "", from = from or session.host, to = to, }; if session.stream_attrs then session:stream_attrs(from, to, attr) end send(""..st.stanza("stream:stream", attr):top_tag()); return true; end return { reset = function () parser = new_parser(handlers, ns_separator, false); parse = parser.parse; n_outstanding_bytes = 0; meta.reset(); end, feed = function (self, data) -- luacheck: ignore 212/self if lxp_supports_bytecount then n_outstanding_bytes = n_outstanding_bytes + #data; end local _parser = parser; local ok, err = parse(_parser, data); if lxp_supports_bytecount and n_outstanding_bytes > stanza_size_limit then return nil, "stanza-too-large"; end if parser ~= _parser then _parser:parse(); _parser:close(); end return ok, err; end, set_session = meta.set_session; set_stanza_size_limit = function (_, new_stanza_size_limit) stanza_size_limit = new_stanza_size_limit; end; }; end return { ns_separator = ns_separator; ns_pattern = ns_pattern; new_sax_handlers = new_sax_handlers; new = new; }; prosody-13.0.1/util/PaxHeaders/xpcall.lua0000644000000000000000000000011714773555365015343 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.907711794 prosody-13.0.1/util/xpcall.lua0000644000175000017500000000030314773555365017536 0ustar00prosodyprosody00000000000000local xpcall = xpcall; if select(2, xpcall(function (x) return x end, function () end, "test")) ~= "test" then xpcall = require"prosody.util.compat".xpcall; end return { xpcall = xpcall; }; prosody-13.0.1/util/PaxHeaders/xtemplate.lua0000644000000000000000000000011714773555365016063 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.907711794 prosody-13.0.1/util/xtemplate.lua0000644000175000017500000000543514773555365020271 0ustar00prosodyprosody00000000000000local s_gsub = string.gsub; local s_match = string.match; local s_sub = string.sub; local t_concat = table.concat; local st = require("prosody.util.stanza"); local function render(template, root, escape, filters) escape = escape or st.xml_escape; 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 if path == "." then value = root; elseif path == "#" then value = root:get_text(); else value = root:find(path); end local is_escaped = false; while pipe == "|" do local func, args, tmpl, p = s_match(inner, "^(%w+)(%b())(%b{})()", pos); if not func then func, args, p = s_match(inner, "^(%w+)(%b())()", pos); end if not func then func, tmpl, p = s_match(inner, "^(%w+)(%b{})()", pos); end if not func then func, p = s_match(inner, "^(%w+)()", pos); end if not func then break end if tmpl then tmpl = s_sub(tmpl, 2, -2); end if args then args = s_sub(args, 2, -2); end if func == "each" and tmpl then 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 ns = s_sub(ns, 2, -2); else name, ns = args, nil; end if ns == "" then ns = nil; end if name == "" then name = nil; end local out, i = {}, 1; for c in (value):childtags(name, ns) do out[i], i = render(tmpl, c, escape, filters), i + 1; end value = t_concat(out); is_escaped = true; elseif func == "and" and tmpl then local condition = value; if args then condition = root:find(args); end if condition then value = render(tmpl, root, escape, filters); is_escaped = true; end elseif func == "or" and tmpl then local condition = value; if args then condition = root:find(args); end if not condition then value = render(tmpl, root, escape, filters); is_escaped = true; end elseif filters and filters[func] then local f = filters[func]; value, is_escaped = f(value, args, tmpl); else error("No such filter function: " .. func); end pipe, pos = s_match(inner, "^(|?)()", p); end if type(value) == "string" then if not is_escaped then value = escape(value); end return pre_blank .. value .. post_blank elseif st.is_stanza(value) then value = value:get_text(); if value then return pre_blank .. escape(value) .. post_blank end end return pre_blank .. post_blank end)) end return { render = render } prosody-13.0.1/PaxHeaders/util-src0000644000000000000000000000013114773555365014061 xustar0029 mtime=1743706869.95171197 30 atime=1743706869.815711427 30 ctime=1743706869.827711475 prosody-13.0.1/util-src/0000755000175000017500000000000014773555365016341 5ustar00prosodyprosody00000000000000prosody-13.0.1/util-src/PaxHeaders/GNUmakefile0000644000000000000000000000011714773555365016214 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.815711427 prosody-13.0.1/util-src/GNUmakefile0000644000175000017500000000123314773555365020412 0ustar00prosodyprosody00000000000000 include ../config.unix CFLAGS+=-I$(LUA_INCDIR) INSTALL_DATA=install -m644 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 crypto.so ifdef RANDOM ALL+=crand.so endif .PHONY: all install clean .SUFFIXES: .c .o .so all: $(ALL) install: $(ALL) $(INSTALL_DATA) $? $(TARGET) clean: rm -f $(ALL) $(patsubst %.so,%.o,$(ALL)) encodings.o: CFLAGS+=$(IDNA_FLAGS) encodings.so: LDLIBS+=$(IDNA_LIBS) crypto.so hashes.so: LDLIBS+=$(OPENSSL_LIBS) crand.o: CFLAGS+=-DWITH_$(RANDOM) crand.so: LDLIBS+=$(RANDOM_LIBS) %.so: %.o $(LD) $(LDFLAGS) -o $@ $^ $(LDLIBS) prosody-13.0.1/util-src/PaxHeaders/Makefile.win0000644000000000000000000000011714773555365016376 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.815711427 prosody-13.0.1/util-src/Makefile.win0000644000175000017500000000250114773555365020573 0ustar00prosodyprosody00000000000000 LUA_PATH=$(LUA_DEV) IDN_PATH=..\..\libidn-1.15 OPENSSL_PATH=..\..\openssl-0.9.8k LUA_INCLUDE=$(LUA_PATH)\include LUA_LIB=$(LUA_PATH)\lib\lua5.1.lib IDN_LIB=$(IDN_PATH)\win32\lib\libidn.lib IDN_INCLUDE1=$(IDN_PATH)\lib IDN_INCLUDE2=$(IDN_PATH)\win32\include OPENSSL_LIB=$(OPENSSL_PATH)\out32dll\libeay32.lib OPENSSL_INCLUDE=$(OPENSSL_PATH)\include CL=cl /LD /MD /nologo all: encodings.dll hashes.dll windows.dll install: encodings.dll hashes.dll windows.dll copy /Y *.dll ..\util\ clean: del encodings.dll encodings.exp encodings.lib encodings.obj encodings.dll.manifest del hashes.dll hashes.exp hashes.lib hashes.obj hashes.dll.manifest del windows.dll windows.exp windows.lib windows.obj windows.dll.manifest encodings.dll: encodings.c $(CL) encodings.c /I"$(LUA_INCLUDE)" /I"$(IDN_INCLUDE1)" /I"$(IDN_INCLUDE2)" /link "$(LUA_LIB)" "$(IDN_LIB)" /export:luaopen_util_encodings del encodings.exp encodings.lib encodings.obj encodings.dll.manifest hashes.dll: hashes.c $(CL) hashes.c /I"$(LUA_INCLUDE)" /I"$(OPENSSL_INCLUDE)" /link "$(LUA_LIB)" "$(OPENSSL_LIB)" /export:luaopen_util_hashes del hashes.exp hashes.lib hashes.obj hashes.dll.manifest windows.dll: windows.c $(CL) windows.c /I"$(LUA_INCLUDE)" /link "$(LUA_LIB)" dnsapi.lib /export:luaopen_util_windows del windows.exp windows.lib windows.obj windows.dll.manifest prosody-13.0.1/util-src/PaxHeaders/compat.c0000644000000000000000000000011714773555365015571 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.815711427 prosody-13.0.1/util-src/compat.c0000644000175000017500000000136014773555365017770 0ustar00prosodyprosody00000000000000 #include #include static int lc_xpcall (lua_State *L) { int ret; int n_arg = lua_gettop(L); /* f, msgh, p1, p2... */ luaL_argcheck(L, n_arg >= 2, 2, "value expected"); lua_pushvalue(L, 1); /* f to top */ lua_pushvalue(L, 2); /* msgh to top */ lua_replace(L, 1); /* msgh to 1 */ lua_replace(L, 2); /* f to 2 */ /* msgh, f, p1, p2... */ ret = lua_pcall(L, n_arg - 2, LUA_MULTRET, 1); lua_pushboolean(L, ret == 0); lua_replace(L, 1); return lua_gettop(L); } int luaopen_prosody_util_compat(lua_State *L) { lua_createtable(L, 0, 2); { lua_pushcfunction(L, lc_xpcall); lua_setfield(L, -2, "xpcall"); } return 1; } int luaopen_util_compat(lua_State *L) { return luaopen_prosody_util_compat(L); } prosody-13.0.1/util-src/PaxHeaders/crand.c0000644000000000000000000000011714773555365015375 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.815711427 prosody-13.0.1/util-src/crand.c0000644000175000017500000000557014773555365017603 0ustar00prosodyprosody00000000000000/* Prosody IM -- Copyright (C) 2008-2017 Matthew Wild -- Copyright (C) 2008-2017 Waqas Hussain -- Copyright (C) 2016-2017 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- */ /* * crand.c * C PRNG interface * * The purpose of this module is to provide access to a PRNG in * environments without /dev/urandom * * Caution! This has not been extensively tested. * */ #define _DEFAULT_SOURCE #include #include #include #include "lualib.h" #include "lauxlib.h" #if defined(WITH_GETRANDOM) #ifndef __GLIBC_PREREQ /* Not compiled with glibc at all */ #define __GLIBC_PREREQ(a,b) 0 #endif #if ! __GLIBC_PREREQ(2,25) /* Not compiled with a glibc that provides getrandom() */ #include #include #ifndef SYS_getrandom #error getrandom() requires Linux 3.17 or later #endif /* This wasn't present before glibc 2.25 */ static int getrandom(void *buf, size_t buflen, unsigned int flags) { return syscall(SYS_getrandom, buf, buflen, flags); } #else #include #endif #elif defined(WITH_OPENSSL) #include #elif defined(WITH_ARC4RANDOM) #ifdef __linux__ #include #endif #else #error util.crand compiled without a random source #endif #ifndef SMALLBUFSIZ #define SMALLBUFSIZ 32 #endif static int Lrandom(lua_State *L) { char smallbuf[SMALLBUFSIZ]; char *buf = &smallbuf[0]; const lua_Integer l = luaL_checkinteger(L, 1); const size_t len = l; luaL_argcheck(L, l >= 0, 1, "must be > 0"); if(len == 0) { lua_pushliteral(L, ""); return 1; } if(len > SMALLBUFSIZ) { buf = lua_newuserdata(L, len); } #if defined(WITH_GETRANDOM) /* * This acts like a read from /dev/urandom with the exception that it * *does* block if the entropy pool is not yet initialized. */ int left = len; char *p = buf; do { int ret = getrandom(p, left, 0); if(ret < 0) { lua_pushstring(L, strerror(errno)); return lua_error(L); } p += ret; left -= ret; } while(left > 0); #elif defined(WITH_ARC4RANDOM) arc4random_buf(buf, len); #elif defined(WITH_OPENSSL) if(!RAND_status()) { lua_pushliteral(L, "OpenSSL PRNG not seeded"); return lua_error(L); } if(RAND_bytes((unsigned char *)buf, len) != 1) { /* TODO ERR_get_error() */ lua_pushstring(L, "RAND_bytes() failed"); return lua_error(L); } #endif lua_pushlstring(L, buf, len); return 1; } int luaopen_prosody_util_crand(lua_State *L) { luaL_checkversion(L); lua_createtable(L, 0, 2); lua_pushcfunction(L, Lrandom); lua_setfield(L, -2, "bytes"); #if defined(WITH_GETRANDOM) lua_pushstring(L, "Linux"); #elif defined(WITH_ARC4RANDOM) lua_pushstring(L, "arc4random()"); #elif defined(WITH_OPENSSL) lua_pushstring(L, "OpenSSL"); #endif lua_setfield(L, -2, "_source"); return 1; } int luaopen_util_crand(lua_State *L) { return luaopen_prosody_util_crand(L); } prosody-13.0.1/util-src/PaxHeaders/crypto.c0000644000000000000000000000011714773555365015626 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.815711427 prosody-13.0.1/util-src/crypto.c0000644000175000017500000005102014773555365020023 0ustar00prosodyprosody00000000000000/* Prosody IM -- Copyright (C) 2022 Matthew Wild -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- */ /* * crypto.c * Lua library for cryptographic operations using OpenSSL */ #include #include #ifdef _MSC_VER typedef unsigned __int32 uint32_t; #else #include #endif #include "lua.h" #include "lauxlib.h" #include #include #include #include #include #include #include #if (LUA_VERSION_NUM == 501) #define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) #endif /* The max size of an encoded 'R' or 'S' value. P-521 = 521 bits = 66 bytes */ #define MAX_ECDSA_SIG_INT_BYTES 66 #include "managed_pointer.h" #define PKEY_MT_TAG "util.crypto key" static BIO* new_memory_BIO(void) { return BIO_new(BIO_s_mem()); } MANAGED_POINTER_ALLOCATOR(new_managed_EVP_MD_CTX, EVP_MD_CTX*, EVP_MD_CTX_new, EVP_MD_CTX_free) MANAGED_POINTER_ALLOCATOR(new_managed_BIO_s_mem, BIO*, new_memory_BIO, BIO_free) MANAGED_POINTER_ALLOCATOR(new_managed_EVP_CIPHER_CTX, EVP_CIPHER_CTX*, EVP_CIPHER_CTX_new, EVP_CIPHER_CTX_free) #define CRYPTO_KEY_TYPE_ERR "unexpected key type: got '%s', expected '%s'" static EVP_PKEY* pkey_from_arg(lua_State *L, int idx, const int type, const int require_private) { EVP_PKEY *pkey = *(EVP_PKEY**)luaL_checkudata(L, idx, PKEY_MT_TAG); int got_type; if(type || require_private) { lua_getuservalue(L, idx); if(type != 0) { lua_getfield(L, -1, "type"); got_type = lua_tointeger(L, -1); if(got_type != type) { const char *got_key_type_name = OBJ_nid2sn(got_type); const char *want_key_type_name = OBJ_nid2sn(type); lua_pushfstring(L, CRYPTO_KEY_TYPE_ERR, got_key_type_name, want_key_type_name); luaL_argerror(L, idx, lua_tostring(L, -1)); } lua_pop(L, 1); } if(require_private != 0) { lua_getfield(L, -1, "private"); if(lua_toboolean(L, -1) != 1) { luaL_argerror(L, idx, "private key expected, got public key only"); } lua_pop(L, 1); } lua_pop(L, 1); } return pkey; } static int Lpkey_finalizer(lua_State *L) { EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 0); EVP_PKEY_free(pkey); return 0; } static int Lpkey_meth_get_type(lua_State *L) { EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 0); int key_type = EVP_PKEY_id(pkey); lua_pushstring(L, OBJ_nid2sn(key_type)); return 1; } static int 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); } prosody-13.0.1/util-src/PaxHeaders/encodings.c0000644000000000000000000000011714773555365016257 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.819711443 prosody-13.0.1/util-src/encodings.c0000644000175000017500000003447114773555365020467 0ustar00prosodyprosody00000000000000/* Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 1994-2015 Lua.org, PUC-Rio. -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- */ /* * encodings.c * Lua library for base64, stringprep and idna encodings */ /* Newer MSVC compilers deprecate strcpy as unsafe, but we use it in a safe way */ #define _CRT_SECURE_NO_DEPRECATE #include #include #include "lua.h" #include "lauxlib.h" #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif /***************** BASE64 *****************/ static const char code[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; static void base64_encode(luaL_Buffer *b, unsigned int c1, unsigned int c2, unsigned int c3, int n) { unsigned long tuple = c3 + 256UL * (c2 + 256UL * c1); int i; char s[4]; for(i = 0; i < 4; i++) { s[3 - i] = code[tuple % 64]; tuple /= 64; } for(i = n + 1; i < 4; i++) { s[i] = '='; } luaL_addlstring(b, s, 4); } static int Lbase64_encode(lua_State *L) { /** encode(s) */ size_t l; const unsigned char *s = (const unsigned char *)luaL_checklstring(L, 1, &l); luaL_Buffer b; int n; luaL_buffinit(L, &b); for(n = l / 3; n--; s += 3) { base64_encode(&b, s[0], s[1], s[2], 3); } switch(l % 3) { case 1: base64_encode(&b, s[0], 0, 0, 1); break; case 2: base64_encode(&b, s[0], s[1], 0, 2); break; } luaL_pushresult(&b); return 1; } static void base64_decode(luaL_Buffer *b, int c1, int c2, int c3, int c4, int n) { unsigned long tuple = c4 + 64L * (c3 + 64L * (c2 + 64L * c1)); char s[3]; switch(--n) { case 3: s[2] = (char) tuple; /* Falls through. */ case 2: s[1] = (char)(tuple >> 8); /* Falls through. */ case 1: s[0] = (char)(tuple >> 16); } luaL_addlstring(b, s, n); } static int Lbase64_decode(lua_State *L) { /** decode(s) */ size_t l; const char *s = luaL_checklstring(L, 1, &l); luaL_Buffer b; int n = 0; char t[4]; luaL_buffinit(L, &b); for(;;) { int c = *s++; switch(c) { const char *p; default: p = strchr(code, c); if(p == NULL) { return 0; } t[n++] = (char)(p - code); if(n == 4) { base64_decode(&b, t[0], t[1], t[2], t[3], 4); n = 0; } break; case '=': switch(n) { case 1: base64_decode(&b, t[0], 0, 0, 0, 1); break; case 2: base64_decode(&b, t[0], t[1], 0, 0, 2); break; case 3: base64_decode(&b, t[0], t[1], t[2], 0, 3); break; } n = 0; break; case 0: luaL_pushresult(&b); return 1; case '\n': case '\r': case '\t': case ' ': case '\f': case '\b': break; } } } static const luaL_Reg Reg_base64[] = { { "encode", Lbase64_encode }, { "decode", Lbase64_decode }, { NULL, NULL } }; /******************* UTF-8 ********************/ /* * Adapted from Lua 5.3 * Needed because libidn does not validate that input is valid UTF-8 */ #define MAXUNICODE 0x10FFFF /* * Decode one UTF-8 sequence, returning NULL if byte sequence is invalid. */ static const char *utf8_decode(const char *o, int *val) { static const unsigned int limits[] = {0xFF, 0x7F, 0x7FF, 0xFFFF}; const unsigned char *s = (const unsigned char *)o; unsigned int c = s[0]; unsigned int res = 0; /* final result */ if(c < 0x80) { /* ascii? */ res = c; } else { int count = 0; /* to count number of continuation bytes */ while(c & 0x40) { /* still have continuation bytes? */ int cc = s[++count]; /* read next byte */ if((cc & 0xC0) != 0x80) { /* not a continuation byte? */ return NULL; /* invalid byte sequence */ } res = (res << 6) | (cc & 0x3F); /* add lower 6 bits from cont. byte */ c <<= 1; /* to test next bit */ } res |= ((c & 0x7F) << (count * 5)); /* add first byte */ if(count > 3 || res > MAXUNICODE || res <= limits[count] || (0xd800 <= res && res <= 0xdfff)) { return NULL; /* invalid byte sequence */ } s += count; /* skip continuation bytes read */ } if(val) { *val = res; } return (const char *)s + 1; /* +1 to include first byte */ } /* * Check that a string is valid UTF-8 * Returns NULL if not */ static const char *check_utf8(lua_State *L, int idx, size_t *l) { size_t pos, len; const char *s = luaL_checklstring(L, idx, &len); pos = 0; while(pos <= len) { const char *s1 = utf8_decode(s + pos, NULL); if(s1 == NULL) { /* conversion error? */ return NULL; } pos = s1 - s; } if(l != NULL) { *l = len; } return s; } static int Lutf8_valid(lua_State *L) { lua_pushboolean(L, check_utf8(L, 1, NULL) != NULL); return 1; } static int Lutf8_length(lua_State *L) { size_t len; if(!check_utf8(L, 1, &len)) { luaL_pushfail(L); lua_pushliteral(L, "invalid utf8"); return 2; } lua_pushinteger(L, len); return 1; } static const luaL_Reg Reg_utf8[] = { { "valid", Lutf8_valid }, { "length", Lutf8_length }, { NULL, NULL } }; /***************** STRINGPREP *****************/ #ifdef USE_STRINGPREP_ICU #include #include #include #include #include static int icu_stringprep_prep(lua_State *L, const UStringPrepProfile *profile) { size_t input_len; int32_t unprepped_len, prepped_len, output_len; const char *input; char output[1024]; int flags = USPREP_ALLOW_UNASSIGNED; UChar unprepped[1024]; /* Temporary unicode buffer (1024 characters) */ UChar prepped[1024]; UErrorCode err = U_ZERO_ERROR; input = luaL_checklstring(L, 1, &input_len); if(input_len >= 1024) { luaL_pushfail(L); return 1; } /* strict */ if(!lua_isnoneornil(L, 2)) { luaL_checktype(L, 2, LUA_TBOOLEAN); if(lua_toboolean(L, 2)) { flags = 0; } } u_strFromUTF8(unprepped, 1024, &unprepped_len, input, input_len, &err); if(U_FAILURE(err)) { luaL_pushfail(L); return 1; } prepped_len = usprep_prepare(profile, unprepped, unprepped_len, prepped, 1024, flags, NULL, &err); if(U_FAILURE(err)) { luaL_pushfail(L); return 1; } else { u_strToUTF8(output, 1024, &output_len, prepped, prepped_len, &err); if(U_SUCCESS(err) && output_len < 1024) { lua_pushlstring(L, output, output_len); } else { luaL_pushfail(L); } return 1; } } static UStringPrepProfile *icu_nameprep; static UStringPrepProfile *icu_nodeprep; static UStringPrepProfile *icu_resourceprep; static UStringPrepProfile *icu_saslprep; static USpoofChecker *icu_spoofcheck; static UIDNA *icu_idna2008; #if (U_ICU_VERSION_MAJOR_NUM < 58) /* COMPAT */ #define USPOOF_CONFUSABLE (USPOOF_SINGLE_SCRIPT_CONFUSABLE | USPOOF_MIXED_SCRIPT_CONFUSABLE | USPOOF_WHOLE_SCRIPT_CONFUSABLE) #endif /* initialize global ICU stringprep profiles */ static void init_icu(void) { UErrorCode err = U_ZERO_ERROR; utrace_setLevel(UTRACE_VERBOSE); icu_nameprep = usprep_openByType(USPREP_RFC3491_NAMEPREP, &err); icu_nodeprep = usprep_openByType(USPREP_RFC3920_NODEPREP, &err); icu_resourceprep = usprep_openByType(USPREP_RFC3920_RESOURCEPREP, &err); icu_saslprep = usprep_openByType(USPREP_RFC4013_SASLPREP, &err); icu_spoofcheck = uspoof_open(&err); uspoof_setChecks(icu_spoofcheck, USPOOF_CONFUSABLE, &err); int options = UIDNA_DEFAULT; #if 0 /* COMPAT with future Unicode versions */ options |= UIDNA_ALLOW_UNASSIGNED; #endif #if 1 /* Forbid eg labels starting with _ */ options |= UIDNA_USE_STD3_RULES; #endif #if 0 /* TODO determine if we need this */ options |= UIDNA_CHECK_BIDI; #endif #if 0 /* UTS46 makes it sound like these are the responsibility of registrars */ options |= UIDNA_CHECK_CONTEXTJ; options |= UIDNA_CHECK_CONTEXTO; #endif #if 0 /* This disables COMPAT with IDNA 2003 */ options |= UIDNA_NONTRANSITIONAL_TO_ASCII; options |= UIDNA_NONTRANSITIONAL_TO_UNICODE; #endif icu_idna2008 = uidna_openUTS46(options, &err); if(U_FAILURE(err)) { fprintf(stderr, "[c] util.encodings: error: %s\n", u_errorName(err)); } } #define MAKE_PREP_FUNC(myFunc, prep) \ static int myFunc(lua_State *L) { return icu_stringprep_prep(L, prep); } MAKE_PREP_FUNC(Lstringprep_nameprep, icu_nameprep) /** stringprep.nameprep(s) */ MAKE_PREP_FUNC(Lstringprep_nodeprep, icu_nodeprep) /** stringprep.nodeprep(s) */ MAKE_PREP_FUNC(Lstringprep_resourceprep, icu_resourceprep) /** stringprep.resourceprep(s) */ MAKE_PREP_FUNC(Lstringprep_saslprep, icu_saslprep) /** stringprep.saslprep(s) */ static const luaL_Reg Reg_stringprep[] = { { "nameprep", Lstringprep_nameprep }, { "nodeprep", Lstringprep_nodeprep }, { "resourceprep", Lstringprep_resourceprep }, { "saslprep", Lstringprep_saslprep }, { NULL, NULL } }; #else /* USE_STRINGPREP_ICU */ /****************** libidn ********************/ #include static int stringprep_prep(lua_State *L, const Stringprep_profile *profile) { size_t len; const char *s; char string[1024]; int ret; Stringprep_profile_flags flags = 0; s = check_utf8(L, 1, &len); /* strict */ if(!lua_isnoneornil(L, 2)) { luaL_checktype(L, 2, LUA_TBOOLEAN); if(lua_toboolean(L, 2)) { flags = STRINGPREP_NO_UNASSIGNED; } } if(s == NULL || len >= 1024 || len != strlen(s)) { luaL_pushfail(L); return 1; /* TODO return error message */ } strcpy(string, s); ret = stringprep(string, 1024, flags, profile); if(ret == STRINGPREP_OK) { lua_pushstring(L, string); return 1; } else { luaL_pushfail(L); return 1; /* TODO return error message */ } } #define MAKE_PREP_FUNC(myFunc, prep) \ static int myFunc(lua_State *L) { return stringprep_prep(L, prep); } MAKE_PREP_FUNC(Lstringprep_nameprep, stringprep_nameprep) /** stringprep.nameprep(s) */ MAKE_PREP_FUNC(Lstringprep_nodeprep, stringprep_xmpp_nodeprep) /** stringprep.nodeprep(s) */ MAKE_PREP_FUNC(Lstringprep_resourceprep, stringprep_xmpp_resourceprep) /** stringprep.resourceprep(s) */ MAKE_PREP_FUNC(Lstringprep_saslprep, stringprep_saslprep) /** stringprep.saslprep(s) */ static const luaL_Reg Reg_stringprep[] = { { "nameprep", Lstringprep_nameprep }, { "nodeprep", Lstringprep_nodeprep }, { "resourceprep", Lstringprep_resourceprep }, { "saslprep", Lstringprep_saslprep }, { NULL, NULL } }; #endif /***************** IDNA *****************/ #ifdef USE_STRINGPREP_ICU #include #include /* IDNA2003 or IDNA2008 ? ? ? */ static int Lidna_to_ascii(lua_State *L) { /** idna.to_ascii(s) */ size_t len; int32_t ulen, dest_len, output_len; const char *s = luaL_checklstring(L, 1, &len); UChar ustr[1024]; UErrorCode err = U_ZERO_ERROR; UChar dest[1024]; char output[1024]; u_strFromUTF8(ustr, 1024, &ulen, s, len, &err); if(U_FAILURE(err)) { luaL_pushfail(L); return 1; } UIDNAInfo info = UIDNA_INFO_INITIALIZER; dest_len = uidna_nameToASCII(icu_idna2008, ustr, ulen, dest, 256, &info, &err); if(U_FAILURE(err) || info.errors) { luaL_pushfail(L); return 1; } else { u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err); if(U_SUCCESS(err) && output_len < 1024) { lua_pushlstring(L, output, output_len); } else { luaL_pushfail(L); } return 1; } } static int Lidna_to_unicode(lua_State *L) { /** idna.to_unicode(s) */ size_t len; int32_t ulen, dest_len, output_len; const char *s = luaL_checklstring(L, 1, &len); UChar ustr[1024]; UErrorCode err = U_ZERO_ERROR; UChar dest[1024]; char output[1024]; u_strFromUTF8(ustr, 1024, &ulen, s, len, &err); if(U_FAILURE(err)) { luaL_pushfail(L); return 1; } UIDNAInfo info = UIDNA_INFO_INITIALIZER; dest_len = uidna_nameToUnicode(icu_idna2008, ustr, ulen, dest, 1024, &info, &err); if(U_FAILURE(err) || info.errors) { luaL_pushfail(L); return 1; } else { u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err); if(U_SUCCESS(err) && output_len < 1024) { lua_pushlstring(L, output, output_len); } else { luaL_pushfail(L); } return 1; } } static int Lskeleton(lua_State *L) { size_t len; int32_t ulen, dest_len, output_len; const char *s = luaL_checklstring(L, 1, &len); UErrorCode err = U_ZERO_ERROR; UChar ustr[1024]; UChar dest[1024]; char output[1024]; u_strFromUTF8(ustr, 1024, &ulen, s, len, &err); if(U_FAILURE(err)) { luaL_pushfail(L); return 1; } dest_len = uspoof_getSkeleton(icu_spoofcheck, 0, ustr, ulen, dest, 1024, &err); if(U_FAILURE(err)) { luaL_pushfail(L); return 1; } u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err); if(U_SUCCESS(err)) { lua_pushlstring(L, output, output_len); return 1; } luaL_pushfail(L); return 1; } #else /* USE_STRINGPREP_ICU */ /****************** libidn ********************/ #include #include static int Lidna_to_ascii(lua_State *L) { /** idna.to_ascii(s) */ size_t len; const char *s = check_utf8(L, 1, &len); char *output = NULL; int ret; if(s == NULL || len != strlen(s)) { luaL_pushfail(L); return 1; /* TODO return error message */ } ret = idna_to_ascii_8z(s, &output, IDNA_USE_STD3_ASCII_RULES); if(ret == IDNA_SUCCESS) { lua_pushstring(L, output); idn_free(output); return 1; } else { luaL_pushfail(L); idn_free(output); return 1; /* TODO return error message */ } } static int Lidna_to_unicode(lua_State *L) { /** idna.to_unicode(s) */ size_t len; const char *s = luaL_checklstring(L, 1, &len); char *output = NULL; int ret = idna_to_unicode_8z8z(s, &output, 0); if(ret == IDNA_SUCCESS) { lua_pushstring(L, output); idn_free(output); return 1; } else { luaL_pushfail(L); idn_free(output); return 1; /* TODO return error message */ } } #endif static const luaL_Reg Reg_idna[] = { { "to_ascii", Lidna_to_ascii }, { "to_unicode", Lidna_to_unicode }, { NULL, NULL } }; /***************** end *****************/ LUALIB_API int luaopen_prosody_util_encodings(lua_State *L) { luaL_checkversion(L); #ifdef USE_STRINGPREP_ICU init_icu(); #endif lua_newtable(L); lua_newtable(L); luaL_setfuncs(L, Reg_base64, 0); lua_setfield(L, -2, "base64"); lua_newtable(L); luaL_setfuncs(L, Reg_stringprep, 0); lua_setfield(L, -2, "stringprep"); lua_newtable(L); luaL_setfuncs(L, Reg_idna, 0); lua_setfield(L, -2, "idna"); lua_newtable(L); luaL_setfuncs(L, Reg_utf8, 0); lua_setfield(L, -2, "utf8"); #ifdef USE_STRINGPREP_ICU lua_newtable(L); lua_pushcfunction(L, Lskeleton); lua_setfield(L, -2, "skeleton"); lua_setfield(L, -2, "confusable"); #endif lua_pushliteral(L, "-3.14"); lua_setfield(L, -2, "version"); return 1; } LUALIB_API int luaopen_util_encodings(lua_State *L) { return luaopen_prosody_util_encodings(L); } prosody-13.0.1/util-src/PaxHeaders/hashes.c0000644000000000000000000000011714773555365015561 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.819711443 prosody-13.0.1/util-src/hashes.c0000644000175000017500000002005314773555365017760 0ustar00prosodyprosody00000000000000/* Prosody IM -- Copyright (C) 2009-2010 Matthew Wild -- Copyright (C) 2009-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- */ /* * hashes.c * Lua library for sha1, sha256 and md5 hashes */ #include #include #ifdef _MSC_VER typedef unsigned __int32 uint32_t; #else #include #endif #include "lua.h" #include "lauxlib.h" #include #include #include #include #include #include #include /* 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) { int i; for(i = 0; i < length; i++) { out[i * 2] = hex_tab[(in[i] >> 4) & 0xF]; out[i * 2 + 1] = hex_tab[(in[i]) & 0xF]; } } static int Levp_hash(lua_State *L, const EVP_MD *evp) { size_t len; unsigned int size = EVP_MAX_MD_SIZE; const char *s = luaL_checklstring(L, 1, &len); int hex_out = lua_toboolean(L, 2); unsigned char hash[EVP_MAX_MD_SIZE], result[EVP_MAX_MD_SIZE * 2]; EVP_MD_CTX *ctx = EVP_MD_CTX_new(); if(ctx == NULL) { goto fail; } if(!EVP_DigestInit_ex(ctx, evp, NULL)) { goto fail; } if(!EVP_DigestUpdate(ctx, s, len)) { goto fail; } if(!EVP_DigestFinal_ex(ctx, hash, &size)) { goto fail; } EVP_MD_CTX_free(ctx); if(hex_out) { toHex(hash, size, result); lua_pushlstring(L, (char *)result, size * 2); } else { lua_pushlstring(L, (char *)hash, size); } return 1; fail: EVP_MD_CTX_free(ctx); return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); } static int Lsha1(lua_State *L) { return Levp_hash(L, EVP_sha1()); } static int Lsha224(lua_State *L) { return Levp_hash(L, EVP_sha224()); } static int Lsha256(lua_State *L) { return Levp_hash(L, EVP_sha256()); } static int Lsha384(lua_State *L) { return Levp_hash(L, EVP_sha384()); } static int Lsha512(lua_State *L) { return Levp_hash(L, EVP_sha512()); } static int Lmd5(lua_State *L) { return Levp_hash(L, EVP_md5()); } static int Lblake2s256(lua_State *L) { return Levp_hash(L, EVP_blake2s256()); } static int Lblake2b512(lua_State *L) { return Levp_hash(L, EVP_blake2b512()); } 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, out_len, out) == 0) { return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); } 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) { return Levp_pbkdf2(L, EVP_sha256(), SHA256_DIGEST_LENGTH); } /* HKDF(length, input, salt, info) */ static int Levp_hkdf(lua_State *L, const EVP_MD *evp) { unsigned char out[MAX_HKDF_OUTPUT]; size_t input_len, salt_len, info_len; size_t actual_out_len = luaL_checkinteger(L, 1); const unsigned char *input = (unsigned char *)luaL_checklstring(L, 2, &input_len); const unsigned char *salt = (unsigned char *)luaL_optlstring(L, 3, NULL, &salt_len); const unsigned char *info = (unsigned char *)luaL_checklstring(L, 4, &info_len); 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)); } 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); const char *s2 = luaL_checklstring(L, 2, &len2); if(len1 == len2) { lua_pushboolean(L, CRYPTO_memcmp(s1, s2, len1) == 0); } else { lua_pushboolean(L, 0); } return 1; } static const luaL_Reg Reg[] = { { "sha1", Lsha1 }, { "sha224", Lsha224 }, { "sha256", Lsha256 }, { "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_prosody_util_hashes(lua_State *L) { luaL_checkversion(L); 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_hashes(lua_State *L) { return luaopen_prosody_util_hashes(L); } prosody-13.0.1/util-src/PaxHeaders/make.bat0000644000000000000000000000011714773555365015547 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.819711443 prosody-13.0.1/util-src/make.bat0000644000175000017500000000004114773555365017741 0ustar00prosodyprosody00000000000000@nmake /nologo /f Makefile.win %*prosody-13.0.1/util-src/PaxHeaders/makefile0000644000000000000000000000011714773555365015642 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.819711443 prosody-13.0.1/util-src/makefile0000644000175000017500000000162314773555365020043 0ustar00prosodyprosody00000000000000include ../config.unix CFLAGS+=-I$(LUA_INCDIR) INSTALL_DATA=install -m644 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 .ifdef $(RANDOM) ALL+=crand.so .endif .PHONY: all install clean .SUFFIXES: .c .o .so all: $(ALL) install: $(ALL) $(INSTALL_DATA) $(ALL) $(TARGET) clean: rm -f $(ALL) $(patsubst %.so,%.o,$(ALL)) encodings.o: encodings.c $(CC) $(CFLAGS) $(IDNA_FLAGS) -c -o $@ $< encodings.so: encodings.o $(LD) $(LDFLAGS) -o $@ $< $(LDLIBS) $(IDNA_LIBS) hashes.so: hashes.o $(LD) $(LDFLAGS) -o $@ $< $(LDLIBS) $(OPENSSL_LIBS) crand.o: crand.c $(CC) $(CFLAGS) -DWITH_$(RANDOM) -c -o $@ $< crand.so: crand.o $(LD) $(LDFLAGS) -o $@ $< $(LDLIBS) $(RANDOM_LIBS) %.so: %.o $(LD) $(LDFLAGS) -o $@ $< $(LDLIBS) .c.o: $(CC) $(CFLAGS) -c -o $@ $< .o.so: $(LD) $(LDFLAGS) -o $@ $< $(LDLIBS) prosody-13.0.1/util-src/PaxHeaders/managed_pointer.h0000644000000000000000000000011714773555365017447 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.819711443 prosody-13.0.1/util-src/managed_pointer.h0000644000175000017500000000527614773555365021660 0ustar00prosodyprosody00000000000000/* 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; \ } prosody-13.0.1/util-src/PaxHeaders/net.c0000644000000000000000000000011714773555365015074 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.819711443 prosody-13.0.1/util-src/net.c0000644000175000017500000001012714773555365017274 0ustar00prosodyprosody00000000000000/* Prosody IM -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- Copyright (C) 2012 Paul Aurich -- Copyright (C) 2013 Matthew Wild -- Copyright (C) 2013 Florian Zeitz -- */ #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include #include #include #ifndef _WIN32 #include #include #include #include #include #include #include #endif #include #include #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif /* Enumerate all locally configured IP addresses */ static const char *const type_strings[] = { "both", "ipv4", "ipv6", NULL }; static int lc_local_addresses(lua_State *L) { #ifndef _WIN32 /* Link-local IPv4 addresses; see RFC 3927 and RFC 5735 */ const uint32_t ip4_linklocal = htonl(0xa9fe0000); /* 169.254.0.0 */ const uint32_t ip4_mask = htonl(0xffff0000); struct ifaddrs *addr = NULL, *a; #endif int n = 1; int type = luaL_checkoption(L, 1, "both", type_strings); const char link_local = lua_toboolean(L, 2); /* defaults to 0 (false) */ const char ipv4 = (type == 0 || type == 1); const char ipv6 = (type == 0 || type == 2); #ifndef _WIN32 if(getifaddrs(&addr) < 0) { luaL_pushfail(L); lua_pushfstring(L, "getifaddrs failed (%d): %s", errno, strerror(errno)); return 2; } #endif lua_newtable(L); #ifndef _WIN32 for(a = addr; a; a = a->ifa_next) { int family; char ipaddr[INET6_ADDRSTRLEN]; const char *tmp = NULL; if(a->ifa_addr == NULL || a->ifa_flags & IFF_LOOPBACK) { continue; } family = a->ifa_addr->sa_family; if(ipv4 && family == AF_INET) { struct sockaddr_in *sa = (struct sockaddr_in *)a->ifa_addr; if(!link_local && ((sa->sin_addr.s_addr & ip4_mask) == ip4_linklocal)) { continue; } tmp = inet_ntop(family, &sa->sin_addr, ipaddr, sizeof(ipaddr)); } else if(ipv6 && family == AF_INET6) { struct sockaddr_in6 *sa = (struct sockaddr_in6 *)a->ifa_addr; if(!link_local && IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) { continue; } if(IN6_IS_ADDR_V4MAPPED(&sa->sin6_addr) || IN6_IS_ADDR_V4COMPAT(&sa->sin6_addr)) { continue; } tmp = inet_ntop(family, &sa->sin6_addr, ipaddr, sizeof(ipaddr)); } if(tmp != NULL) { lua_pushstring(L, tmp); lua_rawseti(L, -2, n++); } /* TODO: Error reporting? */ } freeifaddrs(addr); #else if(ipv4) { lua_pushstring(L, "0.0.0.0"); lua_rawseti(L, -2, n++); } if(ipv6) { lua_pushstring(L, "::"); lua_rawseti(L, -2, n++); } #endif return 1; } static int lc_pton(lua_State *L) { char buf[16]; const char *ipaddr = luaL_checkstring(L, 1); int errno_ = 0; int family = strchr(ipaddr, ':') ? AF_INET6 : AF_INET; switch(inet_pton(family, ipaddr, &buf)) { case 1: lua_pushlstring(L, buf, family == AF_INET6 ? 16 : 4); return 1; case -1: errno_ = errno; luaL_pushfail(L); lua_pushstring(L, strerror(errno_)); lua_pushinteger(L, errno_); return 3; default: case 0: luaL_pushfail(L); lua_pushstring(L, strerror(EINVAL)); lua_pushinteger(L, EINVAL); return 3; } } static int lc_ntop(lua_State *L) { char buf[INET6_ADDRSTRLEN]; int family; int errno_; size_t l; const char *ipaddr = luaL_checklstring(L, 1, &l); if(l == 16) { family = AF_INET6; } else if(l == 4) { family = AF_INET; } else { luaL_pushfail(L); lua_pushstring(L, strerror(EAFNOSUPPORT)); lua_pushinteger(L, EAFNOSUPPORT); return 3; } if(!inet_ntop(family, ipaddr, buf, INET6_ADDRSTRLEN)) { errno_ = errno; luaL_pushfail(L); lua_pushstring(L, strerror(errno_)); lua_pushinteger(L, errno_); return 3; } lua_pushstring(L, (const char *)(&buf)); return 1; } int luaopen_prosody_util_net(lua_State *L) { luaL_checkversion(L); luaL_Reg exports[] = { { "local_addresses", lc_local_addresses }, { "pton", lc_pton }, { "ntop", lc_ntop }, { NULL, NULL } }; lua_createtable(L, 0, 1); luaL_setfuncs(L, exports, 0); return 1; } int luaopen_util_net(lua_State *L) { return luaopen_prosody_util_net(L); } prosody-13.0.1/util-src/PaxHeaders/poll.c0000644000000000000000000000011614773555365015253 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.82371146 prosody-13.0.1/util-src/poll.c0000644000175000017500000003023314773555365017454 0ustar00prosodyprosody00000000000000 /* * Lua polling library * Copyright (C) 2017-2022 Kim Alvefur * * This project is MIT licensed. Please see the * COPYING file in the source package for more information. * */ #include #include #if defined(__linux__) #define USE_EPOLL #define POLL_BACKEND "epoll" #elif defined(__unix__) #define USE_POLL #define POLL_BACKEND "poll" #else #define USE_SELECT #define POLL_BACKEND "select" #endif #ifdef USE_EPOLL #include #include #ifndef MAX_EVENTS /* Maximum number of returned events, retrieved into Lpoll_state */ #define MAX_EVENTS 256 #endif #endif #ifdef USE_POLL #include #ifndef MAX_WATCHED /* Maximum number of watched sockets, kept in Lpoll_state */ #define MAX_WATCHED 10000 #endif #endif #ifdef USE_SELECT #include #endif #include #include #define STATE_MT "util.poll<" POLL_BACKEND ">" #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif /* * Structure to keep state for each type of API */ typedef struct Lpoll_state { int processed; #ifdef USE_EPOLL int epoll_fd; struct epoll_event events[MAX_EVENTS]; #endif #ifdef USE_POLL nfds_t count; struct pollfd events[MAX_WATCHED]; #endif #ifdef USE_SELECT fd_set wantread; fd_set wantwrite; fd_set readable; fd_set writable; fd_set all; fd_set err; #endif } Lpoll_state; /* * Add an FD to be watched */ static int Ladd(lua_State *L) { struct Lpoll_state *state = luaL_checkudata(L, 1, STATE_MT); int fd = luaL_checkinteger(L, 2); int wantread = lua_toboolean(L, 3); int wantwrite = lua_toboolean(L, 4); if(fd < 0) { luaL_pushfail(L); lua_pushstring(L, strerror(EBADF)); lua_pushinteger(L, EBADF); return 3; } #ifdef USE_EPOLL struct epoll_event event; event.data.fd = fd; event.events = (wantread ? EPOLLIN : 0) | (wantwrite ? EPOLLOUT : 0); event.events |= EPOLLERR | EPOLLHUP | EPOLLRDHUP; int ret = epoll_ctl(state->epoll_fd, EPOLL_CTL_ADD, fd, &event); if(ret < 0) { ret = errno; luaL_pushfail(L); lua_pushstring(L, strerror(ret)); lua_pushinteger(L, ret); return 3; } lua_pushboolean(L, 1); return 1; #endif #ifdef USE_POLL for(nfds_t i = 0; i < state->count; i++) { if(state->events[i].fd == fd) { luaL_pushfail(L); lua_pushstring(L, strerror(EEXIST)); lua_pushinteger(L, EEXIST); return 3; } } if(state->count >= MAX_WATCHED) { luaL_pushfail(L); lua_pushstring(L, strerror(EMFILE)); lua_pushinteger(L, EMFILE); return 3; } state->events[state->count].fd = fd; state->events[state->count].events = (wantread ? POLLIN : 0) | (wantwrite ? POLLOUT : 0); state->events[state->count].revents = 0; state->count++; lua_pushboolean(L, 1); return 1; #endif #ifdef USE_SELECT if(fd > FD_SETSIZE) { luaL_pushfail(L); lua_pushstring(L, strerror(EBADF)); lua_pushinteger(L, EBADF); return 3; } if(FD_ISSET(fd, &state->all)) { luaL_pushfail(L); lua_pushstring(L, strerror(EEXIST)); lua_pushinteger(L, EEXIST); return 3; } FD_CLR(fd, &state->readable); FD_CLR(fd, &state->writable); FD_CLR(fd, &state->err); FD_SET(fd, &state->all); if(wantread) { FD_SET(fd, &state->wantread); } else { FD_CLR(fd, &state->wantread); } if(wantwrite) { FD_SET(fd, &state->wantwrite); } else { FD_CLR(fd, &state->wantwrite); } lua_pushboolean(L, 1); return 1; #endif } /* * Set events to watch for, readable and/or writable */ static int Lset(lua_State *L) { struct Lpoll_state *state = luaL_checkudata(L, 1, STATE_MT); int fd = luaL_checkinteger(L, 2); #ifdef USE_EPOLL int wantread = lua_toboolean(L, 3); int wantwrite = lua_toboolean(L, 4); struct epoll_event event; event.data.fd = fd; event.events = (wantread ? EPOLLIN : 0) | (wantwrite ? EPOLLOUT : 0); event.events |= EPOLLERR | EPOLLHUP | EPOLLRDHUP; int ret = epoll_ctl(state->epoll_fd, EPOLL_CTL_MOD, fd, &event); if(ret == 0) { lua_pushboolean(L, 1); return 1; } else { ret = errno; luaL_pushfail(L); lua_pushstring(L, strerror(ret)); lua_pushinteger(L, ret); return 3; } #endif #ifdef USE_POLL int wantread = lua_toboolean(L, 3); int wantwrite = lua_toboolean(L, 4); for(nfds_t i = 0; i < state->count; i++) { struct pollfd *event = &state->events[i]; if(event->fd == fd) { event->events = (wantread ? POLLIN : 0) | (wantwrite ? POLLOUT : 0); lua_pushboolean(L, 1); return 1; } else if(event->fd == -1) { break; } } luaL_pushfail(L); lua_pushstring(L, strerror(ENOENT)); lua_pushinteger(L, ENOENT); return 3; #endif #ifdef USE_SELECT if(!FD_ISSET(fd, &state->all)) { luaL_pushfail(L); lua_pushstring(L, strerror(ENOENT)); lua_pushinteger(L, ENOENT); return 3; } if(!lua_isnoneornil(L, 3)) { if(lua_toboolean(L, 3)) { FD_SET(fd, &state->wantread); } else { FD_CLR(fd, &state->wantread); } } if(!lua_isnoneornil(L, 4)) { if(lua_toboolean(L, 4)) { FD_SET(fd, &state->wantwrite); } else { FD_CLR(fd, &state->wantwrite); } } lua_pushboolean(L, 1); return 1; #endif } /* * Remove FDs */ static int Ldel(lua_State *L) { struct Lpoll_state *state = luaL_checkudata(L, 1, STATE_MT); int fd = luaL_checkinteger(L, 2); #ifdef USE_EPOLL struct epoll_event event; event.data.fd = fd; int ret = epoll_ctl(state->epoll_fd, EPOLL_CTL_DEL, fd, &event); if(ret == 0) { lua_pushboolean(L, 1); return 1; } else { ret = errno; luaL_pushfail(L); lua_pushstring(L, strerror(ret)); lua_pushinteger(L, ret); return 3; } #endif #ifdef USE_POLL if(state->count == 0) { luaL_pushfail(L); lua_pushstring(L, strerror(ENOENT)); lua_pushinteger(L, ENOENT); return 3; } /* * Move the last item on top of the removed one */ struct pollfd *last = &state->events[state->count - 1]; for(nfds_t i = 0; i < state->count; i++) { struct pollfd *event = &state->events[i]; if(event->fd == fd) { event->fd = last->fd; event->events = last->events; event->revents = last->revents; last->fd = -1; state->count--; lua_pushboolean(L, 1); return 1; } } luaL_pushfail(L); lua_pushstring(L, strerror(ENOENT)); lua_pushinteger(L, ENOENT); return 3; #endif #ifdef USE_SELECT if(!FD_ISSET(fd, &state->all)) { luaL_pushfail(L); lua_pushstring(L, strerror(ENOENT)); lua_pushinteger(L, ENOENT); return 3; } FD_CLR(fd, &state->wantread); FD_CLR(fd, &state->wantwrite); FD_CLR(fd, &state->readable); FD_CLR(fd, &state->writable); FD_CLR(fd, &state->all); FD_CLR(fd, &state->err); lua_pushboolean(L, 1); return 1; #endif } /* * Check previously manipulated event state for FDs ready for reading or writing */ static int Lpushevent(lua_State *L, struct Lpoll_state *state) { #ifdef USE_EPOLL if(state->processed > 0) { state->processed--; struct epoll_event event = state->events[state->processed]; lua_pushinteger(L, event.data.fd); lua_pushboolean(L, event.events & (EPOLLIN | EPOLLHUP | EPOLLRDHUP | EPOLLERR)); lua_pushboolean(L, event.events & EPOLLOUT); return 3; } #endif #ifdef USE_POLL for(int i = state->processed - 1; i >= 0; i--) { struct pollfd *event = &state->events[i]; if(event->fd != -1 && event->revents != 0) { lua_pushinteger(L, event->fd); lua_pushboolean(L, event->revents & (POLLIN | POLLHUP | POLLERR)); lua_pushboolean(L, event->revents & POLLOUT); event->revents = 0; state->processed = i; return 3; } } #endif #ifdef USE_SELECT for(int fd = state->processed + 1; fd < FD_SETSIZE; fd++) { if(FD_ISSET(fd, &state->readable) || FD_ISSET(fd, &state->writable) || FD_ISSET(fd, &state->err)) { lua_pushinteger(L, fd); lua_pushboolean(L, FD_ISSET(fd, &state->readable) | FD_ISSET(fd, &state->err)); lua_pushboolean(L, FD_ISSET(fd, &state->writable)); FD_CLR(fd, &state->readable); FD_CLR(fd, &state->writable); FD_CLR(fd, &state->err); state->processed = fd; return 3; } } #endif return 0; } /* * Wait for event */ static int Lwait(lua_State *L) { struct Lpoll_state *state = luaL_checkudata(L, 1, STATE_MT); int ret = Lpushevent(L, state); if(ret != 0) { return ret; } 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 #ifdef USE_POLL ret = poll(state->events, state->count, timeout * 1000); #endif #ifdef USE_SELECT /* * select(2) mutates the fd_sets passed to it so in order to not * have to recreate it manually every time a copy is made. */ memcpy(&state->readable, &state->wantread, sizeof(fd_set)); memcpy(&state->writable, &state->wantwrite, sizeof(fd_set)); memcpy(&state->err, &state->all, sizeof(fd_set)); struct timeval tv; tv.tv_sec = (time_t)timeout; tv.tv_usec = ((suseconds_t)(timeout * 1000000)) % 1000000; ret = select(FD_SETSIZE, &state->readable, &state->writable, &state->err, &tv); #endif if(ret == 0) { /* Is this an error? */ lua_pushnil(L); lua_pushstring(L, "timeout"); return 2; } else if(ret < 0 && errno == EINTR) { /* Is this an error? */ lua_pushnil(L); lua_pushstring(L, "signal"); return 2; } else if(ret < 0) { ret = errno; luaL_pushfail(L); lua_pushstring(L, strerror(ret)); lua_pushinteger(L, ret); return 3; } /* * Search for the first ready FD and return it */ #ifdef USE_EPOLL state->processed = ret; #endif #ifdef USE_POLL state->processed = state->count; #endif #ifdef USE_SELECT state->processed = -1; #endif return Lpushevent(L, state); } #ifdef USE_EPOLL /* * Return Epoll FD */ static int Lgetfd(lua_State *L) { struct Lpoll_state *state = luaL_checkudata(L, 1, STATE_MT); lua_pushinteger(L, state->epoll_fd); return 1; } /* * Close epoll FD */ static int Lgc(lua_State *L) { struct Lpoll_state *state = luaL_checkudata(L, 1, STATE_MT); if(state->epoll_fd == -1) { return 0; } if(close(state->epoll_fd) == 0) { state->epoll_fd = -1; } else { lua_pushstring(L, strerror(errno)); lua_error(L); } return 0; } #endif /* * String representation */ static int Ltos(lua_State *L) { struct Lpoll_state *state = luaL_checkudata(L, 1, STATE_MT); lua_pushfstring(L, "%s: %p", STATE_MT, state); return 1; } /* * Create a new context */ static int Lnew(lua_State *L) { /* Allocate state */ Lpoll_state *state = lua_newuserdata(L, sizeof(Lpoll_state)); luaL_setmetatable(L, STATE_MT); /* Initialize state */ #ifdef USE_EPOLL state->epoll_fd = -1; state->processed = 0; int epoll_fd = epoll_create1(EPOLL_CLOEXEC); if(epoll_fd <= 0) { luaL_pushfail(L); lua_pushstring(L, strerror(errno)); lua_pushinteger(L, errno); return 3; } state->epoll_fd = epoll_fd; #endif #ifdef USE_POLL state->processed = -1; state->count = 0; for(nfds_t i = 0; i < MAX_WATCHED; i++) { state->events[i].fd = -1; state->events[i].events = 0; state->events[i].revents = 0; } #endif #ifdef USE_SELECT FD_ZERO(&state->wantread); FD_ZERO(&state->wantwrite); FD_ZERO(&state->readable); FD_ZERO(&state->writable); FD_ZERO(&state->all); FD_ZERO(&state->err); state->processed = FD_SETSIZE; #endif return 1; } /* * Open library */ int luaopen_prosody_util_poll(lua_State *L) { luaL_checkversion(L); luaL_newmetatable(L, STATE_MT); { lua_pushliteral(L, STATE_MT); lua_setfield(L, -2, "__name"); lua_pushcfunction(L, Ltos); lua_setfield(L, -2, "__tostring"); lua_createtable(L, 0, 2); { lua_pushcfunction(L, Ladd); lua_setfield(L, -2, "add"); lua_pushcfunction(L, Lset); lua_setfield(L, -2, "set"); lua_pushcfunction(L, Ldel); lua_setfield(L, -2, "del"); lua_pushcfunction(L, Lwait); lua_setfield(L, -2, "wait"); #ifdef USE_EPOLL lua_pushcfunction(L, Lgetfd); lua_setfield(L, -2, "getfd"); #endif } lua_setfield(L, -2, "__index"); #ifdef USE_EPOLL lua_pushcfunction(L, Lgc); lua_setfield(L, -2, "__gc"); #endif } lua_createtable(L, 0, 3); { lua_pushcfunction(L, Lnew); lua_setfield(L, -2, "new"); #define push_errno(named_error) lua_pushinteger(L, named_error);\ lua_setfield(L, -2, #named_error); push_errno(EEXIST); push_errno(EMFILE); push_errno(ENOENT); lua_pushliteral(L, POLL_BACKEND); lua_setfield(L, -2, "api"); } return 1; } /* COMPAT */ int luaopen_util_poll(lua_State *L) { return luaopen_prosody_util_poll(L); } prosody-13.0.1/util-src/PaxHeaders/pposix.c0000644000000000000000000000011614773555365015627 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.82371146 prosody-13.0.1/util-src/pposix.c0000644000175000017500000004606014773555365020035 0ustar00prosodyprosody00000000000000/* Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2009 Tobias Markmann -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- */ /* * pposix.c * POSIX support functions for Lua */ #define MODULE_VERSION "0.4.1" #if defined(__linux__) #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #else #ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #endif #if defined(__APPLE__) #ifndef _DARWIN_C_SOURCE #define _DARWIN_C_SOURCE #endif #endif #if ! defined(__FreeBSD__) #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L #endif #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "lua.h" #include "lualib.h" #include "lauxlib.h" #if (LUA_VERSION_NUM < 503) #define lua_isinteger(L, n) lua_isnumber(L, n) #endif #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif #include #if defined(__linux__) #include #endif #if !defined(WITHOUT_MALLINFO) && defined(__linux__) && defined(__GLIBC__) #include #define WITH_MALLINFO #endif #if defined(__FreeBSD__) && defined(RFPROC) /* * On FreeBSD, calling fork() is equivalent to rfork(RFPROC | RFFDG). * * RFFDG being set means that the file descriptor table is copied, * otherwise it's shared. We want the later, otherwise libevent gets * messed up. * * See issue #412 */ #define fork() rfork(RFPROC) #endif /* Daemonization support */ static int lc_daemonize(lua_State *L) { pid_t pid; if(getppid() == 1) { lua_pushboolean(L, 0); lua_pushstring(L, "already-daemonized"); return 2; } /* Attempt initial fork */ if((pid = fork()) < 0) { /* Forking failed */ lua_pushboolean(L, 0); lua_pushstring(L, "fork-failed"); return 2; } else if(pid != 0) { /* We are the parent process */ lua_pushboolean(L, 1); lua_pushinteger(L, pid); return 2; } /* and we are the child process */ if(setsid() == -1) { /* We failed to become session leader */ /* (we probably already were) */ lua_pushboolean(L, 0); lua_pushstring(L, "setsid-failed"); return 2; } /* Make sure accidental use of FDs 0, 1, 2 don't cause weirdness */ freopen("/dev/null", "r", stdin); freopen("/dev/null", "w", stdout); freopen("/dev/null", "w", stderr); /* Final fork, use it wisely */ if(fork()) { exit(0); } /* Show's over, let's continue */ lua_pushboolean(L, 1); lua_pushnil(L); return 2; } /* Syslog support */ static const char *const facility_strings[] = { "auth", #if !(defined(sun) || defined(__sun)) "authpriv", #endif "cron", "daemon", #if !(defined(sun) || defined(__sun)) "ftp", #endif "kern", "local0", "local1", "local2", "local3", "local4", "local5", "local6", "local7", "lpr", "mail", "syslog", "user", "uucp", NULL }; static int facility_constants[] = { LOG_AUTH, #if !(defined(sun) || defined(__sun)) LOG_AUTHPRIV, #endif LOG_CRON, LOG_DAEMON, #if !(defined(sun) || defined(__sun)) LOG_FTP, #endif LOG_KERN, LOG_LOCAL0, LOG_LOCAL1, LOG_LOCAL2, LOG_LOCAL3, LOG_LOCAL4, LOG_LOCAL5, LOG_LOCAL6, LOG_LOCAL7, LOG_LPR, LOG_MAIL, LOG_NEWS, LOG_SYSLOG, LOG_USER, LOG_UUCP, -1 }; /* " The parameter ident in the call of openlog() is probably stored as-is. Thus, if the string it points to is changed, syslog() may start prepending the changed string, and if the string it points to ceases to exist, the results are undefined. Most portable is to use a string constant. " -- syslog manpage */ static char *syslog_ident = NULL; static int lc_syslog_open(lua_State *L) { int facility = luaL_checkoption(L, 2, "daemon", facility_strings); facility = facility_constants[facility]; luaL_checkstring(L, 1); if(syslog_ident) { free(syslog_ident); } syslog_ident = strdup(lua_tostring(L, 1)); openlog(syslog_ident, LOG_PID, facility); return 0; } static const char *const level_strings[] = { "debug", "info", "notice", "warn", "error", NULL }; static int level_constants[] = { LOG_DEBUG, LOG_INFO, LOG_NOTICE, LOG_WARNING, LOG_CRIT, -1 }; static int lc_syslog_log(lua_State *L) { int level = level_constants[luaL_checkoption(L, 1, "notice", level_strings)]; if(lua_gettop(L) == 3) { syslog(level, "%s: %s", luaL_checkstring(L, 2), luaL_checkstring(L, 3)); } else { syslog(level, "%s", lua_tostring(L, 2)); } return 0; } static int lc_syslog_close(lua_State *L) { (void)L; closelog(); if(syslog_ident) { free(syslog_ident); syslog_ident = NULL; } return 0; } static int lc_syslog_setmask(lua_State *L) { int level_idx = luaL_checkoption(L, 1, "notice", level_strings); int mask = 0; do { mask |= LOG_MASK(level_constants[level_idx]); } while(++level_idx <= 4); setlogmask(mask); return 0; } /* getpid */ static int lc_getpid(lua_State *L) { lua_pushinteger(L, getpid()); return 1; } /* UID/GID functions */ static int lc_getuid(lua_State *L) { lua_pushinteger(L, getuid()); return 1; } static int lc_getgid(lua_State *L) { lua_pushinteger(L, getgid()); return 1; } static int lc_setuid(lua_State *L) { int uid = -1; if(lua_gettop(L) < 1) { return 0; } if(!lua_isinteger(L, 1) && lua_tostring(L, 1)) { /* Passed UID is actually a string, so look up the UID */ struct passwd *p; p = getpwnam(lua_tostring(L, 1)); if(!p) { lua_pushboolean(L, 0); lua_pushstring(L, "no-such-user"); return 2; } uid = p->pw_uid; } else { uid = lua_tointeger(L, 1); } if(uid > -1) { /* Ok, attempt setuid */ errno = 0; if(setuid(uid)) { /* Fail */ lua_pushboolean(L, 0); switch(errno) { case EINVAL: lua_pushstring(L, "invalid-uid"); break; case EPERM: lua_pushstring(L, "permission-denied"); break; default: lua_pushstring(L, "unknown-error"); } return 2; } else { /* Success! */ lua_pushboolean(L, 1); return 1; } } /* Seems we couldn't find a valid UID to switch to */ lua_pushboolean(L, 0); lua_pushstring(L, "invalid-uid"); return 2; } static int lc_setgid(lua_State *L) { int gid = -1; if(lua_gettop(L) < 1) { return 0; } if(!lua_isinteger(L, 1) && lua_tostring(L, 1)) { /* Passed GID is actually a string, so look up the GID */ struct group *g; g = getgrnam(lua_tostring(L, 1)); if(!g) { lua_pushboolean(L, 0); lua_pushstring(L, "no-such-group"); return 2; } gid = g->gr_gid; } else { gid = lua_tointeger(L, 1); } if(gid > -1) { /* Ok, attempt setgid */ errno = 0; if(setgid(gid)) { /* Fail */ lua_pushboolean(L, 0); switch(errno) { case EINVAL: lua_pushstring(L, "invalid-gid"); break; case EPERM: lua_pushstring(L, "permission-denied"); break; default: lua_pushstring(L, "unknown-error"); } return 2; } else { /* Success! */ lua_pushboolean(L, 1); return 1; } } /* Seems we couldn't find a valid GID to switch to */ lua_pushboolean(L, 0); lua_pushstring(L, "invalid-gid"); return 2; } static int lc_initgroups(lua_State *L) { int ret; gid_t gid; struct passwd *p; if(!lua_isstring(L, 1)) { luaL_pushfail(L); lua_pushstring(L, "invalid-username"); return 2; } p = getpwnam(lua_tostring(L, 1)); if(!p) { luaL_pushfail(L); lua_pushstring(L, "no-such-user"); return 2; } if(lua_gettop(L) < 2) { lua_pushnil(L); } switch(lua_type(L, 2)) { case LUA_TNIL: gid = p->pw_gid; break; case LUA_TNUMBER: gid = lua_tointeger(L, 2); break; default: luaL_pushfail(L); lua_pushstring(L, "invalid-gid"); return 2; } ret = initgroups(lua_tostring(L, 1), gid); if(ret) { switch(errno) { case ENOMEM: luaL_pushfail(L); lua_pushstring(L, "no-memory"); break; case EPERM: luaL_pushfail(L); lua_pushstring(L, "permission-denied"); break; default: luaL_pushfail(L); lua_pushstring(L, "unknown-error"); } } else { lua_pushboolean(L, 1); lua_pushnil(L); } return 2; } static int lc_umask(lua_State *L) { char old_mode_string[7]; mode_t old_mode = umask(strtoul(luaL_checkstring(L, 1), NULL, 8)); snprintf(old_mode_string, sizeof(old_mode_string), "%03o", old_mode); old_mode_string[sizeof(old_mode_string) - 1] = 0; lua_pushstring(L, old_mode_string); return 1; } static int lc_mkdir(lua_State *L) { int ret = mkdir(luaL_checkstring(L, 1), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH); /* mode 775 */ lua_pushboolean(L, ret == 0); if(ret) { lua_pushstring(L, strerror(errno)); return 2; } return 1; } /* Like POSIX's setrlimit()/getrlimit() API functions. * * Syntax: * pposix.setrlimit( resource, soft limit, hard limit) * * Any negative limit will be replace with the current limit by an additional call of getrlimit(). * * Example usage: * pposix.setrlimit("NOFILE", 1000, 2000) */ static const char *const resource_strings[] = { /* Defined by POSIX */ "CORE", "CPU", "DATA", "FSIZE", "NOFILE", "STACK", #if !(defined(sun) || defined(__sun) || defined(__APPLE__)) "MEMLOCK", "NPROC", "RSS", #endif #ifdef RLIMIT_NICE "NICE", #endif NULL }; 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 #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)) { case LUA_TSTRING: if(strcmp(lua_tostring(L, idx), "unlimited") == 0) { return RLIM_INFINITY; } return luaL_argerror(L, idx, "unexpected type"); case LUA_TNUMBER: return lua_tointeger(L, idx); case LUA_TNONE: case LUA_TNIL: return current; default: return luaL_argerror(L, idx, "unexpected type"); } } static int lc_setrlimit(lua_State *L) { struct rlimit lim; int arguments = lua_gettop(L); int rid = -1; if(arguments < 1 || arguments > 3) { lua_pushboolean(L, 0); lua_pushstring(L, "incorrect-arguments"); return 2; } rid = resource_constants[luaL_checkoption(L, 1,NULL, resource_strings)]; if(rid == -1) { lua_pushboolean(L, 0); lua_pushstring(L, "invalid-resource"); return 2; } /* Fetch current values to use as defaults */ if(getrlimit(rid, &lim)) { lua_pushboolean(L, 0); lua_pushstring(L, "getrlimit-failed"); return 2; } lim.rlim_cur = arg_to_rlimit(L, 2, lim.rlim_cur); lim.rlim_max = arg_to_rlimit(L, 3, lim.rlim_max); if(setrlimit(rid, &lim)) { lua_pushboolean(L, 0); lua_pushstring(L, "setrlimit-failed"); return 2; } lua_pushboolean(L, 1); return 1; } static int lc_getrlimit(lua_State *L) { int arguments = lua_gettop(L); int rid = -1; struct rlimit lim; if(arguments != 1) { lua_pushboolean(L, 0); lua_pushstring(L, "invalid-arguments"); return 2; } rid = resource_constants[luaL_checkoption(L, 1, NULL, resource_strings)]; if(rid != -1) { if(getrlimit(rid, &lim)) { lua_pushboolean(L, 0); lua_pushstring(L, "getrlimit-failed."); return 2; } } else { /* Unsupported resource. Sorry I'm pretty limited by POSIX standard. */ lua_pushboolean(L, 0); lua_pushstring(L, "invalid-resource"); return 2; } lua_pushboolean(L, 1); if(lim.rlim_cur == RLIM_INFINITY) { lua_pushstring(L, "unlimited"); } else { lua_pushinteger(L, lim.rlim_cur); } if(lim.rlim_max == RLIM_INFINITY) { lua_pushstring(L, "unlimited"); } else { lua_pushinteger(L, lim.rlim_max); } return 3; } static int lc_abort(lua_State *L) { (void)L; abort(); 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; if(uname(&uname_info) != 0) { luaL_pushfail(L); lua_pushstring(L, strerror(errno)); return 2; } lua_createtable(L, 0, 6); lua_pushstring(L, uname_info.sysname); lua_setfield(L, -2, "sysname"); lua_pushstring(L, uname_info.nodename); lua_setfield(L, -2, "nodename"); lua_pushstring(L, uname_info.release); lua_setfield(L, -2, "release"); lua_pushstring(L, uname_info.version); lua_setfield(L, -2, "version"); lua_pushstring(L, uname_info.machine); lua_setfield(L, -2, "machine"); #ifdef __USE_GNU lua_pushstring(L, uname_info.domainname); lua_setfield(L, -2, "domainname"); #endif return 1; } static int lc_setenv(lua_State *L) { const char *var = luaL_checkstring(L, 1); const char *value; /* If the second argument is nil or nothing, unset the var */ if(lua_isnoneornil(L, 2)) { if(unsetenv(var) != 0) { luaL_pushfail(L); lua_pushstring(L, strerror(errno)); return 2; } lua_pushboolean(L, 1); return 1; } value = luaL_checkstring(L, 2); if(setenv(var, value, 1) != 0) { luaL_pushfail(L); lua_pushstring(L, strerror(errno)); return 2; } lua_pushboolean(L, 1); return 1; } #ifdef WITH_MALLINFO static int lc_meminfo(lua_State *L) { #if __GLIBC_PREREQ(2, 33) struct mallinfo2 info = mallinfo2(); #define MALLINFO_T size_t #else struct mallinfo info = mallinfo(); #define MALLINFO_T unsigned #endif lua_createtable(L, 0, 5); /* This is the total size of memory allocated with sbrk by malloc, in bytes. */ lua_pushinteger(L, (MALLINFO_T)info.arena); lua_setfield(L, -2, "allocated"); /* This is the total size of memory allocated with mmap, in bytes. */ lua_pushinteger(L, (MALLINFO_T)info.hblkhd); lua_setfield(L, -2, "allocated_mmap"); /* This is the total size of memory occupied by chunks handed out by malloc. */ lua_pushinteger(L, (MALLINFO_T)info.uordblks); lua_setfield(L, -2, "used"); /* This is the total size of memory occupied by free (not in use) chunks. */ lua_pushinteger(L, (MALLINFO_T)info.fordblks); lua_setfield(L, -2, "unused"); /* This is the size of the top-most releasable chunk that normally borders the end of the heap (i.e., the high end of the virtual address space's data segment). */ lua_pushinteger(L, (MALLINFO_T)info.keepcost); lua_setfield(L, -2, "returnable"); return 1; } #undef MALLINFO_T #endif /* * Append some data to a file handle * Attempt to allocate space first * Truncate to original size on failure */ static int lc_atomic_append(lua_State *L) { int err; size_t len; FILE *f = *(FILE **) luaL_checkudata(L, 1, LUA_FILEHANDLE); const char *data = luaL_checklstring(L, 2, &len); off_t offset = ftell(f); #if defined(__linux__) /* Try to allocate space without changing the file size. */ if((err = fallocate(fileno(f), FALLOC_FL_KEEP_SIZE, offset, len))) { if(errno != 0) { /* Some old versions of Linux apparently use the return value instead of errno */ err = errno; } switch(err) { case ENOSYS: /* Kernel doesn't implement fallocate */ case EOPNOTSUPP: /* Filesystem doesn't support it */ /* Ignore and proceed to try to write */ break; case ENOSPC: /* No space left */ default: /* Other issues */ luaL_pushfail(L); lua_pushstring(L, strerror(err)); lua_pushinteger(L, err); return 3; } } #endif if(fwrite(data, sizeof(char), len, f) == len) { if(fflush(f) == 0) { lua_pushboolean(L, 1); /* Great success! */ return 1; } else { err = errno; } } else { err = ferror(f); } fseek(f, offset, SEEK_SET); /* Cut partially written data */ if(ftruncate(fileno(f), offset)) { /* The file is now most likely corrupted, throw hard error */ return luaL_error(L, "atomic_append() failed in ftruncate(): %s", strerror(errno)); } luaL_pushfail(L); lua_pushstring(L, strerror(err)); lua_pushinteger(L, err); 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); lua_pushboolean(L, isatty(fd)); return 1; } /* Register functions */ int luaopen_prosody_util_pposix(lua_State *L) { luaL_checkversion(L); luaL_Reg exports[] = { { "abort", lc_abort }, { "daemonize", lc_daemonize }, { "syslog_open", lc_syslog_open }, { "syslog_close", lc_syslog_close }, { "syslog_log", lc_syslog_log }, { "syslog_setminlevel", lc_syslog_setmask }, { "getpid", lc_getpid }, { "getuid", lc_getuid }, { "getgid", lc_getgid }, { "setuid", lc_setuid }, { "setgid", lc_setgid }, { "initgroups", lc_initgroups }, { "umask", lc_umask }, { "mkdir", lc_mkdir }, { "pipe", lc_pipe }, { "fdopen", lc_fdopen }, { "setrlimit", lc_setrlimit }, { "getrlimit", lc_getrlimit }, { "uname", lc_uname }, { "setenv", lc_setenv }, #ifdef WITH_MALLINFO { "meminfo", lc_meminfo }, #endif { "atomic_append", lc_atomic_append }, { "remove_blocks", lc_remove_blocks }, { "isatty", lc_isatty }, { NULL, NULL } }; lua_newtable(L); luaL_setfuncs(L, exports, 0); #ifdef ENOENT lua_pushinteger(L, ENOENT); lua_setfield(L, -2, "ENOENT"); #endif lua_pushliteral(L, "pposix"); lua_setfield(L, -2, "_NAME"); lua_pushliteral(L, MODULE_VERSION); lua_setfield(L, -2, "_VERSION"); return 1; } int luaopen_util_pposix(lua_State *L) { return luaopen_prosody_util_pposix(L); } prosody-13.0.1/util-src/PaxHeaders/ringbuffer.c0000644000000000000000000000011614773555365016436 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.82371146 prosody-13.0.1/util-src/ringbuffer.c0000644000175000017500000001673514773555365020652 0ustar00prosodyprosody00000000000000 #include #include #include #include #include #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif typedef struct { size_t rpos; /* read position */ size_t wpos; /* write position */ size_t alen; /* allocated size */ size_t blen; /* current content size */ char buffer[]; } ringbuffer; /* Translate absolute idx to a wrapped index within the buffer, based on current read position */ static int wrap_pos(const ringbuffer *b, const long idx, long *pos) { if(idx > (long)b->blen) { return 0; } if(idx + (long)b->rpos > (long)b->alen) { *pos = idx - (b->alen - b->rpos); } else { *pos = b->rpos + idx; } return 1; } static int calc_splice_positions(const ringbuffer *b, long start, long end, long *out_start, long *out_end) { if(start < 0) { start = 1 + start + b->blen; } if(start <= 0) { start = 1; } if(end < 0) { end = 1 + end + b->blen; } if(end > (long)b->blen) { end = b->blen; } if(start < 1) { start = 1; } if(start > end) { return 0; } start = start - 1; if(!wrap_pos(b, start, out_start)) { return 0; } if(!wrap_pos(b, end, out_end)) { return 0; } return 1; } static void writechar(ringbuffer *b, char c) { b->blen++; b->buffer[(b->wpos++) % b->alen] = c; } /* make sure position counters stay within the allocation */ static void modpos(ringbuffer *b) { b->rpos = b->rpos % b->alen; b->wpos = b->wpos % b->alen; } static int find(ringbuffer *b, const char *s, size_t l) { size_t i, j; int m; if(b->rpos == b->wpos) { /* empty */ return 0; } /* look for a matching first byte */ for(i = 0; i <= b->blen - l; i++) { if(b->buffer[(b->rpos + i) % b->alen] == *s) { m = 1; /* check if the following byte also match */ for(j = 1; j < l; j++) if(b->buffer[(b->rpos + i + j) % b->alen] != s[j]) { m = 0; break; } if(m) { return i + l; } } } return 0; } /* * Find first position of a substring in buffer * (buffer, string) -> number */ static int rb_find(lua_State *L) { size_t l, m; ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt"); const char *s = luaL_checklstring(L, 2, &l); m = find(b, s, l); if(m > 0) { lua_pushinteger(L, m); return 1; } return 0; } /* * Move read position forward without returning the data * (buffer, number) -> boolean */ static int rb_discard(lua_State *L) { ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt"); size_t r = luaL_checkinteger(L, 2); if(r > b->blen) { lua_pushboolean(L, 0); return 1; } b->blen -= r; b->rpos += r; modpos(b); lua_pushboolean(L, 1); return 1; } /* * Read bytes from buffer * (buffer, number, boolean?) -> string */ static int rb_read(lua_State *L) { ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt"); size_t r = luaL_checkinteger(L, 2); int peek = lua_toboolean(L, 3); if(r > b->blen) { luaL_pushfail(L); return 1; } if((b->rpos + r) > b->alen) { /* Substring wraps around to the beginning of the buffer */ lua_pushlstring(L, &b->buffer[b->rpos], b->alen - b->rpos); lua_pushlstring(L, b->buffer, r - (b->alen - b->rpos)); lua_concat(L, 2); } else { lua_pushlstring(L, &b->buffer[b->rpos], r); } if(!peek) { b->blen -= r; b->rpos += r; modpos(b); } return 1; } /* * Read buffer until first occurrence of a substring * (buffer, string) -> string */ static int rb_readuntil(lua_State *L) { size_t l, m; ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt"); const char *s = luaL_checklstring(L, 2, &l); m = find(b, s, l); if(m > 0) { lua_settop(L, 1); lua_pushinteger(L, m); return rb_read(L); } return 0; } /* * Write bytes into the buffer * (buffer, string) -> integer */ static int rb_write(lua_State *L) { size_t l, w = 0; ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt"); const char *s = luaL_checklstring(L, 2, &l); /* Does `l` bytes fit? */ if((l + b->blen) > b->alen) { luaL_pushfail(L); return 1; } while(l-- > 0) { writechar(b, *s++); w++; } modpos(b); lua_pushinteger(L, w); return 1; } static int rb_tostring(lua_State *L) { ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt"); lua_pushfstring(L, "ringbuffer: %p %d/%d", b, b->blen, b->alen); return 1; } static int rb_sub(lua_State *L) { ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt"); long start = luaL_checkinteger(L, 2); long end = luaL_optinteger(L, 3, -1); long wrapped_start, wrapped_end; if(!calc_splice_positions(b, start, end, &wrapped_start, &wrapped_end)) { lua_pushstring(L, ""); } else if(wrapped_end <= wrapped_start) { lua_pushlstring(L, &b->buffer[wrapped_start], b->alen - wrapped_start); lua_pushlstring(L, b->buffer, wrapped_end); lua_concat(L, 2); } else { lua_pushlstring(L, &b->buffer[wrapped_start], (wrapped_end - wrapped_start)); } return 1; } static int rb_byte(lua_State *L) { ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt"); long start = luaL_optinteger(L, 2, 1); long end = luaL_optinteger(L, 3, start); long i; long wrapped_start, wrapped_end; if(calc_splice_positions(b, start, end, &wrapped_start, &wrapped_end)) { if(wrapped_end <= wrapped_start) { for(i = wrapped_start; i < (long)b->alen; i++) { lua_pushinteger(L, (unsigned char)b->buffer[i]); } for(i = 0; i < wrapped_end; i++) { lua_pushinteger(L, (unsigned char)b->buffer[i]); } return wrapped_end + (b->alen - wrapped_start); } else { for(i = wrapped_start; i < wrapped_end; i++) { lua_pushinteger(L, (unsigned char)b->buffer[i]); } return wrapped_end - wrapped_start; } } return 0; } static int rb_length(lua_State *L) { ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt"); lua_pushinteger(L, b->blen); return 1; } static int rb_size(lua_State *L) { ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt"); lua_pushinteger(L, b->alen); return 1; } static int rb_free(lua_State *L) { ringbuffer *b = luaL_checkudata(L, 1, "ringbuffer_mt"); lua_pushinteger(L, b->alen - b->blen); return 1; } static int rb_new(lua_State *L) { lua_Integer size = luaL_optinteger(L, 1, sysconf(_SC_PAGESIZE)); luaL_argcheck(L, size > 0, 1, "positive integer expected"); ringbuffer *b = lua_newuserdata(L, sizeof(ringbuffer) + size); b->rpos = 0; b->wpos = 0; b->alen = size; b->blen = 0; luaL_getmetatable(L, "ringbuffer_mt"); lua_setmetatable(L, -2); return 1; } int luaopen_prosody_util_ringbuffer(lua_State *L) { luaL_checkversion(L); if(luaL_newmetatable(L, "ringbuffer_mt")) { lua_pushcfunction(L, rb_tostring); lua_setfield(L, -2, "__tostring"); lua_pushcfunction(L, rb_length); lua_setfield(L, -2, "__len"); lua_createtable(L, 0, 7); /* __index */ { lua_pushcfunction(L, rb_find); lua_setfield(L, -2, "find"); lua_pushcfunction(L, rb_discard); lua_setfield(L, -2, "discard"); lua_pushcfunction(L, rb_read); lua_setfield(L, -2, "read"); lua_pushcfunction(L, rb_readuntil); lua_setfield(L, -2, "readuntil"); lua_pushcfunction(L, rb_write); lua_setfield(L, -2, "write"); lua_pushcfunction(L, rb_size); lua_setfield(L, -2, "size"); lua_pushcfunction(L, rb_length); lua_setfield(L, -2, "length"); lua_pushcfunction(L, rb_sub); lua_setfield(L, -2, "sub"); lua_pushcfunction(L, rb_byte); lua_setfield(L, -2, "byte"); lua_pushcfunction(L, rb_free); lua_setfield(L, -2, "free"); } lua_setfield(L, -2, "__index"); } lua_createtable(L, 0, 1); lua_pushcfunction(L, rb_new); lua_setfield(L, -2, "new"); return 1; } int luaopen_util_ringbuffer(lua_State *L) { return luaopen_prosody_util_ringbuffer(L); } prosody-13.0.1/util-src/PaxHeaders/signal.c0000644000000000000000000000011614773555365015562 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 29 ctime=1743706869.82371146 prosody-13.0.1/util-src/signal.c0000644000175000017500000002710114773555365017763 0ustar00prosodyprosody00000000000000/* * signal.c -- Signal Handler Library for Lua * * Version: 1.000+changes * * Copyright (C) 2007 Patrick J. Donnelly (batrick@batbytes.com) * * This software is distributed under the same license as Lua 5.0: * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #ifdef __linux__ #define HAVE_SIGNALFD #endif #include #include #include #ifdef HAVE_SIGNALFD #include #endif #include "lua.h" #include "lauxlib.h" #if (LUA_VERSION_NUM < 503) #define lua_isinteger(L, n) lua_isnumber(L, n) #endif #ifndef lsig #define lsig struct lua_signal { char *name; /* name of the signal */ int sig; /* the signal */ }; #endif #define MAX_PENDING_SIGNALS 32 #define LUA_SIGNAL "lua_signal" static const struct lua_signal lua_signals[] = { /* ANSI C signals */ #ifdef SIGABRT {"SIGABRT", SIGABRT}, #endif #ifdef SIGFPE {"SIGFPE", SIGFPE}, #endif #ifdef SIGILL {"SIGILL", SIGILL}, #endif #ifdef SIGINT {"SIGINT", SIGINT}, #endif #ifdef SIGSEGV {"SIGSEGV", SIGSEGV}, #endif #ifdef SIGTERM {"SIGTERM", SIGTERM}, #endif /* posix signals */ #ifdef SIGHUP {"SIGHUP", SIGHUP}, #endif #ifdef SIGQUIT {"SIGQUIT", SIGQUIT}, #endif #ifdef SIGTRAP {"SIGTRAP", SIGTRAP}, #endif #ifdef SIGKILL {"SIGKILL", SIGKILL}, #endif #ifdef SIGUSR1 {"SIGUSR1", SIGUSR1}, #endif #ifdef SIGUSR2 {"SIGUSR2", SIGUSR2}, #endif #ifdef SIGPIPE {"SIGPIPE", SIGPIPE}, #endif #ifdef SIGALRM {"SIGALRM", SIGALRM}, #endif #ifdef SIGCHLD {"SIGCHLD", SIGCHLD}, #endif #ifdef SIGCONT {"SIGCONT", SIGCONT}, #endif #ifdef SIGSTOP {"SIGSTOP", SIGSTOP}, #endif #ifdef SIGTTIN {"SIGTTIN", SIGTTIN}, #endif #ifdef SIGTTOU {"SIGTTOU", SIGTTOU}, #endif /* some BSD signals */ #ifdef SIGIOT {"SIGIOT", SIGIOT}, #endif #ifdef SIGBUS {"SIGBUS", SIGBUS}, #endif #ifdef SIGCLD {"SIGCLD", SIGCLD}, #endif #ifdef SIGURG {"SIGURG", SIGURG}, #endif #ifdef SIGXCPU {"SIGXCPU", SIGXCPU}, #endif #ifdef SIGXFSZ {"SIGXFSZ", SIGXFSZ}, #endif #ifdef SIGVTALRM {"SIGVTALRM", SIGVTALRM}, #endif #ifdef SIGPROF {"SIGPROF", SIGPROF}, #endif #ifdef SIGWINCH {"SIGWINCH", SIGWINCH}, #endif #ifdef SIGPOLL {"SIGPOLL", SIGPOLL}, #endif #ifdef SIGIO {"SIGIO", SIGIO}, #endif /* add odd signals */ #ifdef SIGSTKFLT {"SIGSTKFLT", SIGSTKFLT}, /* stack fault */ #endif #ifdef SIGSYS {"SIGSYS", SIGSYS}, #endif {NULL, 0} }; static lua_State *Lsig = NULL; static lua_Hook Hsig = NULL; static int Hmask = 0; static int Hcount = 0; static int signals[MAX_PENDING_SIGNALS]; static int nsig = 0; static void sighook(lua_State *L, lua_Debug *ar) { (void)ar; /* restore the old hook */ lua_sethook(L, Hsig, Hmask, Hcount); lua_pushstring(L, LUA_SIGNAL); lua_gettable(L, LUA_REGISTRYINDEX); for(int i = 0; i < nsig; i++) { lua_pushinteger(L, signals[i]); lua_gettable(L, -2); lua_call(L, 0, 0); }; nsig = 0; lua_pop(L, 1); /* pop lua_signal table */ } static void handle(int sig) { if(nsig == 0) { /* Store the existing debug hook (if any) and its parameters */ Hsig = lua_gethook(Lsig); Hmask = lua_gethookmask(Lsig); Hcount = lua_gethookcount(Lsig); /* Set our new debug hook */ lua_sethook(Lsig, sighook, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1); } if(nsig < MAX_PENDING_SIGNALS) { signals[nsig++] = sig; } } /* * l_signal == signal(signal [, func [, chook]]) * * signal = signal number or string * func = Lua function to call * chook = catch within C functions * if caught, Lua function _must_ * exit, as the stack is most likely * in an unstable state. */ static int l_signal(lua_State *L) { int args = lua_gettop(L); int t, sig; /* type, signal */ /* get type of signal */ luaL_checkany(L, 1); t = lua_type(L, 1); if(t == LUA_TNUMBER) { sig = (int) lua_tointeger(L, 1); } else if(t == LUA_TSTRING) { lua_pushstring(L, LUA_SIGNAL); lua_gettable(L, LUA_REGISTRYINDEX); lua_pushvalue(L, 1); lua_gettable(L, -2); if(!lua_isinteger(L, -1)) { return luaL_error(L, "invalid signal string"); } sig = (int) lua_tointeger(L, -1); lua_pop(L, 1); /* get rid of number we pushed */ } else { luaL_checknumber(L, 1); /* will always error, with good error msg */ return luaL_error(L, "unreachable: invalid number was accepted"); } /* set handler */ if(args == 1 || lua_isnil(L, 2)) { /* clear handler */ lua_pushstring(L, LUA_SIGNAL); lua_gettable(L, LUA_REGISTRYINDEX); lua_pushinteger(L, sig); lua_gettable(L, -2); /* return old handler */ lua_pushinteger(L, sig); lua_pushnil(L); lua_settable(L, -4); lua_remove(L, -2); /* remove LUA_SIGNAL table */ signal(sig, SIG_DFL); } else { luaL_checktype(L, 2, LUA_TFUNCTION); lua_pushstring(L, LUA_SIGNAL); lua_gettable(L, LUA_REGISTRYINDEX); lua_pushinteger(L, sig); lua_pushvalue(L, 2); lua_settable(L, -3); /* Set the state for the handler */ Lsig = L; if(lua_toboolean(L, 3)) { /* c hook? */ if(signal(sig, handle) == SIG_ERR) { lua_pushboolean(L, 0); } else { lua_pushboolean(L, 1); } } else { /* lua_hook */ if(signal(sig, handle) == SIG_ERR) { lua_pushboolean(L, 0); } else { lua_pushboolean(L, 1); } } } return 1; } /* * l_raise == raise(signal) * * signal = signal number or string */ static int l_raise(lua_State *L) { /* int args = lua_gettop(L); */ int t = 0; /* type */ lua_Integer ret; luaL_checkany(L, 1); t = lua_type(L, 1); if(t == LUA_TNUMBER) { ret = (lua_Integer) raise((int) lua_tointeger(L, 1)); lua_pushinteger(L, ret); } else if(t == LUA_TSTRING) { lua_pushstring(L, LUA_SIGNAL); lua_gettable(L, LUA_REGISTRYINDEX); lua_pushvalue(L, 1); lua_gettable(L, -2); if(!lua_isnumber(L, -1)) { return luaL_error(L, "invalid signal string"); } ret = (lua_Integer) raise((int) lua_tointeger(L, -1)); lua_pop(L, 1); /* get rid of number we pushed */ lua_pushinteger(L, ret); } else { luaL_checknumber(L, 1); /* will always error, with good error msg */ } return 1; } #if defined(__unix__) || defined(__APPLE__) /* define some posix only functions */ /* * l_kill == kill(pid, signal) * * pid = process id * signal = signal number or string */ static int l_kill(lua_State *L) { int t; /* type */ lua_Integer ret; /* return value */ luaL_checknumber(L, 1); /* must be int for pid */ luaL_checkany(L, 2); /* check for a second arg */ t = lua_type(L, 2); if(t == LUA_TNUMBER) { ret = (lua_Integer) kill((int) lua_tointeger(L, 1), (int) lua_tointeger(L, 2)); lua_pushinteger(L, ret); } else if(t == LUA_TSTRING) { lua_pushstring(L, LUA_SIGNAL); lua_gettable(L, LUA_REGISTRYINDEX); lua_pushvalue(L, 2); lua_gettable(L, -2); if(!lua_isnumber(L, -1)) { return luaL_error(L, "invalid signal string"); } ret = (lua_Integer) kill((int) lua_tointeger(L, 1), (int) lua_tointeger(L, -1)); lua_pop(L, 1); /* get rid of number we pushed */ lua_pushinteger(L, ret); } else { luaL_checknumber(L, 2); /* will always error, with good error msg */ } return 1; } #endif struct lsignalfd { int fd; sigset_t mask; #ifndef HAVE_SIGNALFD int write_fd; #endif }; #ifndef HAVE_SIGNALFD #define MAX_SIGNALFD 32 struct lsignalfd signalfds[MAX_SIGNALFD]; static int signalfd_num = 0; static void signal2fd(int sig) { for(int i = 0; i < signalfd_num; i++) { if(sigismember(&signalfds[i].mask, sig)) { write(signalfds[i].write_fd, &sig, sizeof(sig)); } } } #endif static int l_signalfd(lua_State *L) { struct lsignalfd *sfd = lua_newuserdata(L, sizeof(struct lsignalfd)); int sig = luaL_checkinteger(L, 1); sigemptyset(&sfd->mask); sigaddset(&sfd->mask, sig); #ifdef HAVE_SIGNALFD 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; } #else if(signalfd_num >= MAX_SIGNALFD) { lua_pushnil(L); return 1; } if(signal(sig, signal2fd) == SIG_ERR) { lua_pushnil(L); return 1; } int pipefd[2]; if(pipe(pipefd) == -1) { lua_pushnil(L); return 1; } sfd->fd = pipefd[0]; sfd->write_fd = pipefd[1]; signalfds[signalfd_num++] = *sfd; #endif 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"); #ifdef HAVE_SIGNALFD struct signalfd_siginfo siginfo; if(read(sfd->fd, &siginfo, sizeof(siginfo)) < 0) { return 0; } lua_pushinteger(L, siginfo.ssi_signo); return 1; #else int signo; if(read(sfd->fd, &signo, sizeof(int)) < 0) { return 0; } lua_pushinteger(L, signo); return 1; #endif } 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; } #ifndef HAVE_SIGNALFD if(close(sfd->write_fd) != 0) { lua_pushboolean(L, 0); return 1; } for(int i = signalfd_num; i > 0; i--) { if(signalfds[i].fd == sfd->fd) { signalfds[i] = signalfds[signalfd_num--]; } } #endif sfd->fd = -1; lua_pushboolean(L, 1); return 1; } static const struct luaL_Reg lsignal_lib[] = { {"signal", l_signal}, {"raise", l_raise}, #if defined(__unix__) || defined(__APPLE__) {"kill", l_kill}, #endif {"signalfd", l_signalfd}, {NULL, NULL} }; int luaopen_prosody_util_signal(lua_State *L) { luaL_checkversion(L); int i = 0; 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); /* add the library */ lua_newtable(L); luaL_setfuncs(L, lsignal_lib, 0); /* push lua_signals table into the registry */ /* put the signals inside the library table too, * they are only a reference */ lua_pushstring(L, LUA_SIGNAL); lua_newtable(L); while(lua_signals[i].name != NULL) { /* registry table */ lua_pushstring(L, lua_signals[i].name); lua_pushinteger(L, lua_signals[i].sig); lua_settable(L, -3); /* signal table */ lua_pushstring(L, lua_signals[i].name); lua_pushinteger(L, lua_signals[i].sig); lua_settable(L, -5); i++; } /* add newtable to the registry */ lua_settable(L, LUA_REGISTRYINDEX); return 1; } int luaopen_util_signal(lua_State *L) { return luaopen_prosody_util_signal(L); } prosody-13.0.1/util-src/PaxHeaders/strbitop.c0000644000000000000000000000011714773555365016154 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.827711475 prosody-13.0.1/util-src/strbitop.c0000644000175000017500000000470114773555365020355 0ustar00prosodyprosody00000000000000/* * This project is MIT licensed. Please see the * COPYING file in the source package for more information. * * Copyright (C) 2016 Kim Alvefur */ #include #include #include #include /* TODO Deduplicate code somehow */ static int strop_and(lua_State *L) { luaL_Buffer buf; size_t a, b, i; const char *str_a = luaL_checklstring(L, 1, &a); const char *str_b = luaL_checklstring(L, 2, &b); luaL_buffinit(L, &buf); if(a == 0 || b == 0) { lua_settop(L, 1); return 1; } for(i = 0; i < a; i++) { luaL_addchar(&buf, str_a[i] & str_b[i % b]); } luaL_pushresult(&buf); return 1; } static int strop_or(lua_State *L) { luaL_Buffer buf; size_t a, b, i; const char *str_a = luaL_checklstring(L, 1, &a); const char *str_b = luaL_checklstring(L, 2, &b); luaL_buffinit(L, &buf); if(a == 0 || b == 0) { lua_settop(L, 1); return 1; } for(i = 0; i < a; i++) { luaL_addchar(&buf, str_a[i] | str_b[i % b]); } luaL_pushresult(&buf); return 1; } static int strop_xor(lua_State *L) { luaL_Buffer buf; size_t a, b, i; const char *str_a = luaL_checklstring(L, 1, &a); const char *str_b = luaL_checklstring(L, 2, &b); luaL_buffinit(L, &buf); if(a == 0 || b == 0) { lua_settop(L, 1); return 1; } for(i = 0; i < a; i++) { luaL_addchar(&buf, str_a[i] ^ str_b[i % b]); } luaL_pushresult(&buf); return 1; } 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 - big endian ** < - little endian ** ![num] - alignment ** x - padding ** b/B - signed/unsigned byte ** h/H - signed/unsigned short ** l/L - signed/unsigned long ** T - size_t ** i/In - signed/unsigned integer with size 'n' (default is size of int) ** cn - sequence of 'n' chars (from/to a string); when packing, n==0 means the whole string; when unpacking, n==0 means use the previous read number as the string length ** s - zero-terminated string ** f - float ** d - double ** ' ' - ignored */ #include #include #include #include #include "lua.h" #include "lauxlib.h" /* basic integer type */ #if !defined(STRUCT_INT) #define STRUCT_INT long #endif typedef STRUCT_INT Inttype; /* corresponding unsigned version */ typedef unsigned STRUCT_INT Uinttype; /* maximum size (in bytes) for integral types */ #define MAXINTSIZE 32 /* is 'x' a power of 2? */ #define isp2(x) ((x) > 0 && ((x) & ((x) - 1)) == 0) /* dummy structure to get alignment requirements */ struct cD { char c; double d; }; #define PADDING (sizeof(struct cD) - sizeof(double)) #define MAXALIGN (PADDING > sizeof(int) ? PADDING : sizeof(int)) /* endian options */ #define BIG 0 #define LITTLE 1 static union { int dummy; char endian; } const native = {1}; typedef struct Header { int endian; int align; } Header; static int getnum (const char **fmt, int df) { if (!isdigit(**fmt)) /* no number? */ return df; /* return default value */ else { int a = 0; do { a = a*10 + *((*fmt)++) - '0'; } while (isdigit(**fmt)); return a; } } #define defaultoptions(h) ((h)->endian = native.endian, (h)->align = 1) static size_t optsize (lua_State *L, char opt, const char **fmt) { switch (opt) { case 'B': case 'b': return sizeof(char); case 'H': case 'h': return sizeof(short); case 'L': case 'l': return sizeof(long); case 'T': return sizeof(size_t); case 'f': return sizeof(float); case 'd': return sizeof(double); case 'x': return 1; case 'c': return getnum(fmt, 1); case 'i': case 'I': { int sz = getnum(fmt, sizeof(int)); if (sz > MAXINTSIZE) luaL_error(L, "integral size %d is larger than limit of %d", sz, MAXINTSIZE); return sz; } default: return 0; /* other cases do not need alignment */ } } /* ** return number of bytes needed to align an element of size 'size' ** at current position 'len' */ static int gettoalign (size_t len, Header *h, int opt, size_t size) { if (size == 0 || opt == 'c') return 0; if (size > (size_t)h->align) size = h->align; /* respect max. alignment */ return (size - (len & (size - 1))) & (size - 1); } /* ** options to control endianness and alignment */ static void controloptions (lua_State *L, int opt, const char **fmt, Header *h) { switch (opt) { case ' ': return; /* ignore white spaces */ case '>': h->endian = BIG; return; case '<': h->endian = LITTLE; return; case '!': { int a = getnum(fmt, MAXALIGN); if (!isp2(a)) luaL_error(L, "alignment %d is not a power of 2", a); h->align = a; return; } default: { const char *msg = lua_pushfstring(L, "invalid format option '%c'", opt); luaL_argerror(L, 1, msg); } } } static void putinteger (lua_State *L, luaL_Buffer *b, int arg, int endian, int size) { lua_Number n = luaL_checknumber(L, arg); Uinttype value; char buff[MAXINTSIZE]; if (n < 0) value = (Uinttype)(Inttype)n; else value = (Uinttype)n; if (endian == LITTLE) { int i; for (i = 0; i < size; i++) { buff[i] = (value & 0xff); value >>= 8; } } else { int i; for (i = size - 1; i >= 0; i--) { buff[i] = (value & 0xff); value >>= 8; } } luaL_addlstring(b, buff, size); } static void correctbytes (char *b, int size, int endian) { if (endian != native.endian) { int i = 0; while (i < --size) { char temp = b[i]; b[i++] = b[size]; b[size] = temp; } } } static int b_pack (lua_State *L) { luaL_Buffer b; const char *fmt = luaL_checkstring(L, 1); Header h; int arg = 2; size_t totalsize = 0; defaultoptions(&h); lua_pushnil(L); /* mark to separate arguments from string buffer */ luaL_buffinit(L, &b); while (*fmt != '\0') { int opt = *fmt++; size_t size = optsize(L, opt, &fmt); int toalign = gettoalign(totalsize, &h, opt, size); totalsize += toalign; while (toalign-- > 0) luaL_addchar(&b, '\0'); switch (opt) { case 'b': case 'B': case 'h': case 'H': case 'l': case 'L': case 'T': case 'i': case 'I': { /* integer types */ putinteger(L, &b, arg++, h.endian, size); break; } case 'x': { luaL_addchar(&b, '\0'); break; } case 'f': { float f = (float)luaL_checknumber(L, arg++); correctbytes((char *)&f, size, h.endian); luaL_addlstring(&b, (char *)&f, size); break; } case 'd': { double d = luaL_checknumber(L, arg++); correctbytes((char *)&d, size, h.endian); luaL_addlstring(&b, (char *)&d, size); break; } case 'c': case 's': { size_t l; const char *s = luaL_checklstring(L, arg++, &l); if (size == 0) size = l; luaL_argcheck(L, l >= (size_t)size, arg, "string too short"); luaL_addlstring(&b, s, size); if (opt == 's') { luaL_addchar(&b, '\0'); /* add zero at the end */ size++; } break; } default: controloptions(L, opt, &fmt, &h); } totalsize += size; } luaL_pushresult(&b); return 1; } static lua_Number getinteger (const char *buff, int endian, int issigned, int size) { Uinttype l = 0; int i; if (endian == BIG) { for (i = 0; i < size; i++) { l <<= 8; l |= (Uinttype)(unsigned char)buff[i]; } } else { for (i = size - 1; i >= 0; i--) { l <<= 8; l |= (Uinttype)(unsigned char)buff[i]; } } if (!issigned) return (lua_Number)l; else { /* signed format */ Uinttype mask = (Uinttype)(~((Uinttype)0)) << (size*8 - 1); if (l & mask) /* negative value? */ l |= mask; /* signal extension */ return (lua_Number)(Inttype)l; } } static int b_unpack (lua_State *L) { Header h; const char *fmt = luaL_checkstring(L, 1); size_t ld; const char *data = luaL_checklstring(L, 2, &ld); size_t pos = (size_t)luaL_optinteger(L, 3, 1) - 1; int n = 0; /* number of results */ luaL_argcheck(L, pos <= ld, 3, "initial position out of string"); defaultoptions(&h); while (*fmt) { int opt = *fmt++; size_t size = optsize(L, opt, &fmt); pos += gettoalign(pos, &h, opt, size); luaL_argcheck(L, size <= ld - pos, 2, "data string too short"); /* stack space for item + next position */ luaL_checkstack(L, 2, "too many results"); switch (opt) { case 'b': case 'B': case 'h': case 'H': case 'l': case 'L': case 'T': case 'i': case 'I': { /* integer types */ int issigned = islower(opt); lua_Number res = getinteger(data+pos, h.endian, issigned, size); lua_pushnumber(L, res); n++; break; } case 'x': { break; } case 'f': { float f; memcpy(&f, data+pos, size); correctbytes((char *)&f, sizeof(f), h.endian); lua_pushnumber(L, f); n++; break; } case 'd': { double d; memcpy(&d, data+pos, size); correctbytes((char *)&d, sizeof(d), h.endian); lua_pushnumber(L, d); n++; break; } case 'c': { if (size == 0) { if (n == 0 || !lua_isnumber(L, -1)) luaL_error(L, "format 'c0' needs a previous size"); size = lua_tonumber(L, -1); lua_pop(L, 1); n--; luaL_argcheck(L, size <= ld - pos, 2, "data string too short"); } lua_pushlstring(L, data+pos, size); n++; break; } case 's': { const char *e = (const char *)memchr(data+pos, '\0', ld - pos); if (e == NULL) luaL_error(L, "unfinished string in data"); size = (e - (data+pos)) + 1; lua_pushlstring(L, data+pos, size - 1); n++; break; } default: controloptions(L, opt, &fmt, &h); } pos += size; } lua_pushinteger(L, pos + 1); /* next position */ return n + 1; } static int b_size (lua_State *L) { Header h; const char *fmt = luaL_checkstring(L, 1); size_t pos = 0; defaultoptions(&h); while (*fmt) { int opt = *fmt++; size_t size = optsize(L, opt, &fmt); pos += gettoalign(pos, &h, opt, size); if (opt == 's') luaL_argerror(L, 1, "option 's' has no fixed size"); else if (opt == 'c' && size == 0) luaL_argerror(L, 1, "option 'c0' has no fixed size"); if (!isalnum(opt)) controloptions(L, opt, &fmt, &h); pos += size; } lua_pushinteger(L, pos); return 1; } /* }====================================================== */ static const struct luaL_Reg thislib[] = { {"pack", b_pack}, {"unpack", b_unpack}, {"size", b_size}, {NULL, NULL} }; LUALIB_API int luaopen_util_struct (lua_State *L); LUALIB_API int luaopen_prosody_util_struct (lua_State *L) { luaL_newlib(L, thislib); return 1; } LUALIB_API int luaopen_util_struct (lua_State *L) { return luaopen_prosody_util_struct(L); } /****************************************************************************** * Copyright (C) 2010-2018 Lua.org, PUC-Rio. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ******************************************************************************/ prosody-13.0.1/util-src/PaxHeaders/table.c0000644000000000000000000000011714773555365015375 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.827711475 prosody-13.0.1/util-src/table.c0000644000175000017500000000355614773555365017605 0ustar00prosodyprosody00000000000000#include #include #ifndef LUA_MAXINTEGER #include #define LUA_MAXINTEGER PTRDIFF_MAX #endif static int Lcreate_table(lua_State *L) { lua_createtable(L, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2)); return 1; } /* COMPAT: w/ Lua pre-5.2 */ static int Lpack(lua_State *L) { unsigned int n_args = lua_gettop(L); lua_createtable(L, n_args, 1); lua_insert(L, 1); for(int arg = n_args; arg >= 1; arg--) { lua_rawseti(L, 1, arg); } lua_pushinteger(L, n_args); lua_setfield(L, -2, "n"); return 1; } /* 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); 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); } prosody-13.0.1/util-src/PaxHeaders/time.c0000644000000000000000000000011714773555365015244 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.827711475 prosody-13.0.1/util-src/time.c0000644000175000017500000000146014773555365017444 0ustar00prosodyprosody00000000000000#ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L #endif #include #include static lua_Number tv2number(struct timespec *tv) { return tv->tv_sec + tv->tv_nsec * 1e-9; } static int lc_time_realtime(lua_State *L) { struct timespec t; clock_gettime(CLOCK_REALTIME, &t); lua_pushnumber(L, tv2number(&t)); return 1; } static int lc_time_monotonic(lua_State *L) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); lua_pushnumber(L, tv2number(&t)); return 1; } int luaopen_prosody_util_time(lua_State *L) { lua_createtable(L, 0, 2); { lua_pushcfunction(L, lc_time_realtime); lua_setfield(L, -2, "now"); lua_pushcfunction(L, lc_time_monotonic); lua_setfield(L, -2, "monotonic"); } return 1; } int luaopen_util_time(lua_State *L) { return luaopen_prosody_util_time(L); } prosody-13.0.1/util-src/PaxHeaders/windows.c0000644000000000000000000000011714773555365016000 xustar0029 mtime=1743706869.95171197 20 atime=1743706737 30 ctime=1743706869.827711475 prosody-13.0.1/util-src/windows.c0000644000175000017500000000503214773555365020177 0ustar00prosodyprosody00000000000000/* Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- */ /* * windows.c * Windows support functions for Lua */ #include #include #include #include "lua.h" #include "lauxlib.h" #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif static int Lget_nameservers(lua_State *L) { char stack_buffer[1024]; // stack allocated buffer IP4_ARRAY *ips = (IP4_ARRAY *) stack_buffer; DWORD len = sizeof(stack_buffer); DNS_STATUS status; status = DnsQueryConfig(DnsConfigDnsServerList, FALSE, NULL, NULL, ips, &len); if(status == 0) { DWORD i; lua_createtable(L, ips->AddrCount, 0); for(i = 0; i < ips->AddrCount; i++) { DWORD ip = ips->AddrArray[i]; char ip_str[16] = ""; sprintf_s(ip_str, sizeof(ip_str), "%d.%d.%d.%d", (ip >> 0) & 255, (ip >> 8) & 255, (ip >> 16) & 255, (ip >> 24) & 255); lua_pushstring(L, ip_str); lua_rawseti(L, -2, i + 1); } return 1; } else { luaL_pushfail(L); lua_pushfstring(L, "DnsQueryConfig returned %d", status); return 2; } } static int lerror(lua_State *L, char *string) { luaL_pushfail(L); lua_pushfstring(L, "%s: %d", string, GetLastError()); return 2; } static int Lget_consolecolor(lua_State *L) { HWND console = GetStdHandle(STD_OUTPUT_HANDLE); WORD color; DWORD read_len; CONSOLE_SCREEN_BUFFER_INFO info; if(console == INVALID_HANDLE_VALUE) { return lerror(L, "GetStdHandle"); } if(!GetConsoleScreenBufferInfo(console, &info)) { return lerror(L, "GetConsoleScreenBufferInfo"); } if(!ReadConsoleOutputAttribute(console, &color, 1, info.dwCursorPosition, &read_len)) { return lerror(L, "ReadConsoleOutputAttribute"); } lua_pushnumber(L, color); return 1; } static int Lset_consolecolor(lua_State *L) { int color = luaL_checkint(L, 1); HWND console = GetStdHandle(STD_OUTPUT_HANDLE); if(console == INVALID_HANDLE_VALUE) { return lerror(L, "GetStdHandle"); } if(!SetConsoleTextAttribute(console, color)) { return lerror(L, "SetConsoleTextAttribute"); } lua_pushboolean(L, 1); return 1; } static const luaL_Reg Reg[] = { { "get_nameservers", Lget_nameservers }, { "get_consolecolor", Lget_consolecolor }, { "set_consolecolor", Lset_consolecolor }, { NULL, NULL } }; LUALIB_API int luaopen_util_windows(lua_State *L) { luaL_checkversion(L); lua_newtable(L); luaL_setfuncs(L, Reg, 0); lua_pushliteral(L, "-3.14"); lua_setfield(L, -2, "version"); return 1; }