mirror of
https://github.com/bjc/prosody.git
synced 2025-04-04 05:37:39 +03:00
Merge 0.12->trunk
This commit is contained in:
commit
080d7974bf
181 changed files with 6150 additions and 1993 deletions
|
@ -2,7 +2,7 @@ cache = true
|
|||
codes = true
|
||||
ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV", "431/log", "214", "581" }
|
||||
|
||||
std = "lua53c"
|
||||
std = "lua54c"
|
||||
max_line_length = 150
|
||||
|
||||
read_globals = {
|
||||
|
@ -62,6 +62,8 @@ files["plugins/"] = {
|
|||
"module.broadcast",
|
||||
"module.context",
|
||||
"module.depends",
|
||||
"module.default_permission",
|
||||
"module.default_permissions",
|
||||
"module.fire_event",
|
||||
"module.get_directory",
|
||||
"module.get_host",
|
||||
|
@ -86,6 +88,7 @@ files["plugins/"] = {
|
|||
"module.load_resource",
|
||||
"module.log",
|
||||
"module.log_status",
|
||||
"module.may",
|
||||
"module.measure",
|
||||
"module.metric",
|
||||
"module.open_store",
|
||||
|
@ -149,8 +152,6 @@ if os.getenv("PROSODY_STRICT_LINT") ~= "1" then
|
|||
"net/dns.lua";
|
||||
"net/server_select.lua";
|
||||
|
||||
"util/vcard.lua";
|
||||
|
||||
"plugins/mod_storage_sql1.lua";
|
||||
|
||||
"spec/core_moduleapi_spec.lua";
|
||||
|
@ -171,6 +172,7 @@ if os.getenv("PROSODY_STRICT_LINT") ~= "1" then
|
|||
"tools/migration/migrator/prosody_sql.lua";
|
||||
"tools/migration/prosody-migrator.lua";
|
||||
"tools/openfire2prosody.lua";
|
||||
"tools/test_mutants.sh.lua";
|
||||
"tools/xep227toprosody.lua";
|
||||
}
|
||||
for _, file in ipairs(exclude_files) do
|
||||
|
|
|
@ -22,3 +22,9 @@ rules:
|
|||
message: Non-string default from :get_option_string
|
||||
severity: ERROR
|
||||
languages: [lua]
|
||||
- id: stanza-empty-text-constructor
|
||||
patterns:
|
||||
- pattern: $A:text()
|
||||
message: Use :get_text() to read text, or pass a value here to add text
|
||||
severity: WARNING
|
||||
languages: [lua]
|
||||
|
|
42
CHANGES
42
CHANGES
|
@ -1,3 +1,45 @@
|
|||
TRUNK
|
||||
=====
|
||||
|
||||
## New
|
||||
|
||||
### Administration
|
||||
|
||||
- Add 'watch log' command to follow live debug logs at runtime (even if disabled)
|
||||
|
||||
### Networking
|
||||
|
||||
- Honour 'weight' parameter during SRV record selection
|
||||
- Support for RFC 8305 "Happy Eyeballs" to improve IPv4/IPv6 connectivity
|
||||
- Support for TCP Fast Open in server_epoll (pending LuaSocket support)
|
||||
- Support for deferred accept in server_epoll (pending LuaSocket support)
|
||||
|
||||
### MUC
|
||||
|
||||
- Permissions updates:
|
||||
- Room creation restricted to local users (of the parent host) by default
|
||||
- restrict_room_creation = true restricts to admins, false disables all restrictions
|
||||
- Persistent rooms can only be created by local users (parent host) by default
|
||||
- muc_room_allow_persistent = false restricts to admins
|
||||
- Public rooms can only be created by local users (parent host) by default
|
||||
- muc_room_allow_public = false restricts to admins
|
||||
|
||||
### Security and authentication
|
||||
|
||||
- Advertise supported SASL Channel-Binding types (XEP-0440)
|
||||
- Implement RFC 9266 'tls-exporter' channel binding with TLS 1.3
|
||||
- New role and permissions framework and API
|
||||
|
||||
## Changes
|
||||
|
||||
- Support sub-second precision timestamps
|
||||
- mod_blocklist: New option 'migrate_legacy_blocking' to disable migration from mod_privacy
|
||||
|
||||
## Removed
|
||||
|
||||
- Lua 5.1 support
|
||||
- XEP-0090 support removed from mod_time
|
||||
|
||||
0.12.0
|
||||
======
|
||||
|
||||
|
|
|
@ -71,12 +71,13 @@ install-util: util/encodings.so util/encodings.so util/pposix.so util/signal.so
|
|||
|
||||
install-plugins:
|
||||
$(MKDIR) $(MODULES)
|
||||
$(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam
|
||||
$(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam $(MODULES)/mod_debug_stanzas
|
||||
$(INSTALL_DATA) plugins/*.lua $(MODULES)
|
||||
$(INSTALL_DATA) plugins/mod_pubsub/*.lua $(MODULES)/mod_pubsub
|
||||
$(INSTALL_DATA) plugins/adhoc/*.lua $(MODULES)/adhoc
|
||||
$(INSTALL_DATA) plugins/muc/*.lua $(MODULES)/muc
|
||||
$(INSTALL_DATA) plugins/mod_mam/*.lua $(MODULES)/mod_mam
|
||||
$(INSTALL_DATA) plugins/mod_debug_stanzas/*.lua $(MODULES)/mod_debug_stanzas
|
||||
|
||||
install-man:
|
||||
$(MKDIR) $(MAN)/man1
|
||||
|
|
|
@ -46,7 +46,7 @@ subjectAltName = @subject_alternative_name
|
|||
|
||||
[ subject_alternative_name ]
|
||||
|
||||
# See http://tools.ietf.org/html/rfc6120#section-13.7.1.2 for more info.
|
||||
# See https://www.rfc-editor.org/rfc/rfc6120.html#section-13.7.1.2 for more info.
|
||||
|
||||
DNS.0 = example.com
|
||||
otherName.0 = xmppAddr;FORMAT:UTF8,UTF8:example.com
|
||||
|
|
25
configure
vendored
25
configure
vendored
|
@ -45,7 +45,7 @@ Configure $APP_NAME prior to building.
|
|||
Default is \$PREFIX/lib
|
||||
--datadir=DIR Location where the server data should be stored.
|
||||
Default is \$PREFIX/var/lib/$APP_DIRNAME
|
||||
--lua-version=VERSION Use specific Lua version: 5.1, 5.2, or 5.3
|
||||
--lua-version=VERSION Use specific Lua version: 5.2, 5.3, or 5.4
|
||||
Default is auto-detected.
|
||||
--lua-suffix=SUFFIX Versioning suffix to use in Lua filenames.
|
||||
Default is "$LUA_SUFFIX" (lua$LUA_SUFFIX...)
|
||||
|
@ -173,7 +173,8 @@ do
|
|||
--lua-version|--with-lua-version)
|
||||
[ -n "$value" ] || die "Missing value in flag $key."
|
||||
LUA_VERSION="$value"
|
||||
[ "$LUA_VERSION" = "5.1" ] || [ "$LUA_VERSION" = "5.2" ] || [ "$LUA_VERSION" = "5.3" ] || [ "$LUA_VERSION" = "5.4" ] || die "Invalid Lua version in flag $key."
|
||||
[ "$LUA_VERSION" != "5.1" ] || die "Lua 5.1 is no longer supported"
|
||||
[ "$LUA_VERSION" = "5.2" ] || [ "$LUA_VERSION" = "5.3" ] || [ "$LUA_VERSION" = "5.4" ] || die "Invalid Lua version in flag $key."
|
||||
LUA_VERSION_SET=yes
|
||||
;;
|
||||
--with-lua)
|
||||
|
@ -275,11 +276,11 @@ if [ "$OSPRESET_SET" = "yes" ]; then
|
|||
CFLAGS="$CFLAGS -ggdb"
|
||||
fi
|
||||
if [ "$OSPRESET" = "freebsd" ] || [ "$OSPRESET" = "openbsd" ]; then
|
||||
LUA_INCDIR="/usr/local/include/lua51"
|
||||
LUA_INCDIR="/usr/local/include/lua52"
|
||||
LUA_INCDIR_SET=yes
|
||||
CFLAGS="-Wall -fPIC -I/usr/local/include"
|
||||
LDFLAGS="-I/usr/local/include -L/usr/local/lib -shared"
|
||||
LUA_SUFFIX="51"
|
||||
LUA_SUFFIX="52"
|
||||
LUA_SUFFIX_SET=yes
|
||||
LUA_DIR=/usr/local
|
||||
LUA_DIR_SET=yes
|
||||
|
@ -291,16 +292,16 @@ if [ "$OSPRESET_SET" = "yes" ]; then
|
|||
LUA_INCDIR_SET="yes"
|
||||
fi
|
||||
if [ "$OSPRESET" = "netbsd" ]; then
|
||||
LUA_INCDIR="/usr/pkg/include/lua-5.1"
|
||||
LUA_INCDIR="/usr/pkg/include/lua-5.2"
|
||||
LUA_INCDIR_SET=yes
|
||||
LUA_LIBDIR="/usr/pkg/lib/lua/5.1"
|
||||
LUA_LIBDIR="/usr/pkg/lib/lua/5.2"
|
||||
LUA_LIBDIR_SET=yes
|
||||
CFLAGS="-Wall -fPIC -I/usr/pkg/include"
|
||||
LDFLAGS="-L/usr/pkg/lib -Wl,-rpath,/usr/pkg/lib -shared"
|
||||
fi
|
||||
if [ "$OSPRESET" = "pkg-config" ]; then
|
||||
if [ "$LUA_SUFFIX_SET" != "yes" ]; then
|
||||
LUA_SUFFIX="5.1";
|
||||
LUA_SUFFIX="5.4";
|
||||
LUA_SUFFIX_SET=yes
|
||||
fi
|
||||
LUA_CF="$(pkg-config --cflags-only-I lua$LUA_SUFFIX)"
|
||||
|
@ -335,7 +336,7 @@ then
|
|||
fi
|
||||
|
||||
detect_lua_version() {
|
||||
detected_lua=$("$1" -e 'print(_VERSION:match(" (5%.[1234])$"))' 2> /dev/null)
|
||||
detected_lua=$("$1" -e 'print(_VERSION:match(" (5%.[234])$"))' 2> /dev/null)
|
||||
if [ "$detected_lua" != "nil" ]
|
||||
then
|
||||
if [ "$LUA_VERSION_SET" != "yes" ]
|
||||
|
@ -389,10 +390,7 @@ search_interpreter() {
|
|||
lua_interp_found=no
|
||||
if [ "$LUA_SUFFIX_SET" != "yes" ]
|
||||
then
|
||||
if [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.1" ]
|
||||
then
|
||||
suffixes="5.1 51 -5.1 -51"
|
||||
elif [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.2" ]
|
||||
if [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.2" ]
|
||||
then
|
||||
suffixes="5.2 52 -5.2 -52"
|
||||
elif [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.3" ]
|
||||
|
@ -402,8 +400,7 @@ then
|
|||
then
|
||||
suffixes="5.4 54 -5.4 -54"
|
||||
else
|
||||
suffixes="5.1 51 -5.1 -51"
|
||||
suffixes="$suffixes 5.2 52 -5.2 -52"
|
||||
suffixes="5.2 52 -5.2 -52"
|
||||
suffixes="$suffixes 5.3 53 -5.3 -53"
|
||||
suffixes="$suffixes 5.4 54 -5.4 -54"
|
||||
fi
|
||||
|
|
|
@ -9,9 +9,8 @@
|
|||
local ssl = require "ssl";
|
||||
local configmanager = require "core.configmanager";
|
||||
local log = require "util.logger".init("certmanager");
|
||||
local ssl_context = ssl.context or require "ssl.context";
|
||||
local ssl_newcontext = ssl.newcontext;
|
||||
local new_config = require"util.sslconfig".new;
|
||||
local new_config = require"net.server".tls_builder;
|
||||
local stat = require "lfs".attributes;
|
||||
|
||||
local x509 = require "util.x509";
|
||||
|
@ -313,10 +312,6 @@ else
|
|||
core_defaults.curveslist = nil;
|
||||
end
|
||||
|
||||
local path_options = { -- These we pass through resolve_path()
|
||||
key = true, certificate = true, cafile = true, capath = true, dhparam = true
|
||||
}
|
||||
|
||||
local function create_context(host, mode, ...)
|
||||
local cfg = new_config();
|
||||
cfg:apply(core_defaults);
|
||||
|
@ -352,34 +347,7 @@ local function create_context(host, mode, ...)
|
|||
if user_ssl_config.certificate and not user_ssl_config.key then return nil, "No key present in SSL/TLS configuration for "..host; end
|
||||
end
|
||||
|
||||
for option in pairs(path_options) do
|
||||
if type(user_ssl_config[option]) == "string" then
|
||||
user_ssl_config[option] = resolve_path(config_path, user_ssl_config[option]);
|
||||
else
|
||||
user_ssl_config[option] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- LuaSec expects dhparam to be a callback that takes two arguments.
|
||||
-- We ignore those because it is mostly used for having a separate
|
||||
-- set of params for EXPORT ciphers, which we don't have by default.
|
||||
if type(user_ssl_config.dhparam) == "string" then
|
||||
local f, err = io_open(user_ssl_config.dhparam);
|
||||
if not f then return nil, "Could not open DH parameters: "..err end
|
||||
local dhparam = f:read("*a");
|
||||
f:close();
|
||||
user_ssl_config.dhparam = function() return dhparam; end
|
||||
end
|
||||
|
||||
local ctx, err = ssl_newcontext(user_ssl_config);
|
||||
|
||||
-- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care
|
||||
-- of it ourselves (W/A for #x)
|
||||
if ctx and user_ssl_config.ciphers then
|
||||
local success;
|
||||
success, err = ssl_context.setcipher(ctx, user_ssl_config.ciphers);
|
||||
if not success then ctx = nil; end
|
||||
end
|
||||
local ctx, err = cfg:build();
|
||||
|
||||
if not ctx then
|
||||
err = err or "invalid ssl config"
|
||||
|
|
|
@ -40,16 +40,10 @@ function _M.getconfig()
|
|||
return config;
|
||||
end
|
||||
|
||||
function _M.get(host, key, _oldkey)
|
||||
if key == "core" then
|
||||
key = _oldkey; -- COMPAT with code that still uses "core"
|
||||
end
|
||||
function _M.get(host, key)
|
||||
return config[host][key];
|
||||
end
|
||||
function _M.rawget(host, key, _oldkey)
|
||||
if key == "core" then
|
||||
key = _oldkey; -- COMPAT with code that still uses "core"
|
||||
end
|
||||
function _M.rawget(host, key)
|
||||
local hostconfig = rawget(config, host);
|
||||
if hostconfig then
|
||||
return rawget(hostconfig, key);
|
||||
|
@ -68,10 +62,7 @@ local function set(config_table, host, key, value)
|
|||
return false;
|
||||
end
|
||||
|
||||
function _M.set(host, key, value, _oldvalue)
|
||||
if key == "core" then
|
||||
key, value = value, _oldvalue; --COMPAT with code that still uses "core"
|
||||
end
|
||||
function _M.set(host, key, value)
|
||||
return set(config, host, key, value);
|
||||
end
|
||||
|
||||
|
|
|
@ -4,5 +4,7 @@ return {
|
|||
available = set.new{
|
||||
-- mod_bookmarks bundled
|
||||
"mod_bookmarks";
|
||||
-- Roles, module.may and per-session authz
|
||||
"permissions";
|
||||
};
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@ local promise = require "util.promise";
|
|||
local time_now = require "util.time".now;
|
||||
local format = require "util.format".format;
|
||||
local jid_node = require "util.jid".node;
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_resource = require "util.jid".resource;
|
||||
|
||||
local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
|
||||
|
@ -26,8 +27,8 @@ local error, setmetatable, type = error, setmetatable, type;
|
|||
local ipairs, pairs, select = ipairs, pairs, select;
|
||||
local tonumber, tostring = tonumber, tostring;
|
||||
local require = require;
|
||||
local pack = table.pack or require "util.table".pack; -- table.pack is only in 5.2
|
||||
local unpack = table.unpack or unpack; --luacheck: ignore 113 -- renamed in 5.2
|
||||
local pack = table.pack;
|
||||
local unpack = table.unpack;
|
||||
|
||||
local prosody = prosody;
|
||||
local hosts = prosody.hosts;
|
||||
|
@ -537,6 +538,7 @@ function api:load_resource(path, mode)
|
|||
end
|
||||
|
||||
function api:open_store(name, store_type)
|
||||
if self.host == "*" then return nil, "global-storage-not-supported"; end
|
||||
return require"core.storagemanager".open(self.host, name or self.name, store_type);
|
||||
end
|
||||
|
||||
|
@ -601,4 +603,73 @@ 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:may(action, context)
|
||||
if action:byte(1) == 58 then -- action begins with ':'
|
||||
action = self.name..action; -- prepend module name
|
||||
end
|
||||
if type(context) == "string" then -- check JID permissions
|
||||
local role;
|
||||
local node, host = jid_split(context);
|
||||
if host == self.host then
|
||||
role = hosts[host].authz.get_user_role(node);
|
||||
else
|
||||
role = hosts[self.host].authz.get_jid_role(context);
|
||||
end
|
||||
if not role then
|
||||
self:log("debug", "Access denied: JID <%s> may not %s (no role found)", context, action);
|
||||
return false;
|
||||
end
|
||||
local permit = role:may(action);
|
||||
if not permit then
|
||||
self:log("debug", "Access denied: JID <%s> may not %s (not permitted by role %s)", context, action, role.name);
|
||||
end
|
||||
return permit;
|
||||
end
|
||||
|
||||
local session = context.origin or context.session;
|
||||
if type(session) ~= "table" then
|
||||
error("Unable to identify actor session from context");
|
||||
end
|
||||
if session.role and session.type == "c2s" and session.host == self.host then
|
||||
local permit = session.role:may(action, context);
|
||||
if not permit then
|
||||
self:log("debug", "Access denied: session %s (%s) may not %s (not permitted by role %s)",
|
||||
session.id, session.full_jid, action, session.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
|
||||
self:log("debug", "Access denied: JID <%s> may not %s (no role found)", actor_jid, action);
|
||||
return false;
|
||||
end
|
||||
local permit = role:may(action, context);
|
||||
if not permit 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
|
||||
|
||||
return api;
|
||||
|
|
|
@ -240,21 +240,22 @@ local function add_sni_host(host, service)
|
|||
log("debug", "Gathering certificates for SNI for host %s, %s service", host, service or "default");
|
||||
for name, interface, port, n, active_service --luacheck: ignore 213
|
||||
in active_services:iter(service, nil, nil, nil) do
|
||||
if active_service.server.hosts and active_service.tls_cfg then
|
||||
local config_prefix = (active_service.config_prefix or name).."_";
|
||||
if config_prefix == "_" then config_prefix = ""; end
|
||||
local prefix_ssl_config = config.get(host, config_prefix.."ssl");
|
||||
if active_service.server and active_service.tls_cfg then
|
||||
local alternate_host = name and config.get(host, name.."_host");
|
||||
if not alternate_host and name == "https" then
|
||||
-- TODO should this be some generic thing? e.g. in the service definition
|
||||
alternate_host = config.get(host, "http_host");
|
||||
end
|
||||
local autocert = certmanager.find_host_cert(alternate_host or host);
|
||||
-- luacheck: ignore 211/cfg
|
||||
local ssl, err, cfg = certmanager.create_context(host, "server", prefix_ssl_config, autocert, active_service.tls_cfg);
|
||||
if ssl then
|
||||
active_service.server.hosts[alternate_host or host] = ssl;
|
||||
else
|
||||
local manualcert = active_service.tls_cfg;
|
||||
local certificate = (autocert and autocert.certificate) or manualcert.certificate;
|
||||
local key = (autocert and autocert.key) or manualcert.key;
|
||||
local ok, err = active_service.server:sslctx():set_sni_host(
|
||||
host,
|
||||
certificate,
|
||||
key
|
||||
);
|
||||
if not ok then
|
||||
log("error", "Error creating TLS context for SNI host %s: %s", host, err);
|
||||
end
|
||||
end
|
||||
|
@ -277,7 +278,7 @@ prosody.events.add_handler("host-deactivated", function (host)
|
|||
for name, interface, port, n, active_service --luacheck: ignore 213
|
||||
in active_services:iter(nil, nil, nil, nil) do
|
||||
if active_service.tls_cfg then
|
||||
active_service.server.hosts[host] = nil;
|
||||
active_service.server:sslctx():remove_sni_host(host)
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
local tostring, setmetatable = tostring, setmetatable;
|
||||
local pairs, next= pairs, next;
|
||||
|
||||
local hosts = prosody.hosts;
|
||||
local prosody, hosts = prosody, prosody.hosts;
|
||||
local full_sessions = prosody.full_sessions;
|
||||
local bare_sessions = prosody.bare_sessions;
|
||||
|
||||
|
@ -92,6 +92,49 @@ local function retire_session(session)
|
|||
return setmetatable(session, resting_session);
|
||||
end
|
||||
|
||||
-- Update a session with a new one (transplanting connection, filters, etc.)
|
||||
-- new_session should be discarded after this call returns
|
||||
local function update_session(to_session, from_session)
|
||||
to_session.log("debug", "Updating with parameters from session %s", from_session.id);
|
||||
from_session.log("debug", "Session absorbed into %s", to_session.id);
|
||||
|
||||
local replaced_conn = to_session.conn;
|
||||
if replaced_conn then
|
||||
to_session.log("debug", "closing a replaced connection for this session");
|
||||
replaced_conn:close();
|
||||
end
|
||||
|
||||
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.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);
|
||||
|
||||
-- Retire the session we've pulled from, to avoid two sessions on the same connection
|
||||
retire_session(from_session);
|
||||
|
||||
prosody.events.fire_event("c2s-session-updated", {
|
||||
session = to_session;
|
||||
from_session = from_session;
|
||||
replaced_conn = replaced_conn;
|
||||
});
|
||||
end
|
||||
|
||||
local function destroy_session(session, err)
|
||||
(session.log or log)("debug", "Destroying session for %s (%s@%s)%s",
|
||||
session.full_jid or "(unknown)", session.username or "(unknown)",
|
||||
|
@ -123,15 +166,24 @@ local function destroy_session(session, err)
|
|||
retire_session(session);
|
||||
end
|
||||
|
||||
local function make_authenticated(session, username, scope)
|
||||
local function make_authenticated(session, username, role_name)
|
||||
username = nodeprep(username);
|
||||
if not username or #username == 0 then return nil, "Invalid username"; end
|
||||
session.username = username;
|
||||
if session.type == "c2s_unauthed" then
|
||||
session.type = "c2s_unbound";
|
||||
end
|
||||
session.auth_scope = scope;
|
||||
session.log("info", "Authenticated as %s@%s", username, session.host or "(unknown)");
|
||||
|
||||
local role;
|
||||
if role_name then
|
||||
role = hosts[session.host].authz.get_role_by_name(role_name);
|
||||
else
|
||||
role = hosts[session.host].authz.get_user_role(username);
|
||||
end
|
||||
if role then
|
||||
sessionlib.set_role(session, role);
|
||||
end
|
||||
session.log("info", "Authenticated as %s@%s [%s]", username, session.host or "(unknown)", role and role.name or "no role");
|
||||
return true;
|
||||
end
|
||||
|
||||
|
@ -258,6 +310,7 @@ end
|
|||
return {
|
||||
new_session = new_session;
|
||||
retire_session = retire_session;
|
||||
update_session = update_session;
|
||||
destroy_session = destroy_session;
|
||||
make_authenticated = make_authenticated;
|
||||
bind_resource = bind_resource;
|
||||
|
|
|
@ -127,7 +127,7 @@ function core_process_stanza(origin, stanza)
|
|||
end
|
||||
core_post_stanza(origin, stanza, origin.full_jid);
|
||||
else
|
||||
local h = hosts[stanza.attr.to or origin.host or origin.to_host];
|
||||
local h = hosts[stanza.attr.to or origin.host];
|
||||
if h then
|
||||
local event;
|
||||
if xmlns == nil then
|
||||
|
@ -143,7 +143,7 @@ function core_process_stanza(origin, stanza)
|
|||
if h.events.fire_event(event, {origin = origin, stanza = stanza}) then return; end
|
||||
end
|
||||
if host and not hosts[host] then host = nil; end -- COMPAT: workaround for a Pidgin bug which sets 'to' to the SRV result
|
||||
handle_unhandled_stanza(host or origin.host or origin.to_host, origin, stanza);
|
||||
handle_unhandled_stanza(host or origin.host, origin, stanza);
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -9,14 +9,10 @@
|
|||
local modulemanager = require "core.modulemanager";
|
||||
local log = require "util.logger".init("usermanager");
|
||||
local type = type;
|
||||
local it = require "util.iterators";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_prep = require "util.jid".prep;
|
||||
local config = require "core.configmanager";
|
||||
local sasl_new = require "util.sasl".new;
|
||||
local storagemanager = require "core.storagemanager";
|
||||
local set = require "util.set";
|
||||
|
||||
local prosody = _G.prosody;
|
||||
local hosts = prosody.hosts;
|
||||
|
@ -25,6 +21,8 @@ local setmetatable = setmetatable;
|
|||
|
||||
local default_provider = "internal_hashed";
|
||||
|
||||
local debug = debug;
|
||||
|
||||
local _ENV = nil;
|
||||
-- luacheck: std none
|
||||
|
||||
|
@ -36,26 +34,25 @@ local function new_null_provider()
|
|||
});
|
||||
end
|
||||
|
||||
local global_admins_config = config.get("*", "admins");
|
||||
if type(global_admins_config) ~= "table" then
|
||||
global_admins_config = nil; -- TODO: factor out moduleapi magic config handling and use it here
|
||||
end
|
||||
local global_admins = set.new(global_admins_config) / jid_prep;
|
||||
local fallback_authz_provider = {
|
||||
-- luacheck: ignore 212
|
||||
get_jids_with_role = function (role) end;
|
||||
|
||||
local admin_role = { ["prosody:admin"] = true };
|
||||
local global_authz_provider = {
|
||||
get_user_roles = function (user) end; --luacheck: ignore 212/user
|
||||
get_jid_roles = function (jid)
|
||||
if global_admins:contains(jid) then
|
||||
return admin_role;
|
||||
end
|
||||
end;
|
||||
get_jids_with_role = function (role)
|
||||
if role ~= "prosody:admin" then return {}; end
|
||||
return it.to_array(global_admins);
|
||||
end;
|
||||
set_user_roles = function (user, roles) end; -- luacheck: ignore 212
|
||||
set_jid_roles = function (jid, roles) end; -- luacheck: ignore 212
|
||||
get_user_role = function (user) end;
|
||||
set_user_role = function (user, role_name) end;
|
||||
|
||||
get_user_secondary_roles = function (user) end;
|
||||
add_user_secondary_role = function (user, host, role_name) end;
|
||||
remove_user_secondary_role = function (user, host, role_name) end;
|
||||
|
||||
user_can_assume_role = function(user, role_name) end;
|
||||
|
||||
get_jid_role = function (jid) end;
|
||||
set_jid_role = function (jid, role) end;
|
||||
|
||||
get_users_with_role = function (role_name) end;
|
||||
add_default_permission = function (role_name, action, policy) end;
|
||||
get_role_by_name = function (role_name) end;
|
||||
};
|
||||
|
||||
local provider_mt = { __index = new_null_provider() };
|
||||
|
@ -66,7 +63,7 @@ local function initialize_host(host)
|
|||
local authz_provider_name = config.get(host, "authorization") or "internal";
|
||||
|
||||
local authz_mod = modulemanager.load(host, "authz_"..authz_provider_name);
|
||||
host_session.authz = authz_mod or global_authz_provider;
|
||||
host_session.authz = authz_mod or fallback_authz_provider;
|
||||
|
||||
if host_session.type ~= "local" then return; end
|
||||
|
||||
|
@ -116,6 +113,12 @@ local function set_password(username, password, host, resource)
|
|||
return ok, err;
|
||||
end
|
||||
|
||||
local function get_account_info(username, host)
|
||||
local method = hosts[host].users.get_account_info;
|
||||
if not method then return nil, "method-not-supported"; end
|
||||
return method(username);
|
||||
end
|
||||
|
||||
local function user_exists(username, host)
|
||||
if hosts[host].sessions[username] then return true; end
|
||||
return hosts[host].users.user_exists(username);
|
||||
|
@ -144,70 +147,113 @@ local function get_provider(host)
|
|||
return hosts[host].users;
|
||||
end
|
||||
|
||||
local function get_roles(jid, host)
|
||||
local function get_user_role(user, host)
|
||||
if host and not hosts[host] then return false; end
|
||||
if type(jid) ~= "string" then return false; end
|
||||
if type(user) ~= "string" then return false; end
|
||||
|
||||
jid = jid_bare(jid);
|
||||
host = host or "*";
|
||||
|
||||
local actor_user, actor_host = jid_split(jid);
|
||||
local roles;
|
||||
|
||||
local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider;
|
||||
|
||||
if actor_user and actor_host == host then -- Local user
|
||||
roles = authz_provider.get_user_roles(actor_user);
|
||||
else -- Remote user/JID
|
||||
roles = authz_provider.get_jid_roles(jid);
|
||||
end
|
||||
|
||||
return roles;
|
||||
return hosts[host].authz.get_user_role(user);
|
||||
end
|
||||
|
||||
local function set_roles(jid, host, roles)
|
||||
local function set_user_role(user, host, role_name)
|
||||
if host and not hosts[host] then return false; end
|
||||
if type(jid) ~= "string" then return false; end
|
||||
if type(user) ~= "string" then return false; end
|
||||
|
||||
jid = jid_bare(jid);
|
||||
host = host or "*";
|
||||
|
||||
local actor_user, actor_host = jid_split(jid);
|
||||
|
||||
local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider;
|
||||
if actor_user and actor_host == host then -- Local user
|
||||
local ok, err = authz_provider.set_user_roles(actor_user, roles);
|
||||
if ok then
|
||||
prosody.events.fire_event("user-roles-changed", {
|
||||
username = actor_user, host = actor_host
|
||||
});
|
||||
end
|
||||
return ok, err;
|
||||
else -- Remote entity
|
||||
return authz_provider.set_jid_roles(jid, roles)
|
||||
local role, err = hosts[host].authz.set_user_role(user, role_name);
|
||||
if role then
|
||||
prosody.events.fire_event("user-role-changed", {
|
||||
username = user, host = host, role = role;
|
||||
});
|
||||
end
|
||||
return role, err;
|
||||
end
|
||||
|
||||
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 = role;
|
||||
});
|
||||
end
|
||||
return role, err;
|
||||
end
|
||||
|
||||
local function remove_user_secondary_role(user, host, role_name)
|
||||
if host and not hosts[host] then return false; end
|
||||
if type(user) ~= "string" then return false; end
|
||||
|
||||
local ok, err = hosts[host].authz.remove_user_secondary_role(user, role_name);
|
||||
if ok then
|
||||
prosody.events.fire_event("user-role-removed", {
|
||||
username = user, host = host, role_name = role_name;
|
||||
});
|
||||
end
|
||||
return ok, err;
|
||||
end
|
||||
|
||||
local function get_user_secondary_roles(user, host)
|
||||
if host and not hosts[host] then return false; end
|
||||
if type(user) ~= "string" then return false; end
|
||||
|
||||
return hosts[host].authz.get_user_secondary_roles(user);
|
||||
end
|
||||
|
||||
local function get_jid_role(jid, host)
|
||||
local jid_node, jid_host = jid_split(jid);
|
||||
if host == jid_host and jid_node then
|
||||
return hosts[host].authz.get_user_role(jid_node);
|
||||
end
|
||||
return hosts[host].authz.get_jid_role(jid);
|
||||
end
|
||||
|
||||
local function set_jid_role(jid, host, role_name)
|
||||
local _, jid_host = jid_split(jid);
|
||||
if host == jid_host then
|
||||
return nil, "unexpected-local-jid";
|
||||
end
|
||||
return hosts[host].authz.set_jid_role(jid, role_name)
|
||||
end
|
||||
|
||||
local strict_deprecate_is_admin;
|
||||
local legacy_admin_roles = { ["prosody:admin"] = true, ["prosody:operator"] = true };
|
||||
local function is_admin(jid, host)
|
||||
local roles = get_roles(jid, host);
|
||||
return roles and roles["prosody:admin"];
|
||||
if strict_deprecate_is_admin == nil then
|
||||
strict_deprecate_is_admin = (config.get("*", "strict_deprecate_is_admin") == true);
|
||||
end
|
||||
if strict_deprecate_is_admin then
|
||||
log("error", "Attempt to use deprecated is_admin() API: %s", debug.traceback());
|
||||
return false;
|
||||
end
|
||||
log("warn", "Usage of legacy is_admin() API, which will be disabled in a future build: %s", debug.traceback());
|
||||
log("warn", "See https://prosody.im/doc/developers/permissions about the new permissions API");
|
||||
return legacy_admin_roles[get_jid_role(jid, host)] or false;
|
||||
end
|
||||
|
||||
local function get_users_with_role(role, host)
|
||||
if not hosts[host] then return false; end
|
||||
if type(role) ~= "string" then return false; end
|
||||
|
||||
return hosts[host].authz.get_users_with_role(role);
|
||||
end
|
||||
|
||||
local function get_jids_with_role(role, host)
|
||||
if host and not hosts[host] then return false; end
|
||||
if type(role) ~= "string" then return false; end
|
||||
return hosts[host].authz.get_jids_with_role(role);
|
||||
end
|
||||
|
||||
host = host or "*";
|
||||
|
||||
local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider;
|
||||
return authz_provider.get_jids_with_role(role);
|
||||
local function get_role_by_name(role_name, host)
|
||||
if host and not hosts[host] then return false; end
|
||||
if type(role_name) ~= "string" then return false; end
|
||||
return hosts[host].authz.get_role_by_name(role_name);
|
||||
end
|
||||
|
||||
return {
|
||||
|
@ -216,15 +262,25 @@ return {
|
|||
test_password = test_password;
|
||||
get_password = get_password;
|
||||
set_password = set_password;
|
||||
get_account_info = get_account_info;
|
||||
user_exists = user_exists;
|
||||
create_user = create_user;
|
||||
delete_user = delete_user;
|
||||
users = users;
|
||||
get_sasl_handler = get_sasl_handler;
|
||||
get_provider = get_provider;
|
||||
get_roles = get_roles;
|
||||
set_roles = set_roles;
|
||||
is_admin = is_admin;
|
||||
get_user_role = get_user_role;
|
||||
set_user_role = set_user_role;
|
||||
user_can_assume_role = user_can_assume_role;
|
||||
add_user_secondary_role = add_user_secondary_role;
|
||||
remove_user_secondary_role = remove_user_secondary_role;
|
||||
get_user_secondary_roles = get_user_secondary_roles;
|
||||
get_users_with_role = get_users_with_role;
|
||||
get_jid_role = get_jid_role;
|
||||
set_jid_role = set_jid_role;
|
||||
get_jids_with_role = get_jids_with_role;
|
||||
get_role_by_name = get_role_by_name;
|
||||
|
||||
-- Deprecated
|
||||
is_admin = is_admin;
|
||||
};
|
||||
|
|
50
doc/doap.xml
50
doc/doap.xml
|
@ -60,6 +60,8 @@
|
|||
<implements rdf:resource="https://www.rfc-editor.org/info/rfc7395"/>
|
||||
<implements rdf:resource="https://www.rfc-editor.org/info/rfc7590"/>
|
||||
<implements rdf:resource="https://www.rfc-editor.org/info/rfc7673"/>
|
||||
<implements rdf:resource="https://www.rfc-editor.org/info/rfc8305"/>
|
||||
<implements rdf:resource="https://www.rfc-editor.org/info/rfc9266"/>
|
||||
<implements rdf:resource="https://datatracker.ietf.org/doc/draft-cridland-xmpp-session/">
|
||||
<!-- since=0.6.0 note=Added in hg:0bbbc9042361 -->
|
||||
</implements>
|
||||
|
@ -67,7 +69,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html"/>
|
||||
<xmpp:version>2.12.1</xmpp:version>
|
||||
<xmpp:version>2.13.0</xmpp:version>
|
||||
<xmpp:since>0.4.0</xmpp:since>
|
||||
<xmpp:status>partial</xmpp:status>
|
||||
<xmpp:note>no support for multiple items (reported tag)</xmpp:note>
|
||||
|
@ -119,7 +121,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0045.html"/>
|
||||
<xmpp:version>1.34.1</xmpp:version>
|
||||
<xmpp:version>1.34.3</xmpp:version>
|
||||
<xmpp:since>0.3.0</xmpp:since>
|
||||
<xmpp:status>partial</xmpp:status>
|
||||
</xmpp:SupportedXep>
|
||||
|
@ -172,7 +174,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html"/>
|
||||
<xmpp:version>1.22.0</xmpp:version>
|
||||
<xmpp:version>1.24.1</xmpp:version>
|
||||
<xmpp:since>0.9.0</xmpp:since>
|
||||
<xmpp:status>partial</xmpp:status>
|
||||
<xmpp:note>mod_pubsub</xmpp:note>
|
||||
|
@ -240,7 +242,8 @@
|
|||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0090.html"/>
|
||||
<xmpp:version>1.2</xmpp:version>
|
||||
<xmpp:since>0.1.0</xmpp:since>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:until>trunk</xmpp:until>
|
||||
<xmpp:status>removed</xmpp:status>
|
||||
<xmpp:note>mod_time</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
|
@ -268,6 +271,7 @@
|
|||
<xmpp:version>1.0</xmpp:version>
|
||||
<xmpp:since>0.9.0</xmpp:since>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:note>util.jid.(un)escape, missing rejection of \20 at start or end per xep version 1.1</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
|
@ -297,7 +301,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html"/>
|
||||
<xmpp:version>1.5.2</xmpp:version>
|
||||
<xmpp:version>1.6.0</xmpp:version>
|
||||
<xmpp:since>0.8.0</xmpp:since>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
</xmpp:SupportedXep>
|
||||
|
@ -355,7 +359,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0138.html"/>
|
||||
<xmpp:version>2.0</xmpp:version>
|
||||
<xmpp:version>2.1</xmpp:version>
|
||||
<xmpp:since>0.6.0</xmpp:since>
|
||||
<xmpp:until>0.10.0</xmpp:until>
|
||||
<xmpp:status>removed</xmpp:status>
|
||||
|
@ -390,7 +394,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0163.html"/>
|
||||
<xmpp:version>1.2.1</xmpp:version>
|
||||
<xmpp:version>1.2.2</xmpp:version>
|
||||
<xmpp:since>0.5.0</xmpp:since>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
</xmpp:SupportedXep>
|
||||
|
@ -561,7 +565,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0215.html"/>
|
||||
<xmpp:version>0.7.1</xmpp:version>
|
||||
<xmpp:version>1.0.0</xmpp:version>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:since>0.12.0</xmpp:since>
|
||||
<xmpp:note>mod_external_services</xmpp:note>
|
||||
|
@ -623,7 +627,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html"/>
|
||||
<xmpp:version>1.0.0</xmpp:version>
|
||||
<xmpp:version>1.0.1</xmpp:version>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:since>0.10.0</xmpp:since>
|
||||
</xmpp:SupportedXep>
|
||||
|
@ -657,7 +661,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0297.html"/>
|
||||
<xmpp:version>1.0.0</xmpp:version>
|
||||
<xmpp:version>1.0</xmpp:version>
|
||||
<xmpp:since>0.11.0</xmpp:since>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:note>Used by XEP-0280, XEP-0313</xmpp:note>
|
||||
|
@ -683,7 +687,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0313.html"/>
|
||||
<xmpp:version>1.0.0</xmpp:version>
|
||||
<xmpp:version>1.0.1</xmpp:version>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:since>0.10.0</xmpp:since>
|
||||
<xmpp:note>mod_mam, mod_muc_mam</xmpp:note>
|
||||
|
@ -737,7 +741,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0363.html"/>
|
||||
<xmpp:version>1.0.0</xmpp:version>
|
||||
<xmpp:version>1.1.0</xmpp:version>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:since>0.12.0</xmpp:since>
|
||||
<xmpp:note>mod_http_file_share</xmpp:note>
|
||||
|
@ -763,7 +767,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html"/>
|
||||
<xmpp:version>0.3.0</xmpp:version>
|
||||
<xmpp:version>0.4.0</xmpp:version>
|
||||
<xmpp:since>0.11.0</xmpp:since>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:note>Used in context of XEP-0352</xmpp:note>
|
||||
|
@ -772,7 +776,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0384.html"/>
|
||||
<xmpp:version>0.8.1</xmpp:version>
|
||||
<xmpp:version>0.8.3</xmpp:version>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
<xmpp:note>via XEP-0163, XEP-0222</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
|
@ -789,7 +793,7 @@
|
|||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0401.html"/>
|
||||
<xmpp:version>0.3.0</xmpp:version>
|
||||
<xmpp:version>0.5.0</xmpp:version>
|
||||
<xmpp:since>0.12.0</xmpp:since>
|
||||
<xmpp:status>partial</xmpp:status>
|
||||
</xmpp:SupportedXep>
|
||||
|
@ -845,5 +849,21 @@
|
|||
<xmpp:note>Broken out of XEP-0313</xmpp:note>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0440.html"/>
|
||||
<xmpp:version>0.4.0</xmpp:version>
|
||||
<xmpp:since>trunk</xmpp:since>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0445.html"/>
|
||||
<xmpp:version>0.2.0</xmpp:version>
|
||||
<xmpp:since>0.12.0</xmpp:since>
|
||||
<xmpp:status>complete</xmpp:status>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
</Project>
|
||||
</rdf:RDF>
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
-- Storage Interface API Description
|
||||
--
|
||||
-- This is written as a TypedLua description
|
||||
|
||||
-- Key-Value stores (the default)
|
||||
|
||||
interface keyval_store
|
||||
get : ( self, string? ) -> (any) | (nil, string)
|
||||
set : ( self, string?, any ) -> (boolean) | (nil, string)
|
||||
end
|
||||
|
||||
-- Map stores (key-key-value stores)
|
||||
|
||||
interface map_store
|
||||
get : ( self, string?, any ) -> (any) | (nil, string)
|
||||
set : ( self, string?, any, any ) -> (boolean) | (nil, string)
|
||||
set_keys : ( self, string?, { any : any }) -> (boolean) | (nil, string)
|
||||
remove : {}
|
||||
end
|
||||
|
||||
-- Archive stores
|
||||
|
||||
typealias archive_query = {
|
||||
"start" : number?, -- timestamp
|
||||
"end" : number?, -- timestamp
|
||||
"with" : string?,
|
||||
"after" : string?, -- archive id
|
||||
"before" : string?, -- archive id
|
||||
"total" : boolean?,
|
||||
}
|
||||
|
||||
interface archive_store
|
||||
-- Optional set of capabilities
|
||||
caps : {
|
||||
-- Optional total count of matching items returned as second return value from :find()
|
||||
"total" : boolean?,
|
||||
}?
|
||||
|
||||
-- Add to the archive
|
||||
append : ( self, string?, string?, any, number?, string? ) -> (string) | (nil, string)
|
||||
|
||||
-- Iterate over archive
|
||||
find : ( self, string?, archive_query? ) -> ( () -> ( string, any, number?, string? ), integer? )
|
||||
|
||||
-- Removal of items. API like find. Optional?
|
||||
delete : ( self, string?, archive_query? ) -> (boolean) | (number) | (nil, string)
|
||||
|
||||
-- Array of dates which do have messages (Optional?)
|
||||
dates : ( self, string? ) -> ({ string }) | (nil, string)
|
||||
|
||||
-- Map of counts per "with" field
|
||||
summary : ( self, string?, archive_query? ) -> ( { string : integer } ) | (nil, string)
|
||||
|
||||
-- Map-store API
|
||||
get : ( self, string, string ) -> (stanza, number?, string?) | (nil, string)
|
||||
set : ( self, string, string, stanza, number?, string? ) -> (boolean) | (nil, string)
|
||||
end
|
||||
|
||||
-- This represents moduleapi
|
||||
interface module
|
||||
-- If the first string is omitted then the name of the module is used
|
||||
-- The second string is one of "keyval" (default), "map" or "archive"
|
||||
open_store : (self, string?, string?) -> (keyval_store) | (map_store) | (archive_store) | (nil, string)
|
||||
|
||||
-- Other module methods omitted
|
||||
end
|
||||
|
||||
module : module
|
3
makefile
3
makefile
|
@ -73,12 +73,13 @@ install-util: util/encodings.so util/encodings.so util/pposix.so util/signal.so
|
|||
|
||||
install-plugins:
|
||||
$(MKDIR) $(MODULES)
|
||||
$(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam
|
||||
$(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam $(MODULES)/mod_debug_stanzas
|
||||
$(INSTALL_DATA) plugins/*.lua $(MODULES)
|
||||
$(INSTALL_DATA) plugins/mod_pubsub/*.lua $(MODULES)/mod_pubsub
|
||||
$(INSTALL_DATA) plugins/adhoc/*.lua $(MODULES)/adhoc
|
||||
$(INSTALL_DATA) plugins/muc/*.lua $(MODULES)/muc
|
||||
$(INSTALL_DATA) plugins/mod_mam/*.lua $(MODULES)/mod_mam
|
||||
$(INSTALL_DATA) plugins/mod_debug_stanzas/*.lua $(MODULES)/mod_debug_stanzas
|
||||
|
||||
install-man:
|
||||
$(MKDIR) $(MAN)/man1
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
local server = require "net.server";
|
||||
local log = require "util.logger".init("net.connect");
|
||||
local new_id = require "util.id".short;
|
||||
local timer = require "util.timer";
|
||||
|
||||
-- TODO #1246 Happy Eyeballs
|
||||
-- FIXME RFC 6724
|
||||
-- FIXME Error propagation from resolvers doesn't work
|
||||
-- FIXME #1428 Reuse DNS resolver object between service and basic resolver
|
||||
|
@ -28,16 +28,17 @@ local pending_connection_listeners = {};
|
|||
|
||||
local function attempt_connection(p)
|
||||
p:log("debug", "Checking for targets...");
|
||||
if p.conn then
|
||||
pending_connections_map[p.conn] = nil;
|
||||
p.conn = nil;
|
||||
end
|
||||
p.target_resolver:next(function (conn_type, ip, port, extra)
|
||||
p.target_resolver:next(function (conn_type, ip, port, extra, more_targets_available)
|
||||
if not conn_type then
|
||||
-- No more targets to try
|
||||
p:log("debug", "No more connection targets to try", p.target_resolver.last_error);
|
||||
if p.listeners.onfail then
|
||||
p.listeners.onfail(p.data, p.last_error or p.target_resolver.last_error or "unable to resolve service");
|
||||
if next(p.conns) == nil then
|
||||
p:log("debug", "No more targets, no pending connections. Connection failed.");
|
||||
if p.listeners.onfail then
|
||||
p.listeners.onfail(p.data, p.last_error or p.target_resolver.last_error or "unable to resolve service");
|
||||
end
|
||||
else
|
||||
p:log("debug", "One or more connection attempts are still pending. Waiting for now.");
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
@ -49,8 +50,16 @@ local function attempt_connection(p)
|
|||
p.last_error = err or "unknown reason";
|
||||
return attempt_connection(p);
|
||||
end
|
||||
p.conn = conn;
|
||||
p.conns[conn] = true;
|
||||
pending_connections_map[conn] = p;
|
||||
if more_targets_available then
|
||||
timer.add_task(0.250, function ()
|
||||
if not p.connected then
|
||||
p:log("debug", "Still not connected, making parallel connection attempt...");
|
||||
attempt_connection(p);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
|
@ -62,6 +71,13 @@ function pending_connection_listeners.onconnect(conn)
|
|||
return;
|
||||
end
|
||||
pending_connections_map[conn] = nil;
|
||||
if p.connected then
|
||||
-- We already succeeded in connecting
|
||||
p.conns[conn] = nil;
|
||||
conn:close();
|
||||
return;
|
||||
end
|
||||
p.connected = true;
|
||||
p:log("debug", "Successfully connected");
|
||||
conn:setlistener(p.listeners, p.data);
|
||||
return p.listeners.onconnect(conn);
|
||||
|
@ -73,9 +89,18 @@ function pending_connection_listeners.ondisconnect(conn, reason)
|
|||
log("warn", "Failed connection, but unexpected!");
|
||||
return;
|
||||
end
|
||||
p.conns[conn] = nil;
|
||||
pending_connections_map[conn] = nil;
|
||||
p.last_error = reason or "unknown reason";
|
||||
p:log("debug", "Connection attempt failed: %s", p.last_error);
|
||||
attempt_connection(p);
|
||||
if p.connected then
|
||||
p:log("debug", "Connection already established, ignoring failure");
|
||||
elseif next(p.conns) == nil then
|
||||
p:log("debug", "No pending connection attempts, and not yet connected");
|
||||
attempt_connection(p);
|
||||
else
|
||||
p:log("debug", "Other attempts are still pending, ignoring failure");
|
||||
end
|
||||
end
|
||||
|
||||
local function connect(target_resolver, listeners, options, data)
|
||||
|
@ -85,6 +110,7 @@ local function connect(target_resolver, listeners, options, data)
|
|||
listeners = assert(listeners);
|
||||
options = options or {};
|
||||
data = data;
|
||||
conns = {};
|
||||
}, pending_connection_mt);
|
||||
|
||||
p:log("debug", "Starting connection process");
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
-- todo: cache results of encodeName
|
||||
|
||||
|
||||
-- reference: http://tools.ietf.org/html/rfc1035
|
||||
-- reference: http://tools.ietf.org/html/rfc1876 (LOC)
|
||||
-- reference: https://www.rfc-editor.org/rfc/rfc1035.html
|
||||
-- reference: https://www.rfc-editor.org/rfc/rfc1876.html (LOC)
|
||||
|
||||
|
||||
local socket = require "socket";
|
||||
|
|
|
@ -2,62 +2,62 @@
|
|||
local response_codes = {
|
||||
-- Source: http://www.iana.org/assignments/http-status-codes
|
||||
|
||||
[100] = "Continue"; -- RFC7231, Section 6.2.1
|
||||
[101] = "Switching Protocols"; -- RFC7231, Section 6.2.2
|
||||
[100] = "Continue"; -- RFC9110, Section 15.2.1
|
||||
[101] = "Switching Protocols"; -- RFC9110, Section 15.2.2
|
||||
[102] = "Processing";
|
||||
[103] = "Early Hints";
|
||||
-- [104-199] = "Unassigned";
|
||||
|
||||
[200] = "OK"; -- RFC7231, Section 6.3.1
|
||||
[201] = "Created"; -- RFC7231, Section 6.3.2
|
||||
[202] = "Accepted"; -- RFC7231, Section 6.3.3
|
||||
[203] = "Non-Authoritative Information"; -- RFC7231, Section 6.3.4
|
||||
[204] = "No Content"; -- RFC7231, Section 6.3.5
|
||||
[205] = "Reset Content"; -- RFC7231, Section 6.3.6
|
||||
[206] = "Partial Content"; -- RFC7233, Section 4.1
|
||||
[200] = "OK"; -- RFC9110, Section 15.3.1
|
||||
[201] = "Created"; -- RFC9110, Section 15.3.2
|
||||
[202] = "Accepted"; -- RFC9110, Section 15.3.3
|
||||
[203] = "Non-Authoritative Information"; -- RFC9110, Section 15.3.4
|
||||
[204] = "No Content"; -- RFC9110, Section 15.3.5
|
||||
[205] = "Reset Content"; -- RFC9110, Section 15.3.6
|
||||
[206] = "Partial Content"; -- RFC9110, Section 15.3.7
|
||||
[207] = "Multi-Status";
|
||||
[208] = "Already Reported";
|
||||
-- [209-225] = "Unassigned";
|
||||
[226] = "IM Used";
|
||||
-- [227-299] = "Unassigned";
|
||||
|
||||
[300] = "Multiple Choices"; -- RFC7231, Section 6.4.1
|
||||
[301] = "Moved Permanently"; -- RFC7231, Section 6.4.2
|
||||
[302] = "Found"; -- RFC7231, Section 6.4.3
|
||||
[303] = "See Other"; -- RFC7231, Section 6.4.4
|
||||
[304] = "Not Modified"; -- RFC7232, Section 4.1
|
||||
[305] = "Use Proxy"; -- RFC7231, Section 6.4.5
|
||||
-- [306] = "(Unused)"; -- RFC7231, Section 6.4.6
|
||||
[307] = "Temporary Redirect"; -- RFC7231, Section 6.4.7
|
||||
[308] = "Permanent Redirect";
|
||||
[300] = "Multiple Choices"; -- RFC9110, Section 15.4.1
|
||||
[301] = "Moved Permanently"; -- RFC9110, Section 15.4.2
|
||||
[302] = "Found"; -- RFC9110, Section 15.4.3
|
||||
[303] = "See Other"; -- RFC9110, Section 15.4.4
|
||||
[304] = "Not Modified"; -- RFC9110, Section 15.4.5
|
||||
[305] = "Use Proxy"; -- RFC9110, Section 15.4.6
|
||||
-- [306] = "(Unused)"; -- RFC9110, Section 15.4.7
|
||||
[307] = "Temporary Redirect"; -- RFC9110, Section 15.4.8
|
||||
[308] = "Permanent Redirect"; -- RFC9110, Section 15.4.9
|
||||
-- [309-399] = "Unassigned";
|
||||
|
||||
[400] = "Bad Request"; -- RFC7231, Section 6.5.1
|
||||
[401] = "Unauthorized"; -- RFC7235, Section 3.1
|
||||
[402] = "Payment Required"; -- RFC7231, Section 6.5.2
|
||||
[403] = "Forbidden"; -- RFC7231, Section 6.5.3
|
||||
[404] = "Not Found"; -- RFC7231, Section 6.5.4
|
||||
[405] = "Method Not Allowed"; -- RFC7231, Section 6.5.5
|
||||
[406] = "Not Acceptable"; -- RFC7231, Section 6.5.6
|
||||
[407] = "Proxy Authentication Required"; -- RFC7235, Section 3.2
|
||||
[408] = "Request Timeout"; -- RFC7231, Section 6.5.7
|
||||
[409] = "Conflict"; -- RFC7231, Section 6.5.8
|
||||
[410] = "Gone"; -- RFC7231, Section 6.5.9
|
||||
[411] = "Length Required"; -- RFC7231, Section 6.5.10
|
||||
[412] = "Precondition Failed"; -- RFC7232, Section 4.2
|
||||
[413] = "Payload Too Large"; -- RFC7231, Section 6.5.11
|
||||
[414] = "URI Too Long"; -- RFC7231, Section 6.5.12
|
||||
[415] = "Unsupported Media Type"; -- RFC7231, Section 6.5.13
|
||||
[416] = "Range Not Satisfiable"; -- RFC7233, Section 4.4
|
||||
[417] = "Expectation Failed"; -- RFC7231, Section 6.5.14
|
||||
[400] = "Bad Request"; -- RFC9110, Section 15.5.1
|
||||
[401] = "Unauthorized"; -- RFC9110, Section 15.5.2
|
||||
[402] = "Payment Required"; -- RFC9110, Section 15.5.3
|
||||
[403] = "Forbidden"; -- RFC9110, Section 15.5.4
|
||||
[404] = "Not Found"; -- RFC9110, Section 15.5.5
|
||||
[405] = "Method Not Allowed"; -- RFC9110, Section 15.5.6
|
||||
[406] = "Not Acceptable"; -- RFC9110, Section 15.5.7
|
||||
[407] = "Proxy Authentication Required"; -- RFC9110, Section 15.5.8
|
||||
[408] = "Request Timeout"; -- RFC9110, Section 15.5.9
|
||||
[409] = "Conflict"; -- RFC9110, Section 15.5.10
|
||||
[410] = "Gone"; -- RFC9110, Section 15.5.11
|
||||
[411] = "Length Required"; -- RFC9110, Section 15.5.12
|
||||
[412] = "Precondition Failed"; -- RFC9110, Section 15.5.13
|
||||
[413] = "Content Too Large"; -- RFC9110, Section 15.5.14
|
||||
[414] = "URI Too Long"; -- RFC9110, Section 15.5.15
|
||||
[415] = "Unsupported Media Type"; -- RFC9110, Section 15.5.16
|
||||
[416] = "Range Not Satisfiable"; -- RFC9110, Section 15.5.17
|
||||
[417] = "Expectation Failed"; -- RFC9110, Section 15.5.18
|
||||
[418] = "I'm a teapot"; -- RFC2324, Section 2.3.2
|
||||
-- [419-420] = "Unassigned";
|
||||
[421] = "Misdirected Request"; -- RFC7540, Section 9.1.2
|
||||
[422] = "Unprocessable Entity";
|
||||
[421] = "Misdirected Request"; -- RFC9110, Section 15.5.20
|
||||
[422] = "Unprocessable Content"; -- RFC9110, Section 15.5.21
|
||||
[423] = "Locked";
|
||||
[424] = "Failed Dependency";
|
||||
[425] = "Too Early";
|
||||
[426] = "Upgrade Required"; -- RFC7231, Section 6.5.15
|
||||
[426] = "Upgrade Required"; -- RFC9110, Section 15.5.22
|
||||
-- [427] = "Unassigned";
|
||||
[428] = "Precondition Required";
|
||||
[429] = "Too Many Requests";
|
||||
|
@ -67,17 +67,17 @@ local response_codes = {
|
|||
[451] = "Unavailable For Legal Reasons";
|
||||
-- [452-499] = "Unassigned";
|
||||
|
||||
[500] = "Internal Server Error"; -- RFC7231, Section 6.6.1
|
||||
[501] = "Not Implemented"; -- RFC7231, Section 6.6.2
|
||||
[502] = "Bad Gateway"; -- RFC7231, Section 6.6.3
|
||||
[503] = "Service Unavailable"; -- RFC7231, Section 6.6.4
|
||||
[504] = "Gateway Timeout"; -- RFC7231, Section 6.6.5
|
||||
[505] = "HTTP Version Not Supported"; -- RFC7231, Section 6.6.6
|
||||
[500] = "Internal Server Error"; -- RFC9110, Section 15.6.1
|
||||
[501] = "Not Implemented"; -- RFC9110, Section 15.6.2
|
||||
[502] = "Bad Gateway"; -- RFC9110, Section 15.6.3
|
||||
[503] = "Service Unavailable"; -- RFC9110, Section 15.6.4
|
||||
[504] = "Gateway Timeout"; -- RFC9110, Section 15.6.5
|
||||
[505] = "HTTP Version Not Supported"; -- RFC9110, Section 15.6.6
|
||||
[506] = "Variant Also Negotiates";
|
||||
[507] = "Insufficient Storage";
|
||||
[508] = "Loop Detected";
|
||||
-- [509] = "Unassigned";
|
||||
[510] = "Not Extended";
|
||||
[510] = "Not Extended"; -- (OBSOLETED)
|
||||
[511] = "Network Authentication Required";
|
||||
-- [512-599] = "Unassigned";
|
||||
};
|
||||
|
|
|
@ -2,13 +2,61 @@ local adns = require "net.adns";
|
|||
local inet_pton = require "util.net".pton;
|
||||
local inet_ntop = require "util.net".ntop;
|
||||
local idna_to_ascii = require "util.encodings".idna.to_ascii;
|
||||
local unpack = table.unpack or unpack; -- luacheck: ignore 113
|
||||
local promise = require "util.promise";
|
||||
local t_move = require "util.table".move;
|
||||
|
||||
local methods = {};
|
||||
local resolver_mt = { __index = methods };
|
||||
|
||||
-- FIXME RFC 6724
|
||||
|
||||
local function do_dns_lookup(self, dns_resolver, record_type, name, allow_insecure)
|
||||
return promise.new(function (resolve, reject)
|
||||
local ipv = (record_type == "A" and "4") or (record_type == "AAAA" and "6") or nil;
|
||||
if ipv and self.extra["use_ipv"..ipv] == false then
|
||||
return reject(("IPv%s disabled - %s lookup skipped"):format(ipv, record_type));
|
||||
elseif record_type == "TLSA" and self.extra.use_dane ~= true then
|
||||
return reject("DANE disabled - TLSA lookup skipped");
|
||||
end
|
||||
dns_resolver:lookup(function (answer, err)
|
||||
if not answer then
|
||||
return reject(err);
|
||||
elseif answer.bogus then
|
||||
return reject(("Validation error in %s lookup"):format(record_type));
|
||||
elseif not (answer.secure or allow_insecure) then
|
||||
return reject(("Insecure response in %s lookup"):format(record_type));
|
||||
elseif answer.status and #answer == 0 then
|
||||
return reject(("%s in %s lookup"):format(answer.status, record_type));
|
||||
end
|
||||
|
||||
local targets = { secure = answer.secure };
|
||||
for _, record in ipairs(answer) do
|
||||
if ipv then
|
||||
table.insert(targets, { self.conn_type..ipv, record[record_type:lower()], self.port, self.extra });
|
||||
else
|
||||
table.insert(targets, record[record_type:lower()]);
|
||||
end
|
||||
end
|
||||
return resolve(targets);
|
||||
end, name, record_type, "IN");
|
||||
end);
|
||||
end
|
||||
|
||||
local function merge_targets(ipv4_targets, ipv6_targets)
|
||||
local result = { secure = ipv4_targets.secure and ipv6_targets.secure };
|
||||
local common_length = math.min(#ipv4_targets, #ipv6_targets);
|
||||
for i = 1, common_length do
|
||||
table.insert(result, ipv6_targets[i]);
|
||||
table.insert(result, ipv4_targets[i]);
|
||||
end
|
||||
if common_length < #ipv4_targets then
|
||||
t_move(ipv4_targets, common_length+1, #ipv4_targets, common_length+1, result);
|
||||
elseif common_length < #ipv6_targets then
|
||||
t_move(ipv6_targets, common_length+1, #ipv6_targets, common_length+1, result);
|
||||
end
|
||||
return result;
|
||||
end
|
||||
|
||||
-- Find the next target to connect to, and
|
||||
-- pass it to cb()
|
||||
function methods:next(cb)
|
||||
|
@ -18,7 +66,7 @@ function methods:next(cb)
|
|||
return;
|
||||
end
|
||||
local next_target = table.remove(self.targets, 1);
|
||||
cb(unpack(next_target, 1, 4));
|
||||
cb(next_target[1], next_target[2], next_target[3], next_target[4], not not self.targets[1]);
|
||||
return;
|
||||
end
|
||||
|
||||
|
@ -28,91 +76,45 @@ function methods:next(cb)
|
|||
return;
|
||||
end
|
||||
|
||||
local secure = true;
|
||||
local tlsa = {};
|
||||
local targets = {};
|
||||
local n = 3;
|
||||
local function ready()
|
||||
n = n - 1;
|
||||
if n > 0 then return; end
|
||||
self.targets = targets;
|
||||
-- Resolve DNS to target list
|
||||
local dns_resolver = adns.resolver();
|
||||
|
||||
local dns_lookups = {
|
||||
ipv4 = do_dns_lookup(self, dns_resolver, "A", self.hostname, true);
|
||||
ipv6 = do_dns_lookup(self, dns_resolver, "AAAA", self.hostname, true);
|
||||
tlsa = do_dns_lookup(self, dns_resolver, "TLSA", ("_%d._%s.%s"):format(self.port, self.conn_type, self.hostname));
|
||||
};
|
||||
|
||||
promise.all_settled(dns_lookups):next(function (dns_results)
|
||||
-- Combine targets, assign to self.targets, self:next(cb)
|
||||
local have_ipv4 = dns_results.ipv4.status == "fulfilled";
|
||||
local have_ipv6 = dns_results.ipv6.status == "fulfilled";
|
||||
|
||||
if have_ipv4 and have_ipv6 then
|
||||
self.targets = merge_targets(dns_results.ipv4.value, dns_results.ipv6.value);
|
||||
elseif have_ipv4 then
|
||||
self.targets = dns_results.ipv4.value;
|
||||
elseif have_ipv6 then
|
||||
self.targets = dns_results.ipv6.value;
|
||||
else
|
||||
self.targets = {};
|
||||
end
|
||||
|
||||
if self.extra and self.extra.use_dane then
|
||||
if secure and tlsa[1] then
|
||||
self.extra.tlsa = tlsa;
|
||||
if self.targets.secure and dns_results.tlsa.status == "fulfilled" then
|
||||
self.extra.tlsa = dns_results.tlsa.value;
|
||||
self.extra.dane_hostname = self.hostname;
|
||||
else
|
||||
self.extra.tlsa = nil;
|
||||
self.extra.dane_hostname = nil;
|
||||
end
|
||||
end
|
||||
|
||||
self:next(cb);
|
||||
end
|
||||
|
||||
-- Resolve DNS to target list
|
||||
local dns_resolver = adns.resolver();
|
||||
|
||||
if not self.extra or self.extra.use_ipv4 ~= false then
|
||||
dns_resolver:lookup(function (answer, err)
|
||||
if answer then
|
||||
secure = secure and answer.secure;
|
||||
for _, record in ipairs(answer) do
|
||||
table.insert(targets, { self.conn_type.."4", record.a, self.port, self.extra });
|
||||
end
|
||||
if answer.bogus then
|
||||
self.last_error = "Validation error in A lookup";
|
||||
elseif answer.status then
|
||||
self.last_error = answer.status .. " in A lookup";
|
||||
end
|
||||
else
|
||||
self.last_error = err;
|
||||
end
|
||||
ready();
|
||||
end, self.hostname, "A", "IN");
|
||||
else
|
||||
ready();
|
||||
end
|
||||
|
||||
if not self.extra or self.extra.use_ipv6 ~= false then
|
||||
dns_resolver:lookup(function (answer, err)
|
||||
if answer then
|
||||
secure = secure and answer.secure;
|
||||
for _, record in ipairs(answer) do
|
||||
table.insert(targets, { self.conn_type.."6", record.aaaa, self.port, self.extra });
|
||||
end
|
||||
if answer.bogus then
|
||||
self.last_error = "Validation error in AAAA lookup";
|
||||
elseif answer.status then
|
||||
self.last_error = answer.status .. " in AAAA lookup";
|
||||
end
|
||||
else
|
||||
self.last_error = err;
|
||||
end
|
||||
ready();
|
||||
end, self.hostname, "AAAA", "IN");
|
||||
else
|
||||
ready();
|
||||
end
|
||||
|
||||
if self.extra and self.extra.use_dane == true then
|
||||
dns_resolver:lookup(function (answer, err)
|
||||
if answer then
|
||||
secure = secure and answer.secure;
|
||||
for _, record in ipairs(answer) do
|
||||
table.insert(tlsa, record.tlsa);
|
||||
end
|
||||
if answer.bogus then
|
||||
self.last_error = "Validation error in TLSA lookup";
|
||||
elseif answer.status then
|
||||
self.last_error = answer.status .. " in TLSA lookup";
|
||||
end
|
||||
else
|
||||
self.last_error = err;
|
||||
end
|
||||
ready();
|
||||
end, ("_%d._tcp.%s"):format(self.port, self.hostname), "TLSA", "IN");
|
||||
else
|
||||
ready();
|
||||
end
|
||||
end):catch(function (err)
|
||||
self.last_error = err;
|
||||
self.targets = {};
|
||||
end);
|
||||
end
|
||||
|
||||
local function new(hostname, port, conn_type, extra)
|
||||
|
@ -137,7 +139,7 @@ local function new(hostname, port, conn_type, extra)
|
|||
hostname = ascii_host;
|
||||
port = port;
|
||||
conn_type = conn_type;
|
||||
extra = extra;
|
||||
extra = extra or {};
|
||||
targets = targets;
|
||||
}, resolver_mt);
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
local methods = {};
|
||||
local resolver_mt = { __index = methods };
|
||||
local unpack = table.unpack or unpack; -- luacheck: ignore 113
|
||||
local unpack = table.unpack;
|
||||
|
||||
-- Find the next target to connect to, and
|
||||
-- pass it to cb()
|
||||
|
|
|
@ -2,23 +2,78 @@ local adns = require "net.adns";
|
|||
local basic = require "net.resolvers.basic";
|
||||
local inet_pton = require "util.net".pton;
|
||||
local idna_to_ascii = require "util.encodings".idna.to_ascii;
|
||||
local unpack = table.unpack or unpack; -- luacheck: ignore 113
|
||||
|
||||
local methods = {};
|
||||
local resolver_mt = { __index = methods };
|
||||
|
||||
local function new_target_selector(rrset)
|
||||
local rr_count = rrset and #rrset;
|
||||
if not rr_count or rr_count == 0 then
|
||||
rrset = nil;
|
||||
else
|
||||
table.sort(rrset, function (a, b) return a.srv.priority < b.srv.priority end);
|
||||
end
|
||||
local rrset_pos = 1;
|
||||
local priority_bucket, bucket_total_weight, bucket_len, bucket_used;
|
||||
return function ()
|
||||
if not rrset then return; end
|
||||
|
||||
if not priority_bucket or bucket_used >= bucket_len then
|
||||
if rrset_pos > rr_count then return; end -- Used up all records
|
||||
|
||||
-- Going to start on a new priority now. Gather up all the next
|
||||
-- records with the same priority and add them to priority_bucket
|
||||
priority_bucket, bucket_total_weight, bucket_len, bucket_used = {}, 0, 0, 0;
|
||||
local current_priority;
|
||||
repeat
|
||||
local curr_record = rrset[rrset_pos].srv;
|
||||
if not current_priority then
|
||||
current_priority = curr_record.priority;
|
||||
elseif current_priority ~= curr_record.priority then
|
||||
break;
|
||||
end
|
||||
table.insert(priority_bucket, curr_record);
|
||||
bucket_total_weight = bucket_total_weight + curr_record.weight;
|
||||
bucket_len = bucket_len + 1;
|
||||
rrset_pos = rrset_pos + 1;
|
||||
until rrset_pos > rr_count;
|
||||
end
|
||||
|
||||
bucket_used = bucket_used + 1;
|
||||
local n, running_total = math.random(0, bucket_total_weight), 0;
|
||||
local target_record;
|
||||
for i = 1, bucket_len do
|
||||
local candidate = priority_bucket[i];
|
||||
if candidate then
|
||||
running_total = running_total + candidate.weight;
|
||||
if running_total >= n then
|
||||
target_record = candidate;
|
||||
bucket_total_weight = bucket_total_weight - candidate.weight;
|
||||
priority_bucket[i] = nil;
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
return target_record;
|
||||
end;
|
||||
end
|
||||
|
||||
-- Find the next target to connect to, and
|
||||
-- pass it to cb()
|
||||
function methods:next(cb)
|
||||
if self.targets then
|
||||
if not self.resolver then
|
||||
if #self.targets == 0 then
|
||||
if self.resolver or self._get_next_target then
|
||||
if not self.resolver then -- Do we have a basic resolver currently?
|
||||
-- We don't, so fetch a new SRV target, create a new basic resolver for it
|
||||
local next_srv_target = self._get_next_target and self._get_next_target();
|
||||
if not next_srv_target then
|
||||
-- No more SRV targets left
|
||||
cb(nil);
|
||||
return;
|
||||
end
|
||||
local next_target = table.remove(self.targets, 1);
|
||||
self.resolver = basic.new(unpack(next_target, 1, 4));
|
||||
-- Create a new basic resolver for this SRV target
|
||||
self.resolver = basic.new(next_srv_target.target, next_srv_target.port, self.conn_type, self.extra);
|
||||
end
|
||||
-- Look up the next (basic) target from the current target's resolver
|
||||
self.resolver:next(function (...)
|
||||
if self.resolver then
|
||||
self.last_error = self.resolver.last_error;
|
||||
|
@ -31,6 +86,9 @@ function methods:next(cb)
|
|||
end
|
||||
end);
|
||||
return;
|
||||
elseif self.in_progress then
|
||||
cb(nil);
|
||||
return;
|
||||
end
|
||||
|
||||
if not self.hostname then
|
||||
|
@ -39,9 +97,9 @@ function methods:next(cb)
|
|||
return;
|
||||
end
|
||||
|
||||
local targets = {};
|
||||
self.in_progress = true;
|
||||
|
||||
local function ready()
|
||||
self.targets = targets;
|
||||
self:next(cb);
|
||||
end
|
||||
|
||||
|
@ -63,7 +121,7 @@ function methods:next(cb)
|
|||
|
||||
if #answer == 0 then
|
||||
if self.extra and self.extra.default_port then
|
||||
table.insert(targets, { self.hostname, self.extra.default_port, self.conn_type, self.extra });
|
||||
self.resolver = basic.new(self.hostname, self.extra.default_port, self.conn_type, self.extra);
|
||||
else
|
||||
self.last_error = "zero SRV records found";
|
||||
end
|
||||
|
@ -77,10 +135,7 @@ function methods:next(cb)
|
|||
return;
|
||||
end
|
||||
|
||||
table.sort(answer, function (a, b) return a.srv.priority < b.srv.priority end);
|
||||
for _, record in ipairs(answer) do
|
||||
table.insert(targets, { record.srv.target, record.srv.port, self.conn_type, self.extra });
|
||||
end
|
||||
self._get_next_target = new_target_selector(answer);
|
||||
else
|
||||
self.last_error = err;
|
||||
end
|
||||
|
|
|
@ -118,6 +118,13 @@ if prosody and set_config then
|
|||
prosody.events.add_handler("config-reloaded", load_config);
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
-- require "net.server" shall now forever return this,
|
||||
-- ie. server_select or server_event as chosen above.
|
||||
return server;
|
||||
|
|
|
@ -18,7 +18,6 @@ local traceback = debug.traceback;
|
|||
local logger = require "util.logger";
|
||||
local log = logger.init("server_epoll");
|
||||
local socket = require "socket";
|
||||
local luasec = require "ssl";
|
||||
local realtime = require "util.time".now;
|
||||
local monotonic = require "util.time".monotonic;
|
||||
local indexedbheap = require "util.indexedbheap";
|
||||
|
@ -28,6 +27,8 @@ local inet_pton = inet.pton;
|
|||
local _SOCKETINVALID = socket._SOCKETINVALID or -1;
|
||||
local new_id = require "util.id".short;
|
||||
local xpcall = require "util.xpcall".xpcall;
|
||||
local sslconfig = require "util.sslconfig";
|
||||
local tls_impl = require "net.tls_luasec";
|
||||
|
||||
local poller = require "util.poll"
|
||||
local EEXIST = poller.EEXIST;
|
||||
|
@ -91,6 +92,12 @@ local default_config = { __index = {
|
|||
|
||||
--- How long to wait after getting the shutdown signal before forcefully tearing down every socket
|
||||
shutdown_deadline = 5;
|
||||
|
||||
-- TCP Fast Open
|
||||
tcp_fastopen = false;
|
||||
|
||||
-- Defer accept until incoming data is available
|
||||
tcp_defer_accept = false;
|
||||
}};
|
||||
local cfg = default_config.__index;
|
||||
|
||||
|
@ -614,6 +621,42 @@ 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.info then return nil, "not-implemented"; end
|
||||
return sock:info();
|
||||
end
|
||||
|
||||
function interface:ssl_peercertificate()
|
||||
local sock = self.conn;
|
||||
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.getpeerverification then return nil, { { "Chain verification not supported" } }; end
|
||||
return sock:getpeerverification();
|
||||
end
|
||||
|
||||
function interface:ssl_peerfinished()
|
||||
local sock = self.conn;
|
||||
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 sock.exportkeyingmaterial then
|
||||
return sock:exportkeyingmaterial(label, len, context);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function interface:starttls(tls_ctx)
|
||||
if tls_ctx then self.tls_ctx = tls_ctx; end
|
||||
self.starttls = false;
|
||||
|
@ -641,11 +684,7 @@ function interface:inittls(tls_ctx, now)
|
|||
self.starttls = false;
|
||||
self:debug("Starting TLS now");
|
||||
self:updatenames(); -- Can't getpeer/sockname after wrap()
|
||||
local ok, conn, err = pcall(luasec.wrap, self.conn, self.tls_ctx);
|
||||
if not ok then
|
||||
conn, err = ok, conn;
|
||||
self:debug("Failed to initialize TLS: %s", err);
|
||||
end
|
||||
local conn, err = self.tls_ctx:wrap(self.conn);
|
||||
if not conn then
|
||||
self:on("disconnect", err);
|
||||
self:destroy();
|
||||
|
@ -656,8 +695,8 @@ function interface:inittls(tls_ctx, now)
|
|||
if conn.sni then
|
||||
if self.servername then
|
||||
conn:sni(self.servername);
|
||||
elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then
|
||||
conn:sni(self._server.hosts, true);
|
||||
elseif next(self.tls_ctx._sni_contexts) ~= nil then
|
||||
conn:sni(self.tls_ctx._sni_contexts, true);
|
||||
end
|
||||
end
|
||||
if self.extra and self.extra.tlsa and conn.settlsa then
|
||||
|
@ -741,7 +780,6 @@ local function wrapsocket(client, server, read_size, listeners, tls_ctx, extra)
|
|||
end
|
||||
end
|
||||
|
||||
conn:updatenames();
|
||||
return conn;
|
||||
end
|
||||
|
||||
|
@ -767,6 +805,7 @@ function interface:onacceptable()
|
|||
return;
|
||||
end
|
||||
local client = wrapsocket(conn, self, nil, self.listeners);
|
||||
client:updatenames();
|
||||
client:debug("New connection %s on server %s", client, self);
|
||||
client:defaultoptions();
|
||||
client._writable = cfg.opportunistic_writes;
|
||||
|
@ -885,6 +924,12 @@ local function wrapserver(conn, addr, port, listeners, config)
|
|||
log = logger.init(("serv%s"):format(new_id()));
|
||||
}, interface_mt);
|
||||
server:debug("Server %s created", server);
|
||||
if cfg.tcp_fastopen then
|
||||
server:setoption("tcp-fastopen", cfg.tcp_fastopen);
|
||||
end
|
||||
if type(cfg.tcp_defer_accept) == "number" then
|
||||
server:setoption("tcp-defer-accept", cfg.tcp_defer_accept);
|
||||
end
|
||||
server:add(true, false);
|
||||
return server;
|
||||
end
|
||||
|
@ -908,6 +953,7 @@ end
|
|||
-- COMPAT
|
||||
local function wrapclient(conn, addr, port, listeners, read_size, tls_ctx, extra)
|
||||
local client = wrapsocket(conn, nil, read_size, listeners, tls_ctx, extra);
|
||||
client:updatenames();
|
||||
if not client.peername then
|
||||
client.peername, client.peerport = addr, port;
|
||||
end
|
||||
|
@ -941,9 +987,13 @@ local function addclient(addr, port, listeners, read_size, tls_ctx, typ, extra)
|
|||
if not conn then return conn, err; end
|
||||
local ok, err = conn:settimeout(0);
|
||||
if not ok then return ok, err; end
|
||||
local client = wrapsocket(conn, nil, read_size, listeners, tls_ctx, extra)
|
||||
if cfg.tcp_fastopen then
|
||||
client:setoption("tcp-fastopen-connect", 1);
|
||||
end
|
||||
local ok, err = conn:setpeername(addr, port);
|
||||
if not ok and err ~= "timeout" then return ok, err; end
|
||||
local client = wrapsocket(conn, nil, read_size, listeners, tls_ctx, extra)
|
||||
client:updatenames();
|
||||
local ok, err = client:init();
|
||||
if not client.peername then
|
||||
-- otherwise not set until connected
|
||||
|
@ -1085,6 +1135,10 @@ return {
|
|||
cfg = setmetatable(newconfig, default_config);
|
||||
end;
|
||||
|
||||
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)
|
||||
|
|
|
@ -47,11 +47,13 @@ local s_sub = string.sub
|
|||
local coroutine_wrap = coroutine.wrap
|
||||
local coroutine_yield = coroutine.yield
|
||||
|
||||
local has_luasec, ssl = pcall ( require , "ssl" )
|
||||
local has_luasec = pcall ( require , "ssl" )
|
||||
local socket = require "socket"
|
||||
local levent = require "luaevent.core"
|
||||
local inet = require "util.net";
|
||||
local inet_pton = inet.pton;
|
||||
local sslconfig = require "util.sslconfig";
|
||||
local tls_impl = require "net.tls_luasec";
|
||||
|
||||
local socket_gettime = socket.gettime
|
||||
|
||||
|
@ -153,7 +155,7 @@ function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed
|
|||
_ = self.eventwrite and self.eventwrite:close( )
|
||||
self.eventread, self.eventwrite = nil, nil
|
||||
local err
|
||||
self.conn, err = ssl.wrap( self.conn, self._sslctx )
|
||||
self.conn, err = self._sslctx:wrap(self.conn)
|
||||
if err then
|
||||
self.fatalerror = err
|
||||
self.conn = nil -- cannot be used anymore
|
||||
|
@ -168,8 +170,8 @@ function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed
|
|||
if self.conn.sni then
|
||||
if self.servername then
|
||||
self.conn:sni(self.servername);
|
||||
elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then
|
||||
self.conn:sni(self._server.hosts, true);
|
||||
elseif next(self._sslctx._sni_contexts) ~= nil then
|
||||
self.conn:sni(self._sslctx._sni_contexts, true);
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -274,6 +276,34 @@ function interface_mt:pause()
|
|||
return self:_lock(self.nointerface, true, self.nowriting);
|
||||
end
|
||||
|
||||
function interface_mt:sslctx()
|
||||
return self._sslctx
|
||||
end
|
||||
|
||||
function interface_mt:ssl_info()
|
||||
local sock = self.conn;
|
||||
if not sock.info then return nil, "not-implemented"; end
|
||||
return sock:info();
|
||||
end
|
||||
|
||||
function interface_mt:ssl_peercertificate()
|
||||
local sock = self.conn;
|
||||
if not sock.getpeercertificate then return nil, "not-implemented"; end
|
||||
return sock:getpeercertificate();
|
||||
end
|
||||
|
||||
function interface_mt:ssl_peerverification()
|
||||
local sock = self.conn;
|
||||
if not sock.getpeerverification then return nil, { { "Chain verification not supported" } }; end
|
||||
return sock:getpeerverification();
|
||||
end
|
||||
|
||||
function interface_mt:ssl_peerfinished()
|
||||
local sock = self.conn;
|
||||
if not sock.getpeerfinished then return nil, "not-implemented"; end
|
||||
return sock:getpeerfinished();
|
||||
end
|
||||
|
||||
function interface_mt:resume()
|
||||
self:_lock(self.nointerface, false, self.nowriting);
|
||||
if self.readcallback and not self.eventread then
|
||||
|
@ -924,6 +954,10 @@ return {
|
|||
add_task = add_task,
|
||||
watchfd = watchfd,
|
||||
|
||||
tls_builder = function(basedir)
|
||||
return sslconfig._new(tls_impl.new_context, basedir)
|
||||
end,
|
||||
|
||||
__NAME = SCRIPT_NAME,
|
||||
__DATE = LAST_MODIFIED,
|
||||
__AUTHOR = SCRIPT_AUTHOR,
|
||||
|
|
|
@ -47,15 +47,15 @@ local coroutine_yield = coroutine.yield
|
|||
|
||||
--// extern libs //--
|
||||
|
||||
local has_luasec, luasec = pcall ( require , "ssl" )
|
||||
local luasocket = use "socket" or require "socket"
|
||||
local luasocket_gettime = luasocket.gettime
|
||||
local inet = require "util.net";
|
||||
local inet_pton = inet.pton;
|
||||
local sslconfig = require "util.sslconfig";
|
||||
local has_luasec, tls_impl = pcall(require, "net.tls_luasec");
|
||||
|
||||
--// extern lib methods //--
|
||||
|
||||
local ssl_wrap = ( has_luasec and luasec.wrap )
|
||||
local socket_bind = luasocket.bind
|
||||
local socket_select = luasocket.select
|
||||
|
||||
|
@ -359,6 +359,21 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport
|
|||
handler.sslctx = function ( )
|
||||
return sslctx
|
||||
end
|
||||
handler.ssl_info = function( )
|
||||
return socket.info and socket:info()
|
||||
end
|
||||
handler.ssl_peercertificate = function( )
|
||||
if not socket.getpeercertificate then return nil, "not-implemented"; end
|
||||
return socket:getpeercertificate()
|
||||
end
|
||||
handler.ssl_peerverification = function( )
|
||||
if not socket.getpeerverification then return nil, { { "Chain verification not supported" } }; end
|
||||
return socket:getpeerverification();
|
||||
end
|
||||
handler.ssl_peerfinished = function( )
|
||||
if not socket.getpeerfinished then return nil, "not-implemented"; end
|
||||
return socket:getpeerfinished();
|
||||
end
|
||||
handler.send = function( _, data, i, j )
|
||||
return send( socket, data, i, j )
|
||||
end
|
||||
|
@ -652,7 +667,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport
|
|||
end
|
||||
out_put( "server.lua: attempting to start tls on " .. tostring( socket ) )
|
||||
local oldsocket, err = socket
|
||||
socket, err = ssl_wrap( socket, sslctx ) -- wrap socket
|
||||
socket, err = sslctx:wrap(socket) -- wrap socket
|
||||
|
||||
if not socket then
|
||||
out_put( "server.lua: error while starting tls on client: ", tostring(err or "unknown error") )
|
||||
|
@ -662,8 +677,8 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport
|
|||
if socket.sni then
|
||||
if self.servername then
|
||||
socket:sni(self.servername);
|
||||
elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then
|
||||
socket:sni(self.server().hosts, true);
|
||||
elseif next(sslctx._sni_contexts) ~= nil then
|
||||
socket:sni(sslctx._sni_contexts, true);
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1169,4 +1184,8 @@ return {
|
|||
removeserver = removeserver,
|
||||
get_backend = get_backend,
|
||||
changesettings = changesettings,
|
||||
|
||||
tls_builder = function(basedir)
|
||||
return sslconfig._new(tls_impl.new_context, basedir)
|
||||
end,
|
||||
}
|
||||
|
|
89
net/tls_luasec.lua
Normal file
89
net/tls_luasec.lua
Normal file
|
@ -0,0 +1,89 @@
|
|||
-- Prosody IM
|
||||
-- Copyright (C) 2021 Prosody folks
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
|
||||
--[[
|
||||
This file provides a shim abstraction over LuaSec, consolidating some code
|
||||
which was previously spread between net.server backends, portmanager and
|
||||
certmanager.
|
||||
|
||||
The goal is to provide a more or less well-defined API on top of LuaSec which
|
||||
abstracts away some of the things which are not needed and simplifies usage of
|
||||
commonly used things (such as SNI contexts). Eventually, network backends
|
||||
which do not rely on LuaSocket+LuaSec should be able to provide *this* API
|
||||
instead of having to mimic LuaSec.
|
||||
]]
|
||||
local ssl = require "ssl";
|
||||
local ssl_newcontext = ssl.newcontext;
|
||||
local ssl_context = ssl.context or require "ssl.context";
|
||||
local io_open = io.open;
|
||||
|
||||
local context_api = {};
|
||||
local context_mt = {__index = context_api};
|
||||
|
||||
function context_api:set_sni_host(host, cert, key)
|
||||
local ctx, err = self._builder:clone():apply({
|
||||
certificate = cert,
|
||||
key = key,
|
||||
}):build();
|
||||
if not ctx then
|
||||
return false, err
|
||||
end
|
||||
|
||||
self._sni_contexts[host] = ctx._inner
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
function context_api:remove_sni_host(host)
|
||||
self._sni_contexts[host] = nil
|
||||
end
|
||||
|
||||
function context_api:wrap(sock)
|
||||
local ok, conn, err = pcall(ssl.wrap, sock, self._inner);
|
||||
if not ok then
|
||||
return nil, err
|
||||
end
|
||||
return conn, nil
|
||||
end
|
||||
|
||||
local function new_context(cfg, builder)
|
||||
-- LuaSec expects dhparam to be a callback that takes two arguments.
|
||||
-- We ignore those because it is mostly used for having a separate
|
||||
-- set of params for EXPORT ciphers, which we don't have by default.
|
||||
if type(cfg.dhparam) == "string" then
|
||||
local f, err = io_open(cfg.dhparam);
|
||||
if not f then return nil, "Could not open DH parameters: "..err end
|
||||
local dhparam = f:read("*a");
|
||||
f:close();
|
||||
cfg.dhparam = function() return dhparam; end
|
||||
end
|
||||
|
||||
local inner, err = ssl_newcontext(cfg);
|
||||
if not inner then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
-- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care
|
||||
-- of it ourselves (W/A for #x)
|
||||
if inner and cfg.ciphers then
|
||||
local success;
|
||||
success, err = ssl_context.setcipher(inner, cfg.ciphers);
|
||||
if not success then
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
|
||||
return setmetatable({
|
||||
_inner = inner,
|
||||
_builder = builder,
|
||||
_sni_contexts = {},
|
||||
}, context_mt), nil
|
||||
end
|
||||
|
||||
return {
|
||||
new_context = new_context,
|
||||
};
|
|
@ -23,10 +23,16 @@ end
|
|||
function _M.new(name, node, handler, permission)
|
||||
if not permission then
|
||||
error "adhoc.new() expects a permission argument, none given"
|
||||
end
|
||||
if permission == "user" then
|
||||
elseif permission == "user" then
|
||||
error "the permission mode 'user' has been renamed 'any', please update your code"
|
||||
end
|
||||
if permission == "admin" then
|
||||
module:default_permission("prosody:admin", "mod_adhoc:"..node);
|
||||
permission = "check";
|
||||
elseif permission == "global_admin" then
|
||||
module:default_permission("prosody:operator", "mod_adhoc:"..node);
|
||||
permission = "check";
|
||||
end
|
||||
return { name = name, node = node, handler = handler, cmdtag = _cmdtag, permission = permission };
|
||||
end
|
||||
|
||||
|
@ -34,6 +40,8 @@ function _M.handle_cmd(command, origin, stanza)
|
|||
local cmdtag = stanza.tags[1]
|
||||
local sessionid = cmdtag.attr.sessionid or uuid.generate();
|
||||
local dataIn = {
|
||||
origin = origin;
|
||||
stanza = stanza;
|
||||
to = stanza.attr.to;
|
||||
from = stanza.attr.from;
|
||||
action = cmdtag.attr.action or "execute";
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
local it = require "util.iterators";
|
||||
local st = require "util.stanza";
|
||||
local is_admin = require "core.usermanager".is_admin;
|
||||
local jid_host = require "util.jid".host;
|
||||
local adhoc_handle_cmd = module:require "adhoc".handle_cmd;
|
||||
local xmlns_cmd = "http://jabber.org/protocol/commands";
|
||||
|
@ -15,18 +14,17 @@ local commands = {};
|
|||
|
||||
module:add_feature(xmlns_cmd);
|
||||
|
||||
local function check_permissions(event, node, command)
|
||||
return (command.permission == "check" and module:may("mod_adhoc:"..node, event))
|
||||
or (command.permission == "local_user" and jid_host(event.stanza.attr.from) == module.host)
|
||||
or (command.permission == "any");
|
||||
end
|
||||
|
||||
module:hook("host-disco-info-node", function (event)
|
||||
local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
|
||||
if commands[node] then
|
||||
local from = stanza.attr.from;
|
||||
local privileged = is_admin(from, stanza.attr.to);
|
||||
local global_admin = is_admin(from);
|
||||
local hostname = jid_host(from);
|
||||
local command = commands[node];
|
||||
if (command.permission == "admin" and privileged)
|
||||
or (command.permission == "global_admin" and global_admin)
|
||||
or (command.permission == "local_user" and hostname == module.host)
|
||||
or (command.permission == "any") then
|
||||
if check_permissions(event, node, command) then
|
||||
reply:tag("identity", { name = command.name,
|
||||
category = "automation", type = "command-node" }):up();
|
||||
reply:tag("feature", { var = xmlns_cmd }):up();
|
||||
|
@ -44,20 +42,13 @@ module:hook("host-disco-info-node", function (event)
|
|||
end);
|
||||
|
||||
module:hook("host-disco-items-node", function (event)
|
||||
local stanza, reply, disco_node = event.stanza, event.reply, event.node;
|
||||
local reply, disco_node = event.reply, event.node;
|
||||
if disco_node ~= xmlns_cmd then
|
||||
return;
|
||||
end
|
||||
|
||||
local from = stanza.attr.from;
|
||||
local admin = is_admin(from, stanza.attr.to);
|
||||
local global_admin = is_admin(from);
|
||||
local hostname = jid_host(from);
|
||||
for node, command in it.sorted_pairs(commands) do
|
||||
if (command.permission == "admin" and admin)
|
||||
or (command.permission == "global_admin" and global_admin)
|
||||
or (command.permission == "local_user" and hostname == module.host)
|
||||
or (command.permission == "any") then
|
||||
if check_permissions(event, node, command) then
|
||||
reply:tag("item", { name = command.name,
|
||||
node = node, jid = module:get_host() });
|
||||
reply:up();
|
||||
|
@ -71,20 +62,14 @@ module:hook("iq-set/host/"..xmlns_cmd..":command", function (event)
|
|||
local node = stanza.tags[1].attr.node
|
||||
local command = commands[node];
|
||||
if command then
|
||||
local from = stanza.attr.from;
|
||||
local admin = is_admin(from, stanza.attr.to);
|
||||
local global_admin = is_admin(from);
|
||||
local hostname = jid_host(from);
|
||||
if (command.permission == "admin" and not admin)
|
||||
or (command.permission == "global_admin" and not global_admin)
|
||||
or (command.permission == "local_user" and hostname ~= module.host) then
|
||||
if not check_permissions(event, node, command) then
|
||||
origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up()
|
||||
:add_child(commands[node]:cmdtag("canceled")
|
||||
:add_child(command:cmdtag("canceled")
|
||||
:tag("note", {type="error"}):text("You don't have permission to execute this command")));
|
||||
return true
|
||||
end
|
||||
-- User has permission now execute the command
|
||||
adhoc_handle_cmd(commands[node], origin, stanza);
|
||||
adhoc_handle_cmd(command, origin, stanza);
|
||||
return true;
|
||||
end
|
||||
end, 500);
|
||||
|
|
|
@ -22,7 +22,7 @@ local _G = _G;
|
|||
|
||||
local prosody = _G.prosody;
|
||||
|
||||
local unpack = table.unpack or unpack; -- luacheck: ignore 113
|
||||
local unpack = table.unpack;
|
||||
local iterators = require "util.iterators";
|
||||
local keys, values = iterators.keys, iterators.values;
|
||||
local jid_bare, jid_split, jid_join = import("util.jid", "bare", "prepped_split", "join");
|
||||
|
@ -36,6 +36,7 @@ local serialization = require "util.serialization";
|
|||
local serialize_config = serialization.new ({ fatal = false, unquoted = true});
|
||||
local time = require "util.time";
|
||||
local promise = require "util.promise";
|
||||
local logger = require "util.logger";
|
||||
|
||||
local t_insert = table.insert;
|
||||
local t_concat = table.concat;
|
||||
|
@ -83,8 +84,8 @@ function runner_callbacks:error(err)
|
|||
self.data.print("Error: "..tostring(err));
|
||||
end
|
||||
|
||||
local function send_repl_output(session, line)
|
||||
return session.send(st.stanza("repl-output"):text(tostring(line)));
|
||||
local function send_repl_output(session, line, attr)
|
||||
return session.send(st.stanza("repl-output", attr):text(tostring(line)));
|
||||
end
|
||||
|
||||
function console:new_session(admin_session)
|
||||
|
@ -99,8 +100,14 @@ function console:new_session(admin_session)
|
|||
end
|
||||
return send_repl_output(admin_session, table.concat(t, "\t"));
|
||||
end;
|
||||
write = function (t)
|
||||
return send_repl_output(admin_session, t, { eol = "0" });
|
||||
end;
|
||||
serialize = tostring;
|
||||
disconnect = function () admin_session:close(); end;
|
||||
is_connected = function ()
|
||||
return not not admin_session.conn;
|
||||
end
|
||||
};
|
||||
session.env = setmetatable({}, default_env_mt);
|
||||
|
||||
|
@ -126,6 +133,11 @@ local function handle_line(event)
|
|||
session = console:new_session(event.origin);
|
||||
event.origin.shell_session = session;
|
||||
end
|
||||
|
||||
local default_width = 132; -- The common default of 80 is a bit too narrow for e.g. s2s:show(), 132 was another common width for hardware terminals
|
||||
local margin = 2; -- To account for '| ' when lines are printed
|
||||
session.width = (tonumber(event.stanza.attr.width) or default_width)-margin;
|
||||
|
||||
local line = event.stanza:get_text();
|
||||
local useglobalenv;
|
||||
|
||||
|
@ -212,7 +224,7 @@ function commands.help(session, data)
|
|||
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%" } })
|
||||
local row = format_table({ { title = "Section", width = 7 }, { title = "Description", width = "100%" } }, session.width)
|
||||
print(row())
|
||||
print(row { "c2s"; "Commands to manage local client-to-server sessions" })
|
||||
print(row { "s2s"; "Commands to manage sessions between this server and others" })
|
||||
|
@ -228,6 +240,7 @@ function commands.help(session, data)
|
|||
print(row { "dns"; "Commands to manage and inspect the internal DNS resolver" })
|
||||
print(row { "xmpp"; "Commands for sending XMPP stanzas" })
|
||||
print(row { "debug"; "Commands for debugging the server" })
|
||||
print(row { "watch"; "Commands for watching live logs from the server" })
|
||||
print(row { "config"; "Reloading the configuration, etc." })
|
||||
print(row { "columns"; "Information about customizing session listings" })
|
||||
print(row { "console"; "Help regarding the console itself" })
|
||||
|
@ -255,23 +268,22 @@ function commands.help(session, data)
|
|||
print [[host:deactivate(hostname) - Disconnects all clients on this host and deactivates]]
|
||||
print [[host:list() - List the currently-activated hosts]]
|
||||
elseif section == "user" then
|
||||
print [[user:create(jid, password, roles) - Create the specified user account]]
|
||||
print [[user:create(jid, password, role) - Create the specified user account]]
|
||||
print [[user:password(jid, password) - Set the password for the specified user account]]
|
||||
print [[user:roles(jid, host) - Show current roles for an user]]
|
||||
print [[user:setroles(jid, host, roles) - Set roles for an user (see 'help roles')]]
|
||||
print [[user:setrole(jid, host, role) - Set primary role of a user (see 'help roles')]]
|
||||
print [[user:addrole(jid, host, role) - Add a secondary role to a user]]
|
||||
print [[user:delrole(jid, host, role) - Remove a secondary role from a user]]
|
||||
print [[user:delete(jid) - Permanently remove the specified user account]]
|
||||
print [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]]
|
||||
elseif section == "roles" then
|
||||
print [[Roles may grant access or restrict users from certain operations]]
|
||||
print [[Built-in roles are:]]
|
||||
print [[ prosody:admin - Administrator]]
|
||||
print [[ (empty set) - Normal user]]
|
||||
print [[ prosody:user - Normal user (default)]]
|
||||
print [[ prosody:admin - Host administrator]]
|
||||
print [[ prosody:operator - Server administrator]]
|
||||
print [[]]
|
||||
print [[The canonical role format looks like: { ["example:role"] = true }]]
|
||||
print [[For convenience, the following formats are also accepted:]]
|
||||
print [["admin" - short for "prosody:admin", the normal admin status (like the admins config option)]]
|
||||
print [["example:role" - short for {["example:role"]=true}]]
|
||||
print [[{"example:role"} - short for {["example:role"]=true}]]
|
||||
print [[Roles can be assigned using the user management commands (see 'help user').]]
|
||||
elseif section == "muc" then
|
||||
-- TODO `muc:room():foo()` commands
|
||||
print [[muc:create(roomjid, { config }) - Create the specified MUC room with the given config]]
|
||||
|
@ -304,6 +316,9 @@ function commands.help(session, data)
|
|||
print [[debug:logevents(host) - Enable logging of fired events on host]]
|
||||
print [[debug:events(host, event) - Show registered event handlers]]
|
||||
print [[debug:timers() - Show information about scheduled timers]]
|
||||
elseif section == "watch" then
|
||||
print [[watch:log() - Follow debug logs]]
|
||||
print [[watch:stanzas(target, filter) - Watch live stanzas matching the specified target and filter]]
|
||||
elseif section == "console" then
|
||||
print [[Hey! Welcome to Prosody's admin console.]]
|
||||
print [[First thing, if you're ever wondering how to get out, simply type 'quit'.]]
|
||||
|
@ -334,7 +349,7 @@ function commands.help(session, data)
|
|||
meta_columns[2].width = math.max(meta_columns[2].width or 0, #(spec.title or ""));
|
||||
meta_columns[3].width = math.max(meta_columns[3].width or 0, #(spec.description or ""));
|
||||
end
|
||||
local row = format_table(meta_columns, 120)
|
||||
local row = format_table(meta_columns, session.width)
|
||||
print(row());
|
||||
for column, spec in iterators.sorted_pairs(available_columns) do
|
||||
print(row({ column, spec.title, spec.description }));
|
||||
|
@ -480,6 +495,16 @@ function def_env.module:info(name, hosts)
|
|||
|
||||
local function item_name(item) return item.name; end
|
||||
|
||||
local function task_timefmt(t)
|
||||
if not t then
|
||||
return "no last run time"
|
||||
elseif os.difftime(os.time(), t) < 86400 then
|
||||
return os.date("last run today at %H:%M", t);
|
||||
else
|
||||
return os.date("last run %A at %H:%M", t);
|
||||
end
|
||||
end
|
||||
|
||||
local friendly_descriptions = {
|
||||
["adhoc-provider"] = "Ad-hoc commands",
|
||||
["auth-provider"] = "Authentication provider",
|
||||
|
@ -497,12 +522,22 @@ function def_env.module:info(name, hosts)
|
|||
["auth-provider"] = item_name,
|
||||
["storage-provider"] = item_name,
|
||||
["http-provider"] = function(item, mod) return mod:http_url(item.name, item.default_path); end,
|
||||
["net-provider"] = item_name,
|
||||
["net-provider"] = function(item)
|
||||
local service_name = item.name;
|
||||
local ports_list = {};
|
||||
for _, interface, port in portmanager.get_active_services():iter(service_name, nil, nil) do
|
||||
table.insert(ports_list, "["..interface.."]:"..port);
|
||||
end
|
||||
if not ports_list[1] then
|
||||
return service_name..": not listening on any ports";
|
||||
end
|
||||
return service_name..": "..table.concat(ports_list, ", ");
|
||||
end,
|
||||
["measure"] = function(item) return item.name .. " (" .. suf(item.conf and item.conf.unit, " ") .. item.type .. ")"; end,
|
||||
["metric"] = function(item)
|
||||
return ("%s (%s%s)%s"):format(item.name, suf(item.mf.unit, " "), item.mf.type_, pre(": ", item.mf.description));
|
||||
end,
|
||||
["task"] = function (item) return string.format("%s (%s)", item.name or item.id, item.when); end
|
||||
["task"] = function (item) return string.format("%s (%s, %s)", item.name or item.id, item.when, task_timefmt(item.last)); end
|
||||
};
|
||||
|
||||
for host in hosts do
|
||||
|
@ -539,14 +574,14 @@ function def_env.module:info(name, hosts)
|
|||
return true;
|
||||
end
|
||||
|
||||
function def_env.module:load(name, hosts, config)
|
||||
function def_env.module:load(name, hosts)
|
||||
hosts = get_hosts_with_module(hosts);
|
||||
|
||||
-- Load the module for each host
|
||||
local ok, err, count, mod = true, nil, 0;
|
||||
for host in hosts do
|
||||
if (not modulemanager.is_loaded(host, name)) then
|
||||
mod, err = modulemanager.load(host, name, config);
|
||||
mod, err = modulemanager.load(host, name);
|
||||
if not mod then
|
||||
ok = false;
|
||||
if err == "global-module-already-loaded" then
|
||||
|
@ -804,9 +839,7 @@ available_columns = {
|
|||
mapper = function(conn, session)
|
||||
if not session.secure then return "insecure"; end
|
||||
if not conn or not conn:ssl() then return "secure" end
|
||||
local sock = conn and conn:socket();
|
||||
if not sock then return "secure"; end
|
||||
local tls_info = sock.info and sock:info();
|
||||
local tls_info = conn.ssl_info and conn:ssl_info();
|
||||
return tls_info and tls_info.protocol or "secure";
|
||||
end;
|
||||
};
|
||||
|
@ -816,8 +849,7 @@ available_columns = {
|
|||
width = 30;
|
||||
key = "conn";
|
||||
mapper = function(conn)
|
||||
local sock = conn and conn:socket();
|
||||
local info = sock and sock.info and sock:info();
|
||||
local info = conn and conn.ssl_info and conn:ssl_info();
|
||||
if info then return info.cipher end
|
||||
end;
|
||||
};
|
||||
|
@ -914,6 +946,15 @@ available_columns = {
|
|||
end
|
||||
end
|
||||
};
|
||||
role = {
|
||||
title = "Role";
|
||||
description = "Session role";
|
||||
width = 20;
|
||||
key = "role";
|
||||
mapper = function(role)
|
||||
return role and role.name;
|
||||
end;
|
||||
}
|
||||
};
|
||||
|
||||
local function get_colspec(colspec, default)
|
||||
|
@ -934,8 +975,8 @@ end
|
|||
|
||||
function def_env.c2s:show(match_jid, colspec)
|
||||
local print = self.session.print;
|
||||
local columns = get_colspec(colspec, { "id"; "jid"; "ipv"; "status"; "secure"; "smacks"; "csi" });
|
||||
local row = format_table(columns, 120);
|
||||
local columns = get_colspec(colspec, { "id"; "jid"; "role"; "ipv"; "status"; "secure"; "smacks"; "csi" });
|
||||
local row = format_table(columns, self.session.width);
|
||||
|
||||
local function match(session)
|
||||
local jid = get_jid(session)
|
||||
|
@ -1018,7 +1059,7 @@ end
|
|||
function def_env.s2s:show(match_jid, colspec)
|
||||
local print = self.session.print;
|
||||
local columns = get_colspec(colspec, { "id"; "host"; "dir"; "remote"; "ipv"; "secure"; "s2s_sasl"; "dialback" });
|
||||
local row = format_table(columns, 132);
|
||||
local row = format_table(columns, self.session.width);
|
||||
|
||||
local function match(session)
|
||||
local host, remote = get_s2s_hosts(session);
|
||||
|
@ -1228,18 +1269,18 @@ end
|
|||
function def_env.host:list()
|
||||
local print = self.session.print;
|
||||
local i = 0;
|
||||
local type;
|
||||
local host_type;
|
||||
for host, host_session in iterators.sorted_pairs(prosody.hosts, _sort_hosts) do
|
||||
i = i + 1;
|
||||
type = host_session.type;
|
||||
if type == "local" then
|
||||
host_type = host_session.type;
|
||||
if host_type == "local" then
|
||||
print(host);
|
||||
else
|
||||
type = module:context(host):get_option_string("component_module", type);
|
||||
if type ~= "component" then
|
||||
type = type .. " component";
|
||||
host_type = module:context(host):get_option_string("component_module", host_type);
|
||||
if host_type ~= "component" then
|
||||
host_type = host_type .. " component";
|
||||
end
|
||||
print(("%s (%s)"):format(host, type));
|
||||
print(("%s (%s)"):format(host, host_type));
|
||||
end
|
||||
end
|
||||
return true, i.." hosts";
|
||||
|
@ -1345,32 +1386,32 @@ end
|
|||
|
||||
local um = require"core.usermanager";
|
||||
|
||||
local function coerce_roles(roles)
|
||||
if roles == "admin" then roles = "prosody:admin"; end
|
||||
if type(roles) == "string" then roles = { [roles] = true }; end
|
||||
if roles[1] then for i, role in ipairs(roles) do roles[role], roles[i] = true, nil; end end
|
||||
return roles;
|
||||
end
|
||||
|
||||
def_env.user = {};
|
||||
function def_env.user:create(jid, password, roles)
|
||||
function def_env.user:create(jid, password, role)
|
||||
local username, host = jid_split(jid);
|
||||
if not prosody.hosts[host] then
|
||||
return nil, "No such host: "..host;
|
||||
elseif um.user_exists(username, host) then
|
||||
return nil, "User exists";
|
||||
end
|
||||
local ok, err = um.create_user(username, password, host);
|
||||
if ok then
|
||||
if ok and roles then
|
||||
roles = coerce_roles(roles);
|
||||
local roles_ok, rerr = um.set_roles(jid, host, roles);
|
||||
if not roles_ok then return nil, "User created, but could not set roles: " .. tostring(rerr); end
|
||||
end
|
||||
return true, "User created";
|
||||
else
|
||||
local ok, err = um.create_user(username, nil, host);
|
||||
if not ok then
|
||||
return nil, "Could not create user: "..err;
|
||||
end
|
||||
|
||||
if role then
|
||||
local role_ok, rerr = um.set_user_role(jid, host, role);
|
||||
if not role_ok then
|
||||
return nil, "Could not set role: " .. tostring(rerr);
|
||||
end
|
||||
end
|
||||
|
||||
local ok, err = um.set_password(username, password, host, nil);
|
||||
if not ok then
|
||||
return nil, "Could not set password for user: "..err;
|
||||
end
|
||||
|
||||
return true, "User created";
|
||||
end
|
||||
|
||||
function def_env.user:delete(jid)
|
||||
|
@ -1403,41 +1444,64 @@ function def_env.user:password(jid, password)
|
|||
end
|
||||
end
|
||||
|
||||
function def_env.user:roles(jid, host, new_roles)
|
||||
if new_roles or type(host) == "table" then
|
||||
return nil, "Use user:setroles(jid, host, roles) to change user roles";
|
||||
end
|
||||
function def_env.user:role(jid, host)
|
||||
local print = self.session.print;
|
||||
local username, userhost = jid_split(jid);
|
||||
if host == nil then host = userhost; end
|
||||
if host ~= "*" and not prosody.hosts[host] then
|
||||
if not prosody.hosts[host] then
|
||||
return nil, "No such host: "..host;
|
||||
elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then
|
||||
return nil, "No such user";
|
||||
end
|
||||
local roles = um.get_roles(jid, host);
|
||||
if not roles then return true, "No roles"; end
|
||||
local count = 0;
|
||||
local print = self.session.print;
|
||||
for role in pairs(roles) do
|
||||
|
||||
local primary_role = um.get_user_role(username, host);
|
||||
local secondary_roles = um.get_user_secondary_roles(username, host);
|
||||
|
||||
print(primary_role and primary_role.name or "<none>");
|
||||
|
||||
local count = primary_role and 1 or 0;
|
||||
for role_name in pairs(secondary_roles or {}) do
|
||||
count = count + 1;
|
||||
print(role);
|
||||
print(role_name.." (secondary)");
|
||||
end
|
||||
|
||||
return true, count == 1 and "1 role" or count.." roles";
|
||||
end
|
||||
def_env.user.showroles = def_env.user.roles; -- COMPAT
|
||||
def_env.user.roles = def_env.user.role;
|
||||
|
||||
-- user:roles("someone@example.com", "example.com", {"prosody:admin"})
|
||||
-- user:roles("someone@example.com", {"prosody:admin"})
|
||||
function def_env.user:setroles(jid, host, new_roles)
|
||||
-- user:setrole("someone@example.com", "example.com", "prosody:admin")
|
||||
-- user:setrole("someone@example.com", "prosody:admin")
|
||||
function def_env.user:setrole(jid, host, new_role)
|
||||
local username, userhost = jid_split(jid);
|
||||
if new_roles == nil then host, new_roles = userhost, host; end
|
||||
if host ~= "*" and not prosody.hosts[host] then
|
||||
if new_role == nil then host, new_role = userhost, host; end
|
||||
if not prosody.hosts[host] then
|
||||
return nil, "No such host: "..host;
|
||||
elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then
|
||||
return nil, "No such user";
|
||||
end
|
||||
if host == "*" then host = nil; end
|
||||
return um.set_roles(jid, host, coerce_roles(new_roles));
|
||||
return um.set_user_role(username, host, new_role);
|
||||
end
|
||||
|
||||
function def_env.user:addrole(jid, host, new_role)
|
||||
local username, userhost = jid_split(jid);
|
||||
if new_role == nil then host, new_role = userhost, host; end
|
||||
if not prosody.hosts[host] then
|
||||
return nil, "No such host: "..host;
|
||||
elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then
|
||||
return nil, "No such user";
|
||||
end
|
||||
return um.add_user_secondary_role(username, host, new_role);
|
||||
end
|
||||
|
||||
function def_env.user:delrole(jid, host, role_name)
|
||||
local username, userhost = jid_split(jid);
|
||||
if role_name == nil then host, role_name = userhost, host; end
|
||||
if not prosody.hosts[host] then
|
||||
return nil, "No such host: "..host;
|
||||
elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then
|
||||
return nil, "No such user";
|
||||
end
|
||||
return um.remove_user_secondary_role(username, host, role_name);
|
||||
end
|
||||
|
||||
-- TODO switch to table view, include roles
|
||||
|
@ -1508,7 +1572,7 @@ function def_env.xmpp:ping(localhost, remotehost, timeout)
|
|||
module:unhook("s2sin-established", onestablished);
|
||||
module:unhook("s2s-destroyed", ondestroyed);
|
||||
end):next(function(pong)
|
||||
return ("pong from %s in %gs"):format(pong.stanza.attr.from, time.now() - time_start);
|
||||
return ("pong from %s on %s in %gs"):format(pong.stanza.attr.from, pong.origin.id, time.now() - time_start);
|
||||
end);
|
||||
end
|
||||
|
||||
|
@ -1560,7 +1624,7 @@ function def_env.http:list(hosts)
|
|||
local output = format_table({
|
||||
{ title = "Module", width = "20%" },
|
||||
{ title = "URL", width = "80%" },
|
||||
}, 132);
|
||||
}, self.session.width);
|
||||
|
||||
for _, host in ipairs(hosts) do
|
||||
local http_apps = modulemanager.get_items("http-provider", host);
|
||||
|
@ -1591,6 +1655,60 @@ function def_env.http:list(hosts)
|
|||
return true;
|
||||
end
|
||||
|
||||
def_env.watch = {};
|
||||
|
||||
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
|
||||
|
||||
local stanza_watchers = module:require("mod_debug_stanzas/watcher");
|
||||
function def_env.watch:stanzas(target_spec, filter_spec)
|
||||
local function handler(event_type, stanza, session)
|
||||
if stanza then
|
||||
if event_type == "sent" then
|
||||
self.session.print(("\n<!-- sent to %s -->"):format(session.id));
|
||||
elseif event_type == "received" then
|
||||
self.session.print(("\n<!-- received from %s -->"):format(session.id));
|
||||
else
|
||||
self.session.print(("\n<!-- %s (%s) -->"):format(event_type, session.id));
|
||||
end
|
||||
self.session.print(stanza);
|
||||
elseif session then
|
||||
self.session.print("\n<!-- session "..session.id.." "..event_type.." -->");
|
||||
elseif event_type then
|
||||
self.session.print("\n<!-- "..event_type.." -->");
|
||||
end
|
||||
end
|
||||
|
||||
stanza_watchers.add({
|
||||
target_spec = {
|
||||
jid = target_spec;
|
||||
};
|
||||
filter_spec = filter_spec and {
|
||||
with_jid = filter_spec;
|
||||
};
|
||||
}, handler);
|
||||
|
||||
while self.session.is_connected() do
|
||||
async.sleep(3);
|
||||
end
|
||||
|
||||
stanza_watchers.remove(handler);
|
||||
end
|
||||
|
||||
def_env.debug = {};
|
||||
|
||||
function def_env.debug:logevents(host)
|
||||
|
@ -1934,6 +2052,10 @@ function def_env.stats:show(name_filter)
|
|||
end
|
||||
|
||||
|
||||
function module.unload()
|
||||
stanza_watchers.cleanup();
|
||||
end
|
||||
|
||||
|
||||
-------------
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
local st, jid = require "util.stanza", require "util.jid";
|
||||
|
||||
local hosts = prosody.hosts;
|
||||
local is_admin = require "core.usermanager".is_admin;
|
||||
|
||||
function send_to_online(message, host)
|
||||
local sessions;
|
||||
|
@ -34,6 +33,7 @@ function send_to_online(message, host)
|
|||
return c;
|
||||
end
|
||||
|
||||
module:default_permission("prosody:admin", ":send-announcement");
|
||||
|
||||
-- Old <message>-based jabberd-style announcement sending
|
||||
function handle_announcement(event)
|
||||
|
@ -45,8 +45,8 @@ function handle_announcement(event)
|
|||
return; -- Not an announcement
|
||||
end
|
||||
|
||||
if not is_admin(stanza.attr.from, host) then
|
||||
-- Not an admin? Not allowed!
|
||||
if not module:may(":send-announcement", event) then
|
||||
-- Not allowed!
|
||||
module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from);
|
||||
return;
|
||||
end
|
||||
|
|
|
@ -27,6 +27,7 @@ function provider.set_password(username, password)
|
|||
return nil, "Password fails SASLprep.";
|
||||
end
|
||||
if account then
|
||||
account.updated = os.time();
|
||||
account.password = password;
|
||||
return datamanager.store(username, host, "accounts", account);
|
||||
end
|
||||
|
@ -38,7 +39,8 @@ function provider.user_exists(username)
|
|||
end
|
||||
|
||||
function provider.create_user(username, password)
|
||||
return datamanager.store(username, host, "accounts", {password = password});
|
||||
local now = os.time();
|
||||
return datamanager.store(username, host, "accounts", { created = now; updated = now; password = password });
|
||||
end
|
||||
|
||||
function provider.delete_user(username)
|
||||
|
|
|
@ -86,11 +86,21 @@ function provider.set_password(username, password)
|
|||
account.server_key = server_key_hex
|
||||
|
||||
account.password = nil;
|
||||
account.updated = os.time();
|
||||
return accounts:set(username, account);
|
||||
end
|
||||
return nil, "Account not available.";
|
||||
end
|
||||
|
||||
function provider.get_account_info(username)
|
||||
local account = accounts:get(username);
|
||||
if not account then return nil, "Account not available"; end
|
||||
return {
|
||||
created = account.created;
|
||||
password_updated = account.updated;
|
||||
};
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
local account = accounts:get(username);
|
||||
if not account then
|
||||
|
@ -105,8 +115,9 @@ function provider.users()
|
|||
end
|
||||
|
||||
function provider.create_user(username, password)
|
||||
local now = os.time();
|
||||
if password == nil then
|
||||
return accounts:set(username, {});
|
||||
return accounts:set(username, { created = now; updated = now; disabled = true });
|
||||
end
|
||||
local salt = generate_uuid();
|
||||
local valid, stored_key, server_key = get_auth_db(password, salt, default_iteration_count);
|
||||
|
@ -117,7 +128,8 @@ function provider.create_user(username, password)
|
|||
local server_key_hex = to_hex(server_key);
|
||||
return accounts:set(username, {
|
||||
stored_key = stored_key_hex, server_key = server_key_hex,
|
||||
salt = salt, iteration_count = default_iteration_count
|
||||
salt = salt, iteration_count = default_iteration_count,
|
||||
created = now, updated = now;
|
||||
});
|
||||
end
|
||||
|
||||
|
|
|
@ -48,11 +48,21 @@ function provider.set_password(username, password)
|
|||
local account = accounts:get(username);
|
||||
if account then
|
||||
account.password = password;
|
||||
account.updated = os.time();
|
||||
return accounts:set(username, account);
|
||||
end
|
||||
return nil, "Account not available.";
|
||||
end
|
||||
|
||||
function provider.get_account_info(username)
|
||||
local account = accounts:get(username);
|
||||
if not account then return nil, "Account not available"; end
|
||||
return {
|
||||
created = account.created;
|
||||
password_updated = account.updated;
|
||||
};
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
local account = accounts:get(username);
|
||||
if not account then
|
||||
|
@ -71,7 +81,11 @@ function provider.create_user(username, password)
|
|||
if not password then
|
||||
return nil, "Password fails SASLprep.";
|
||||
end
|
||||
return accounts:set(username, {password = password});
|
||||
local now = os.time();
|
||||
return accounts:set(username, {
|
||||
password = password;
|
||||
created = now, updated = now;
|
||||
});
|
||||
end
|
||||
|
||||
function provider.delete_user(username)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
-- mod_auth_ldap
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
local new_sasl = require "util.sasl".new;
|
||||
local lualdap = require "lualdap";
|
||||
|
||||
|
@ -21,6 +20,13 @@ local ldap_admins = module:get_option_string("ldap_admin_filter",
|
|||
module:get_option_string("ldap_admins")); -- COMPAT with mistake in documentation
|
||||
local host = ldap_filter_escape(module:get_option_string("realm", module.host));
|
||||
|
||||
if ldap_admins then
|
||||
module:log("error", "The 'ldap_admin_filter' option has been deprecated, "..
|
||||
"and will be ignored. Equivalent functionality may be added in "..
|
||||
"the future if there is demand."
|
||||
);
|
||||
end
|
||||
|
||||
-- Initiate connection
|
||||
local ld = nil;
|
||||
module.unload = function() if ld then pcall(ld, ld.close); end end
|
||||
|
@ -133,22 +139,4 @@ else
|
|||
module:log("error", "Unsupported ldap_mode %s", tostring(ldap_mode));
|
||||
end
|
||||
|
||||
if ldap_admins then
|
||||
function provider.is_admin(jid)
|
||||
local username, user_host = jid_split(jid);
|
||||
if user_host ~= module.host then
|
||||
return false;
|
||||
end
|
||||
return ldap_do("search", 2, {
|
||||
base = ldap_base;
|
||||
scope = ldap_scope;
|
||||
sizelimit = 1;
|
||||
filter = ldap_admins:gsub("%$(%a+)", {
|
||||
user = ldap_filter_escape(username);
|
||||
host = host;
|
||||
});
|
||||
});
|
||||
end
|
||||
end
|
||||
|
||||
module:provides("auth", provider);
|
||||
|
|
|
@ -1,59 +1,330 @@
|
|||
local array = require "util.array";
|
||||
local it = require "util.iterators";
|
||||
local set = require "util.set";
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_split, jid_bare, jid_host = import("util.jid", "split", "bare", "host");
|
||||
local normalize = require "util.jid".prep;
|
||||
local roles = require "util.roles";
|
||||
|
||||
local config_global_admin_jids = module:context("*"):get_option_set("admins", {}) / normalize;
|
||||
local config_admin_jids = module:get_option_inherited_set("admins", {}) / normalize;
|
||||
local host = module.host;
|
||||
local role_store = module:open_store("roles");
|
||||
local role_map_store = module:open_store("roles", "map");
|
||||
local host_suffix = host:gsub("^[^%.]+%.", "");
|
||||
|
||||
local admin_role = { ["prosody:admin"] = true };
|
||||
local hosts = prosody.hosts;
|
||||
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:user");
|
||||
server_user_role = module:get_option_string("server_user_role");
|
||||
public_user_role = module:get_option_string("public_user_role");
|
||||
end
|
||||
|
||||
function get_user_roles(user)
|
||||
if config_admin_jids:contains(user.."@"..host) then
|
||||
return admin_role;
|
||||
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
|
||||
return role_store:get(user);
|
||||
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
|
||||
|
||||
function set_user_roles(user, roles)
|
||||
role_store:set(user, roles)
|
||||
return true;
|
||||
-- Default roles
|
||||
register_role {
|
||||
name = "prosody:restricted";
|
||||
priority = 15;
|
||||
};
|
||||
|
||||
register_role {
|
||||
name = "prosody:user";
|
||||
priority = 25;
|
||||
inherits = { "prosody:restricted" };
|
||||
};
|
||||
|
||||
register_role {
|
||||
name = "prosody:admin";
|
||||
priority = 50;
|
||||
inherits = { "prosody:user" };
|
||||
};
|
||||
|
||||
register_role {
|
||||
name = "prosody:operator";
|
||||
priority = 75;
|
||||
inherits = { "prosody:admin" };
|
||||
};
|
||||
|
||||
|
||||
-- Process custom roles from config
|
||||
|
||||
local custom_roles = module:get_option("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
|
||||
|
||||
function get_users_with_role(role)
|
||||
local storage_role_users = it.to_array(it.keys(role_map_store:get_all(role) or {}));
|
||||
if role == "prosody:admin" then
|
||||
local config_admin_users = config_admin_jids / function (admin_jid)
|
||||
-- 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["prosody:user"];
|
||||
end
|
||||
if stored_roles._default == nil then
|
||||
-- No primary role explicitly set, return default
|
||||
return role_registry["prosody:user"];
|
||||
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 default user an unknown role: "..tostring(role_name));
|
||||
end
|
||||
local keys_update = {
|
||||
_default = role_name;
|
||||
-- Primary role cannot be secondary role
|
||||
[role_name] = role_map_store.remove;
|
||||
};
|
||||
if role_name == "prosody:user" then
|
||||
-- Don't store default
|
||||
keys_update._default = role_map_store.remove;
|
||||
end
|
||||
local ok, err = role_map_store:set_keys(user, keys_update);
|
||||
if not ok then
|
||||
return nil, err;
|
||||
end
|
||||
return role;
|
||||
end
|
||||
|
||||
function add_user_secondary_role(user, role_name)
|
||||
if not role_registry[role_name] then
|
||||
return error("Cannot assign default user an unknown role: "..tostring(role_name));
|
||||
end
|
||||
role_map_store:set(user, role_name, true);
|
||||
end
|
||||
|
||||
function remove_user_secondary_role(user, role_name)
|
||||
role_map_store:set(user, role_name, nil);
|
||||
end
|
||||
|
||||
function get_user_secondary_roles(user)
|
||||
local stored_roles, err = role_store:get(user);
|
||||
if not stored_roles then
|
||||
if err then
|
||||
-- Unable to fetch role, fail
|
||||
return nil, err;
|
||||
end
|
||||
-- No role set
|
||||
return {};
|
||||
end
|
||||
stored_roles._default = nil;
|
||||
for role_name in pairs(stored_roles) do
|
||||
stored_roles[role_name] = role_registry[role_name];
|
||||
end
|
||||
return stored_roles;
|
||||
end
|
||||
|
||||
function user_can_assume_role(user, role_name)
|
||||
local primary_role = get_user_role(user);
|
||||
if primary_role and primary_role.role_name == role_name then
|
||||
return true;
|
||||
end
|
||||
local secondary_roles = get_user_secondary_roles(user);
|
||||
if secondary_roles and secondary_roles[role_name] then
|
||||
return true;
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
-- This function is *expensive*
|
||||
function get_users_with_role(role_name)
|
||||
local function role_filter(username, default_role) --luacheck: ignore 212/username
|
||||
return default_role == role_name;
|
||||
end
|
||||
local primary_role_users = set.new(it.to_array(it.filter(role_filter, pairs(role_map_store:get_all("_default") or {}))));
|
||||
local secondary_role_users = set.new(it.to_array(it.keys(role_map_store:get_all(role_name) or {})));
|
||||
|
||||
local config_set;
|
||||
if role_name == "prosody:admin" then
|
||||
config_set = config_admin_jids;
|
||||
elseif role_name == "prosody:operator" then
|
||||
config_set = config_global_admin_jids;
|
||||
end
|
||||
if config_set then
|
||||
local config_admin_users = config_set / function (admin_jid)
|
||||
local j_node, j_host = jid_split(admin_jid);
|
||||
if j_host == host then
|
||||
return j_node;
|
||||
end
|
||||
end;
|
||||
return it.to_array(config_admin_users + set.new(storage_role_users));
|
||||
return it.to_array(config_admin_users + primary_role_users + secondary_role_users);
|
||||
end
|
||||
return storage_role_users;
|
||||
return it.to_array(primary_role_users + secondary_role_users);
|
||||
end
|
||||
|
||||
function get_jid_roles(jid)
|
||||
if config_admin_jids:contains(jid) then
|
||||
return admin_role;
|
||||
function get_jid_role(jid)
|
||||
local bare_jid = jid_bare(jid);
|
||||
if config_global_admin_jids:contains(bare_jid) then
|
||||
return role_registry["prosody:operator"];
|
||||
elseif config_admin_jids:contains(bare_jid) then
|
||||
return role_registry["prosody:admin"];
|
||||
elseif is_component then
|
||||
local user_host = jid_host(bare_jid);
|
||||
if host_user_role and user_host == host_suffix then
|
||||
return role_registry[host_user_role];
|
||||
elseif server_user_role and hosts[user_host] then
|
||||
return role_registry[server_user_role];
|
||||
elseif public_user_role then
|
||||
return role_registry[public_user_role];
|
||||
end
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function set_jid_roles(jid) -- luacheck: ignore 212
|
||||
function set_jid_role(jid, role_name) -- luacheck: ignore 212
|
||||
return false;
|
||||
end
|
||||
|
||||
function get_jids_with_role(role)
|
||||
function get_jids_with_role(role_name)
|
||||
-- Fetch role users from storage
|
||||
local storage_role_jids = array.map(get_users_with_role(role), function (username)
|
||||
local storage_role_jids = array.map(get_users_with_role(role_name), function (username)
|
||||
return username.."@"..host;
|
||||
end);
|
||||
if role == "prosody:admin" then
|
||||
if role_name == "prosody:admin" then
|
||||
return it.to_array(config_admin_jids + set.new(storage_role_jids));
|
||||
elseif role_name == "prosody:operator" then
|
||||
return it.to_array(config_global_admin_jids + set.new(storage_role_jids));
|
||||
end
|
||||
return storage_role_jids;
|
||||
end
|
||||
|
||||
function add_default_permission(role_name, action, policy)
|
||||
local role = role_registry[role_name];
|
||||
if not role then
|
||||
module:log("warn", "Attempt to add default permission for unknown role: %s", role_name);
|
||||
return nil, "no-such-role";
|
||||
end
|
||||
if policy == nil then policy = true; end
|
||||
module:log("debug", "Adding policy %s for permission %s on role %s", policy, action, role_name);
|
||||
return role:set_permission(action, policy);
|
||||
end
|
||||
|
||||
function get_role_by_name(role_name)
|
||||
return assert(role_registry[role_name], role_name);
|
||||
end
|
||||
|
||||
-- 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 "core.storagemanager".initialize_host(migrate_host);
|
||||
|
||||
print("II: Migrating roles...");
|
||||
local migrated, failed, skipped = do_migration(migrate_host);
|
||||
print(("II: %d migrated, %d failed, %d skipped"):format(migrated, failed, skipped));
|
||||
return (failed + skipped == 0) and 0 or 1;
|
||||
else
|
||||
print("EE: Unknown command: "..(arg[1] or "<none given>"));
|
||||
print(" Hint: try 'migrate'?");
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,6 +54,7 @@ local function set_blocklist(username, blocklist)
|
|||
end
|
||||
|
||||
-- Migrates from the old mod_privacy storage
|
||||
-- TODO mod_privacy was removed in 0.10.0, this should be phased out
|
||||
local function migrate_privacy_list(username)
|
||||
local legacy_data = module:open_store("privacy"):get(username);
|
||||
if not legacy_data or not legacy_data.lists or not legacy_data.default then return; end
|
||||
|
@ -77,6 +78,13 @@ local function migrate_privacy_list(username)
|
|||
return migrated_data;
|
||||
end
|
||||
|
||||
if not module:get_option_boolean("migrate_legacy_blocking", true) then
|
||||
migrate_privacy_list = function (username)
|
||||
module:log("debug", "Migrating from mod_privacy disabled, user '%s' will start with a fresh blocklist", username);
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
local function get_blocklist(username)
|
||||
local blocklist = cache2:get(username);
|
||||
if not blocklist then
|
||||
|
|
|
@ -117,8 +117,7 @@ function stream_callbacks._streamopened(session, attr)
|
|||
session.secure = true;
|
||||
session.encrypted = true;
|
||||
|
||||
local sock = session.conn:socket();
|
||||
local info = sock.info and sock:info();
|
||||
local info = session.conn:ssl_info();
|
||||
if type(info) == "table" then
|
||||
(session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
|
||||
session.compressed = info.compression;
|
||||
|
@ -129,7 +128,7 @@ function stream_callbacks._streamopened(session, attr)
|
|||
end
|
||||
|
||||
local features = st.stanza("stream:features");
|
||||
hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
|
||||
hosts[session.host].events.fire_event("stream-features", { origin = session, features = features, stream = attr });
|
||||
if features.tags[1] or session.full_jid then
|
||||
send(features);
|
||||
else
|
||||
|
@ -260,9 +259,17 @@ local function disconnect_user_sessions(reason, leave_resource)
|
|||
end
|
||||
|
||||
module:hook_global("user-password-changed", disconnect_user_sessions({ condition = "reset", text = "Password changed" }, true), 200);
|
||||
module:hook_global("user-roles-changed", disconnect_user_sessions({ condition = "reset", text = "Roles changed" }), 200);
|
||||
module:hook_global("user-role-changed", disconnect_user_sessions({ condition = "reset", text = "Role changed" }), 200);
|
||||
module:hook_global("user-deleted", disconnect_user_sessions({ condition = "not-authorized", text = "Account deleted" }), 200);
|
||||
|
||||
module:hook_global("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;
|
||||
end
|
||||
end);
|
||||
|
||||
function runner_callbacks:ready()
|
||||
if self.data.conn then
|
||||
self.data.conn:resume();
|
||||
|
@ -295,8 +302,7 @@ function listener.onconnect(conn)
|
|||
session.encrypted = true;
|
||||
|
||||
-- Check if TLS compression is used
|
||||
local sock = conn:socket();
|
||||
local info = sock.info and sock:info();
|
||||
local info = conn:ssl_info();
|
||||
if type(info) == "table" then
|
||||
(session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
|
||||
session.compressed = info.compression;
|
||||
|
|
|
@ -17,7 +17,7 @@ local logger = require "util.logger";
|
|||
local sha1 = require "util.hashes".sha1;
|
||||
local st = require "util.stanza";
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_host = require "util.jid".host;
|
||||
local new_xmpp_stream = require "util.xmppstream".new;
|
||||
local uuid_gen = require "util.uuid".generate;
|
||||
|
||||
|
@ -222,22 +222,19 @@ function stream_callbacks.handlestanza(session, stanza)
|
|||
end
|
||||
if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then
|
||||
local from = stanza.attr.from;
|
||||
if from then
|
||||
if session.component_validate_from then
|
||||
local _, domain = jid_split(stanza.attr.from);
|
||||
if domain ~= session.host then
|
||||
-- Return error
|
||||
session.log("warn", "Component sent stanza with missing or invalid 'from' address");
|
||||
session:close{
|
||||
condition = "invalid-from";
|
||||
text = "Component tried to send from address <"..tostring(from)
|
||||
.."> which is not in domain <"..tostring(session.host)..">";
|
||||
};
|
||||
return;
|
||||
end
|
||||
if session.component_validate_from then
|
||||
if not from or (jid_host(from) ~= session.host) then
|
||||
-- Return error
|
||||
session.log("warn", "Component sent stanza with missing or invalid 'from' address");
|
||||
session:close{
|
||||
condition = "invalid-from";
|
||||
text = "Component tried to send from address <"..(from or "< [missing 'from' attribute] >")
|
||||
.."> which is not in domain <"..tostring(session.host)..">";
|
||||
};
|
||||
return;
|
||||
end
|
||||
else
|
||||
stanza.attr.from = session.host; -- COMPAT: Strictly we shouldn't allow this
|
||||
elseif not from then
|
||||
stanza.attr.from = session.host;
|
||||
end
|
||||
if not stanza.attr.to then
|
||||
session.log("warn", "Rejecting stanza with no 'to' address");
|
||||
|
|
|
@ -116,6 +116,9 @@ local flush_reasons = module:metric(
|
|||
{ "reason" }
|
||||
);
|
||||
|
||||
local flush_sizes = module:metric("histogram", "flush_stanza_count", "", "Number of stanzas flushed at once", {},
|
||||
{ buckets = { 0, 1, 2, 4, 8, 16, 32, 64, 128, 256 } }):with_labels();
|
||||
|
||||
local function manage_buffer(stanza, session)
|
||||
local ctr = session.csi_counter or 0;
|
||||
if session.state ~= "inactive" then
|
||||
|
@ -129,6 +132,7 @@ local function manage_buffer(stanza, session)
|
|||
session.csi_measure_buffer_hold = nil;
|
||||
end
|
||||
flush_reasons:with_labels(why or "important"):add(1);
|
||||
flush_sizes:sample(ctr);
|
||||
session.log("debug", "Flushing buffer (%s; queue size is %d)", why or "important", session.csi_counter);
|
||||
session.state = "flushing";
|
||||
module:fire_event("csi-flushing", { session = session });
|
||||
|
@ -147,6 +151,7 @@ local function flush_buffer(data, session)
|
|||
session.log("debug", "Flushing buffer (%s; queue size is %d)", "client activity", session.csi_counter);
|
||||
session.state = "flushing";
|
||||
module:fire_event("csi-flushing", { session = session });
|
||||
flush_sizes:sample(ctr);
|
||||
flush_reasons:with_labels("client activity"):add(1);
|
||||
if session.csi_measure_buffer_hold then
|
||||
session.csi_measure_buffer_hold();
|
||||
|
|
220
plugins/mod_debug_stanzas/watcher.lib.lua
Normal file
220
plugins/mod_debug_stanzas/watcher.lib.lua
Normal file
|
@ -0,0 +1,220 @@
|
|||
local filters = require "util.filters";
|
||||
local jid = require "util.jid";
|
||||
local set = require "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;
|
||||
};
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
local get_children = require "core.hostmanager".get_children;
|
||||
local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
|
||||
local um_is_admin = require "core.usermanager".is_admin;
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local st = require "util.stanza"
|
||||
|
@ -162,14 +161,16 @@ module:hook("s2s-stream-features", function (event)
|
|||
end
|
||||
end);
|
||||
|
||||
module:default_permission("prosody:admin", ":be-discovered-admin");
|
||||
|
||||
-- Handle disco requests to user accounts
|
||||
if module:get_host_type() ~= "local" then return end -- skip for components
|
||||
module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local node = stanza.tags[1].attr.node;
|
||||
local username = jid_split(stanza.attr.to) or origin.username;
|
||||
local is_admin = um_is_admin(stanza.attr.to or origin.full_jid, module.host)
|
||||
if not stanza.attr.to or (expose_admins and is_admin) or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
|
||||
local target_is_admin = module:may(":be-discovered-admin", stanza.attr.to or origin.full_jid);
|
||||
if not stanza.attr.to or (expose_admins and target_is_admin) or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
|
||||
if node and node ~= "" then
|
||||
local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node});
|
||||
if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
|
||||
|
@ -185,7 +186,7 @@ module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function(
|
|||
end
|
||||
local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'});
|
||||
if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
|
||||
if is_admin then
|
||||
if target_is_admin then
|
||||
reply:tag('identity', {category='account', type='admin'}):up();
|
||||
elseif prosody.hosts[module.host].users.name == "anonymous" then
|
||||
reply:tag('identity', {category='account', type='anonymous'}):up();
|
||||
|
|
|
@ -16,7 +16,7 @@ local configured_services = module:get_option_array("external_services", {});
|
|||
|
||||
local access = module:get_option_set("external_service_access", {});
|
||||
|
||||
-- https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
||||
-- https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00
|
||||
local function behave_turn_rest_credentials(srv, item, secret)
|
||||
local ttl = default_ttl;
|
||||
if type(item.ttl) == "number" then
|
||||
|
|
|
@ -12,7 +12,6 @@ local jid = require "util.jid";
|
|||
local st = require "util.stanza";
|
||||
local url = require "socket.url";
|
||||
local dm = require "core.storagemanager".olddm;
|
||||
local jwt = require "util.jwt";
|
||||
local errors = require "util.error";
|
||||
local dataform = require "util.dataforms".new;
|
||||
local urlencode = require "util.http".urlencode;
|
||||
|
@ -44,6 +43,8 @@ local expiry = module:get_option_number(module.name .. "_expires_after", 7 * 864
|
|||
local daily_quota = module:get_option_number(module.name .. "_daily_quota", file_size_limit*10); -- 100 MB / day
|
||||
local total_storage_limit = module:get_option_number(module.name.."_global_quota", unlimited);
|
||||
|
||||
local create_jwt, verify_jwt = require "util.jwt".init("HS256", secret);
|
||||
|
||||
local access = module:get_option_set(module.name .. "_access", {});
|
||||
|
||||
if not external_base_url then
|
||||
|
@ -169,16 +170,13 @@ function may_upload(uploader, filename, filesize, filetype) -- > boolean, error
|
|||
end
|
||||
|
||||
function get_authz(slot, uploader, filename, filesize, filetype)
|
||||
local now = os.time();
|
||||
return jwt.sign(secret, {
|
||||
return create_jwt({
|
||||
-- token properties
|
||||
sub = uploader;
|
||||
iat = now;
|
||||
exp = now+300;
|
||||
|
||||
-- slot properties
|
||||
slot = slot;
|
||||
expires = expiry >= 0 and (now+expiry) or nil;
|
||||
expires = expiry >= 0 and (os.time()+expiry) or nil;
|
||||
-- file properties
|
||||
filename = filename;
|
||||
filesize = filesize;
|
||||
|
@ -249,32 +247,34 @@ end
|
|||
|
||||
function handle_upload(event, path) -- PUT /upload/:slot
|
||||
local request = event.request;
|
||||
local authz = request.headers.authorization;
|
||||
if authz then
|
||||
authz = authz:match("^Bearer (.*)")
|
||||
end
|
||||
if not authz then
|
||||
module:log("debug", "Missing or malformed Authorization header");
|
||||
event.response.headers.www_authenticate = "Bearer";
|
||||
return 401;
|
||||
end
|
||||
local authed, upload_info = jwt.verify(secret, authz);
|
||||
if not (authed and type(upload_info) == "table" and type(upload_info.exp) == "number") then
|
||||
module:log("debug", "Unauthorized or invalid token: %s, %q", authed, upload_info);
|
||||
return 401;
|
||||
end
|
||||
if not request.body_sink and upload_info.exp < os.time() then
|
||||
module:log("debug", "Authorization token expired on %s", dt.datetime(upload_info.exp));
|
||||
return 410;
|
||||
end
|
||||
if not path or upload_info.slot ~= path:match("^[^/]+") then
|
||||
module:log("debug", "Invalid upload slot: %q, path: %q", upload_info.slot, path);
|
||||
return 400;
|
||||
end
|
||||
if request.headers.content_length and tonumber(request.headers.content_length) ~= upload_info.filesize then
|
||||
return 413;
|
||||
-- Note: We don't know the size if the upload is streamed in chunked encoding,
|
||||
-- so we also check the final file size on completion.
|
||||
local upload_info = request.http_file_share_upload_info;
|
||||
|
||||
if not upload_info then -- Initial handling of request
|
||||
local authz = request.headers.authorization;
|
||||
if authz then
|
||||
authz = authz:match("^Bearer (.*)")
|
||||
end
|
||||
if not authz then
|
||||
module:log("debug", "Missing or malformed Authorization header");
|
||||
event.response.headers.www_authenticate = "Bearer";
|
||||
return 401;
|
||||
end
|
||||
local authed, authed_upload_info = verify_jwt(authz);
|
||||
if not authed then
|
||||
module:log("debug", "Unauthorized or invalid token: %s, %q", authz, authed_upload_info);
|
||||
return 401;
|
||||
end
|
||||
if not path or authed_upload_info.slot ~= path:match("^[^/]+") then
|
||||
module:log("debug", "Invalid upload slot: %q, path: %q", authed_upload_info.slot, path);
|
||||
return 400;
|
||||
end
|
||||
if request.headers.content_length and tonumber(request.headers.content_length) ~= authed_upload_info.filesize then
|
||||
return 413;
|
||||
-- Note: We don't know the size if the upload is streamed in chunked encoding,
|
||||
-- so we also check the final file size on completion.
|
||||
end
|
||||
upload_info = authed_upload_info;
|
||||
request.http_file_share_upload_info = upload_info;
|
||||
end
|
||||
|
||||
local filename = get_filename(upload_info.slot, true);
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
local dataforms = require "util.dataforms";
|
||||
local datetime = require "util.datetime";
|
||||
local split_jid = require "util.jid".split;
|
||||
local usermanager = require "core.usermanager";
|
||||
|
||||
local new_adhoc = module:require("adhoc").new;
|
||||
|
||||
|
@ -13,8 +12,7 @@ local allow_user_invites = module:get_option_boolean("allow_user_invites", false
|
|||
-- on the server, use the option above instead.
|
||||
local allow_contact_invites = module:get_option_boolean("allow_contact_invites", true);
|
||||
|
||||
local allow_user_invite_roles = module:get_option_set("allow_user_invites_by_roles");
|
||||
local deny_user_invite_roles = module:get_option_set("deny_user_invites_by_roles");
|
||||
module:default_permission(allow_user_invites and "prosody:user" or "prosody:admin", ":invite-users");
|
||||
|
||||
local invites;
|
||||
if prosody.shutdown then -- COMPAT hack to detect prosodyctl
|
||||
|
@ -42,36 +40,8 @@ local invite_result_form = dataforms.new({
|
|||
|
||||
-- This is for checking if the specified JID may create invites
|
||||
-- that allow people to register accounts on this host.
|
||||
local function may_invite_new_users(jid)
|
||||
if usermanager.get_roles then
|
||||
local user_roles = usermanager.get_roles(jid, module.host);
|
||||
if not user_roles then
|
||||
-- User has no roles we can check, just return default
|
||||
return allow_user_invites;
|
||||
end
|
||||
|
||||
if user_roles["prosody:admin"] then
|
||||
return true;
|
||||
end
|
||||
if allow_user_invite_roles then
|
||||
for allowed_role in allow_user_invite_roles do
|
||||
if user_roles[allowed_role] then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
if deny_user_invite_roles then
|
||||
for denied_role in deny_user_invite_roles do
|
||||
if user_roles[denied_role] then
|
||||
return false;
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif usermanager.is_admin(jid, module.host) then -- COMPAT w/0.11
|
||||
return true; -- Admins may always create invitations
|
||||
end
|
||||
-- No role matches, so whatever the default is
|
||||
return allow_user_invites;
|
||||
local function may_invite_new_users(context)
|
||||
return module:may(":invite-users", context);
|
||||
end
|
||||
|
||||
module:depends("adhoc");
|
||||
|
@ -91,7 +61,7 @@ module:provides("adhoc", new_adhoc("Create new contact invite", "urn:xmpp:invite
|
|||
};
|
||||
};
|
||||
end
|
||||
local invite = invites.create_contact(username, may_invite_new_users(data.from), {
|
||||
local invite = invites.create_contact(username, may_invite_new_users(data), {
|
||||
source = data.from
|
||||
});
|
||||
--TODO: check errors
|
||||
|
|
|
@ -34,9 +34,9 @@ local rm_load_roster = require "core.rostermanager".load_roster;
|
|||
|
||||
local is_stanza = st.is_stanza;
|
||||
local tostring = tostring;
|
||||
local time_now = os.time;
|
||||
local time_now = require "util.time".now;
|
||||
local m_min = math.min;
|
||||
local timestamp, datestamp = import( "util.datetime", "datetime", "date");
|
||||
local timestamp, datestamp = import("util.datetime", "datetime", "date");
|
||||
local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
|
||||
local strip_tags = module:get_option_set("dont_archive_namespaces", { "http://jabber.org/protocol/chatstates" });
|
||||
|
||||
|
@ -53,8 +53,12 @@ if not archive.find then
|
|||
end
|
||||
local use_total = module:get_option_boolean("mam_include_total", true);
|
||||
|
||||
function schedule_cleanup()
|
||||
-- replaced later if cleanup is enabled
|
||||
function schedule_cleanup(_username, _date) -- luacheck: ignore 212
|
||||
-- Called to make a note of which users have messages on which days, which in
|
||||
-- turn is used to optimize the message expiry routine.
|
||||
--
|
||||
-- This noop is conditionally replaced later depending on retention settings
|
||||
-- and storage backend capabilities.
|
||||
end
|
||||
|
||||
-- Handle prefs.
|
||||
|
|
|
@ -14,7 +14,7 @@ local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed
|
|||
local pairs = pairs;
|
||||
local next = next;
|
||||
local type = type;
|
||||
local unpack = table.unpack or unpack; -- luacheck: ignore 113
|
||||
local unpack = table.unpack;
|
||||
local calculate_hash = require "util.caps".calculate_hash;
|
||||
local core_post_stanza = prosody.core_post_stanza;
|
||||
local bare_sessions = prosody.bare_sessions;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
local pubsub = require "util.pubsub";
|
||||
local st = require "util.stanza";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local usermanager = require "core.usermanager";
|
||||
local new_id = require "util.id".medium;
|
||||
local storagemanager = require "core.storagemanager";
|
||||
local xtemplate = require "util.xtemplate";
|
||||
|
@ -177,9 +176,10 @@ module:hook("host-disco-items", function (event)
|
|||
end);
|
||||
|
||||
local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
|
||||
module:default_permission("prosody:admin", ":service-admin");
|
||||
local function get_affiliation(jid)
|
||||
local bare_jid = jid_bare(jid);
|
||||
if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then
|
||||
if bare_jid == module.host or module:may(":service-admin", bare_jid) then
|
||||
return admin_aff;
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
local t_unpack = table.unpack or unpack; -- luacheck: ignore 113
|
||||
local t_unpack = table.unpack;
|
||||
local time_now = os.time;
|
||||
|
||||
local jid_prep = require "util.jid".prep;
|
||||
|
@ -678,8 +678,7 @@ end
|
|||
function handlers.set_retract(origin, stanza, retract, service)
|
||||
local node, notify = retract.attr.node, retract.attr.notify;
|
||||
notify = (notify == "1") or (notify == "true");
|
||||
local item = retract:get_child("item");
|
||||
local id = item and item.attr.id
|
||||
local id = retract:get_child_attr("item", nil, "id");
|
||||
if not (node and id) then
|
||||
origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required"));
|
||||
return true;
|
||||
|
|
|
@ -146,17 +146,17 @@ local function bounce_sendq(session, reason)
|
|||
elseif type(reason) == "string" then
|
||||
reason_text = reason;
|
||||
end
|
||||
for i, data in ipairs(sendq) do
|
||||
local reply = data[2];
|
||||
if reply and not(reply.attr.xmlns) and bouncy_stanzas[reply.name] then
|
||||
reply.attr.type = "error";
|
||||
reply:tag("error", {type = error_type, by = session.from_host})
|
||||
:tag(condition, {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
|
||||
if reason_text then
|
||||
reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})
|
||||
:text("Server-to-server connection failed: "..reason_text):up();
|
||||
end
|
||||
for i, stanza in ipairs(sendq) do
|
||||
if not stanza.attr.xmlns and bouncy_stanzas[stanza.name] and stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
|
||||
local reply = st.error_reply(
|
||||
stanza,
|
||||
error_type,
|
||||
condition,
|
||||
reason_text and ("Server-to-server connection failed: "..reason_text) or nil
|
||||
);
|
||||
core_process_stanza(dummy, reply);
|
||||
else
|
||||
(session.log or log)("debug", "Not eligible for bouncing, discarding %s", stanza:top_tag());
|
||||
end
|
||||
sendq[i] = nil;
|
||||
end
|
||||
|
@ -182,15 +182,11 @@ function route_to_existing_session(event)
|
|||
(host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host);
|
||||
|
||||
-- Queue stanza until we are able to send it
|
||||
local queued_item = {
|
||||
tostring(stanza),
|
||||
stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza);
|
||||
};
|
||||
if host.sendq then
|
||||
t_insert(host.sendq, queued_item);
|
||||
t_insert(host.sendq, st.clone(stanza));
|
||||
else
|
||||
-- luacheck: ignore 122
|
||||
host.sendq = { queued_item };
|
||||
host.sendq = { st.clone(stanza) };
|
||||
end
|
||||
host.log("debug", "stanza [%s] queued ", stanza.name);
|
||||
return true;
|
||||
|
@ -215,7 +211,7 @@ function route_to_new_session(event)
|
|||
|
||||
-- Store in buffer
|
||||
host_session.bounce_sendq = bounce_sendq;
|
||||
host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
|
||||
host_session.sendq = { 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
|
||||
|
@ -279,7 +275,7 @@ function module.add_host(module)
|
|||
function module.unload()
|
||||
if module.reloading then return end
|
||||
for _, session in pairs(sessions) do
|
||||
if session.to_host == module.host or session.from_host == module.host then
|
||||
if session.host == module.host then
|
||||
session:close("host-gone");
|
||||
end
|
||||
end
|
||||
|
@ -324,8 +320,8 @@ function mark_connected(session)
|
|||
if sendq then
|
||||
session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host);
|
||||
local send = session.sends2s;
|
||||
for i, data in ipairs(sendq) do
|
||||
send(data[1]);
|
||||
for i, stanza in ipairs(sendq) do
|
||||
send(stanza);
|
||||
sendq[i] = nil;
|
||||
end
|
||||
session.sendq = nil;
|
||||
|
@ -389,10 +385,10 @@ end
|
|||
--- Helper to check that a session peer's certificate is valid
|
||||
local function check_cert_status(session)
|
||||
local host = session.direction == "outgoing" and session.to_host or session.from_host
|
||||
local conn = session.conn:socket()
|
||||
local conn = session.conn
|
||||
local cert
|
||||
if conn.getpeercertificate then
|
||||
cert = conn:getpeercertificate()
|
||||
if conn.ssl_peercertificate then
|
||||
cert = conn:ssl_peercertificate()
|
||||
end
|
||||
|
||||
return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert });
|
||||
|
@ -404,8 +400,7 @@ local function session_secure(session)
|
|||
session.secure = true;
|
||||
session.encrypted = true;
|
||||
|
||||
local sock = session.conn:socket();
|
||||
local info = sock.info and sock:info();
|
||||
local info = session.conn:ssl_info();
|
||||
if type(info) == "table" then
|
||||
(session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
|
||||
session.compressed = info.compression;
|
||||
|
@ -434,7 +429,8 @@ function stream_callbacks._streamopened(session, attr)
|
|||
session.had_stream = true; -- Had a stream opened at least once
|
||||
|
||||
-- TODO: Rename session.secure to session.encrypted
|
||||
if session.secure == false then
|
||||
if session.secure == false then -- Set by mod_tls during STARTTLS handshake
|
||||
session.starttls = "completed";
|
||||
session_secure(session);
|
||||
end
|
||||
|
||||
|
@ -756,6 +752,7 @@ local function initialize_session(session)
|
|||
local w = conn.write;
|
||||
|
||||
if conn:ssl() then
|
||||
-- Direct TLS was used
|
||||
session_secure(session);
|
||||
end
|
||||
|
||||
|
@ -935,6 +932,16 @@ local function friendly_cert_error(session) --> string
|
|||
elseif cert_errors:contains("self signed certificate") then
|
||||
return "is self-signed";
|
||||
end
|
||||
|
||||
local chain_errors = set.new(session.cert_chain_errors[2]);
|
||||
for i, e in pairs(session.cert_chain_errors) do
|
||||
if i > 2 then chain_errors:add_list(e); end
|
||||
end
|
||||
if chain_errors:contains("certificate has expired") then
|
||||
return "has an expired certificate chain";
|
||||
elseif chain_errors:contains("No matching DANE TLSA records") then
|
||||
return "does not match any DANE TLSA records";
|
||||
end
|
||||
end
|
||||
return "is not trusted"; -- for some other reason
|
||||
elseif session.cert_identity_status == "invalid" then
|
||||
|
|
|
@ -9,7 +9,7 @@ local measure_cert_statuses = module:metric("counter", "checked", "", "Certifica
|
|||
|
||||
module:hook("s2s-check-certificate", function(event)
|
||||
local session, host, cert = event.session, event.host, event.cert;
|
||||
local conn = session.conn:socket();
|
||||
local conn = session.conn;
|
||||
local log = session.log or log;
|
||||
|
||||
if not cert then
|
||||
|
@ -18,8 +18,8 @@ module:hook("s2s-check-certificate", function(event)
|
|||
end
|
||||
|
||||
local chain_valid, errors;
|
||||
if conn.getpeerverification then
|
||||
chain_valid, errors = conn:getpeerverification();
|
||||
if conn.ssl_peerverification then
|
||||
chain_valid, errors = conn:ssl_peerverification();
|
||||
else
|
||||
chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } };
|
||||
end
|
||||
|
|
|
@ -52,7 +52,7 @@ local function handle_status(session, status, ret, err_msg)
|
|||
module:fire_event("authentication-failure", { session = session, condition = ret, text = err_msg });
|
||||
session.sasl_handler = session.sasl_handler:clean_clone();
|
||||
elseif status == "success" then
|
||||
local ok, err = sm_make_authenticated(session, session.sasl_handler.username, session.sasl_handler.scope);
|
||||
local ok, err = sm_make_authenticated(session, session.sasl_handler.username, session.sasl_handler.role);
|
||||
if ok then
|
||||
module:fire_event("authentication-success", { session = session });
|
||||
session.sasl_handler = nil;
|
||||
|
@ -242,7 +242,16 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event)
|
|||
end);
|
||||
|
||||
local function tls_unique(self)
|
||||
return self.userdata["tls-unique"]:getpeerfinished();
|
||||
return self.userdata["tls-unique"]:ssl_peerfinished();
|
||||
end
|
||||
|
||||
local function tls_exporter(conn)
|
||||
if not conn.ssl_exportkeyingmaterial then return end
|
||||
return conn:ssl_exportkeyingmaterial("EXPORTER-Channel-Binding", 32, "");
|
||||
end
|
||||
|
||||
local function sasl_tls_exporter(self)
|
||||
return tls_exporter(self.userdata["tls-exporter"]);
|
||||
end
|
||||
|
||||
local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' };
|
||||
|
@ -258,22 +267,29 @@ module:hook("stream-features", function(event)
|
|||
end
|
||||
local sasl_handler = usermanager_get_sasl_handler(module.host, origin)
|
||||
origin.sasl_handler = sasl_handler;
|
||||
local channel_bindings = set.new()
|
||||
if origin.encrypted then
|
||||
-- check whether LuaSec has the nifty binding to the function needed for tls-unique
|
||||
-- FIXME: would be nice to have this check only once and not for every socket
|
||||
if sasl_handler.add_cb_handler then
|
||||
local socket = origin.conn:socket();
|
||||
local info = socket.info and socket:info();
|
||||
if info.protocol == "TLSv1.3" then
|
||||
local info = origin.conn:ssl_info();
|
||||
if info and info.protocol == "TLSv1.3" then
|
||||
log("debug", "Channel binding 'tls-unique' undefined in context of TLS 1.3");
|
||||
elseif socket.getpeerfinished and socket:getpeerfinished() then
|
||||
if tls_exporter(origin.conn) then
|
||||
log("debug", "Channel binding 'tls-exporter' supported");
|
||||
sasl_handler:add_cb_handler("tls-exporter", sasl_tls_exporter);
|
||||
channel_bindings:add("tls-exporter");
|
||||
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
|
||||
sasl_handler["userdata"] = {
|
||||
["tls-unique"] = socket;
|
||||
["tls-unique"] = origin.conn;
|
||||
["tls-exporter"] = origin.conn;
|
||||
};
|
||||
else
|
||||
log("debug", "Channel binding not supported by SASL handler");
|
||||
|
@ -306,6 +322,14 @@ module:hook("stream-features", function(event)
|
|||
mechanisms:tag("mechanism"):text(mechanism):up();
|
||||
end
|
||||
features:add_child(mechanisms);
|
||||
if not channel_bindings:empty() then
|
||||
-- XXX XEP-0440 is Experimental
|
||||
features:tag("sasl-channel-binding", {xmlns='urn:xmpp:sasl-cb:0'})
|
||||
for channel_binding in channel_bindings do
|
||||
features:tag("channel-binding", {type=channel_binding}):up()
|
||||
end
|
||||
features:up();
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
|
@ -328,7 +352,7 @@ module:hook("stream-features", function(event)
|
|||
authmod, available_disabled);
|
||||
end
|
||||
|
||||
else
|
||||
elseif not origin.full_jid then
|
||||
features:tag("bind", bind_attr):tag("required"):up():up();
|
||||
features:tag("session", xmpp_session_attr):tag("optional"):up():up();
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
--
|
||||
-- Copyright (C) 2010-2015 Matthew Wild
|
||||
-- Copyright (C) 2010 Waqas Hussain
|
||||
-- Copyright (C) 2012-2021 Kim Alvefur
|
||||
-- Copyright (C) 2012-2022 Kim Alvefur
|
||||
-- Copyright (C) 2012 Thijs Alkemade
|
||||
-- Copyright (C) 2014 Florian Zeitz
|
||||
-- Copyright (C) 2016-2020 Thilo Molitor
|
||||
|
@ -10,6 +10,7 @@
|
|||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
--
|
||||
-- TODO unify sendq and smqueue
|
||||
|
||||
local tonumber = tonumber;
|
||||
local tostring = tostring;
|
||||
|
@ -83,6 +84,22 @@ 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 track_session(session, id)
|
||||
session_registry[jid.join(session.username, session.host, id or session.resumption_token)] = session;
|
||||
session.resumption_token = id;
|
||||
end
|
||||
|
||||
local function save_old_session(session)
|
||||
session_registry[jid.join(session.username, session.host, session.resumption_token)] = 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[jid.join(session.username, session.host, id or session.resumption_token)] = nil;
|
||||
return old_session_registry:set(session.username, id or session.resumption_token, nil)
|
||||
end
|
||||
|
||||
local ack_errors = require"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" };
|
||||
|
@ -90,6 +107,16 @@ local ack_errors = require"util.error".init("mod_smacks", xmlns_sm3, {
|
|||
overflow = { condition = "resource-constraint", text = "Too many unacked stanzas remaining, session can't be resumed" }
|
||||
});
|
||||
|
||||
local enable_errors = require "util.error".init("mod_smacks", xmlns_sm3, {
|
||||
already_enabled = { condition = "unexpected-request", text = "Stream management is already enabled" };
|
||||
bind_required = { condition = "unexpected-request", text = "Client must bind a resource before enabling stream management" };
|
||||
unavailable = { condition = "service-unavailable", text = "Stream management is not available for this stream" };
|
||||
-- Resumption
|
||||
expired = { condition = "item-not-found", text = "Session expired, and cannot be resumed" };
|
||||
already_bound = { condition = "unexpected-request", text = "Cannot resume another session after a resource is bound" };
|
||||
unknown_session = { condition = "item-not-found", text = "Unknown session" };
|
||||
});
|
||||
|
||||
-- COMPAT note the use of compatibility wrapper in events (queue:table())
|
||||
|
||||
local function ack_delayed(session, stanza)
|
||||
|
@ -104,18 +131,18 @@ local function ack_delayed(session, stanza)
|
|||
end
|
||||
|
||||
local function can_do_smacks(session, advertise_only)
|
||||
if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end
|
||||
if session.smacks then return false, enable_errors.new("already_enabled"); end
|
||||
|
||||
local session_type = session.type;
|
||||
if session.username then
|
||||
if not(advertise_only) and not(session.resource) then -- Fail unless we're only advertising sm
|
||||
return false, "unexpected-request", "Client must bind a resource before enabling stream management";
|
||||
return false, enable_errors.new("bind_required");
|
||||
end
|
||||
return true;
|
||||
elseif s2s_smacks and (session_type == "s2sin" or session_type == "s2sout") then
|
||||
return true;
|
||||
end
|
||||
return false, "service-unavailable", "Stream management is not available for this stream";
|
||||
return false, enable_errors.new("unavailable");
|
||||
end
|
||||
|
||||
module:hook("stream-features",
|
||||
|
@ -155,13 +182,12 @@ end
|
|||
|
||||
local function request_ack(session, reason)
|
||||
local queue = session.outgoing_stanza_queue;
|
||||
session.log("debug", "Sending <r> (inside timer, before send) from %s - #queue=%d", reason, queue:count_unacked());
|
||||
session.log("debug", "Sending <r> from %s - #queue=%d", reason, queue:count_unacked());
|
||||
session.awaiting_ack = true;
|
||||
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }))
|
||||
if session.destroyed then return end -- sending something can trigger destruction
|
||||
-- expected_h could be lower than this expression e.g. more stanzas added to the queue meanwhile)
|
||||
session.last_requested_h = queue:count_acked() + queue:count_unacked();
|
||||
session.log("debug", "Sending <r> (inside timer, after send) from %s - #queue=%d", reason, queue:count_unacked());
|
||||
if not session.delayed_ack_timer then
|
||||
session.delayed_ack_timer = timer.add_task(delayed_ack_timeout, function()
|
||||
ack_delayed(session, nil); -- we don't know if this is the only new stanza in the queue
|
||||
|
@ -180,7 +206,6 @@ local function outgoing_stanza_filter(stanza, session)
|
|||
-- supposed to be nil.
|
||||
-- However, when using mod_smacks with mod_websocket, then mod_websocket's
|
||||
-- stanzas/out filter can get called before this one and adds the xmlns.
|
||||
if session.resending_unacked then return stanza end
|
||||
if not session.smacks then return stanza end
|
||||
local is_stanza = st.is_stanza(stanza) and
|
||||
(not stanza.attr.xmlns or stanza.attr.xmlns == 'jabber:client')
|
||||
|
@ -234,8 +259,7 @@ module:hook("pre-session-close", function(event)
|
|||
if session.smacks == nil then return end
|
||||
if session.resumption_token then
|
||||
session.log("debug", "Revoking resumption token");
|
||||
session_registry[jid.join(session.username, session.host, session.resumption_token)] = nil;
|
||||
old_session_registry:set(session.username, session.resumption_token, nil);
|
||||
clear_old_session(session);
|
||||
session.resumption_token = nil;
|
||||
else
|
||||
session.log("debug", "Session not resumable");
|
||||
|
@ -274,17 +298,16 @@ local function wrap_session(session, resume)
|
|||
return session;
|
||||
end
|
||||
|
||||
function handle_enable(session, stanza, xmlns_sm)
|
||||
local ok, err, err_text = can_do_smacks(session);
|
||||
function do_enable(session, stanza)
|
||||
local ok, err = can_do_smacks(session);
|
||||
if not ok then
|
||||
session.log("warn", "Failed to enable smacks: %s", err_text); -- TODO: XEP doesn't say we can send error text, should it?
|
||||
(session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):tag(err, { xmlns = xmlns_errors}));
|
||||
return true;
|
||||
session.log("warn", "Failed to enable smacks: %s", err.text); -- TODO: XEP doesn't say we can send error text, should it?
|
||||
return nil, err;
|
||||
end
|
||||
|
||||
if session.username then
|
||||
local old_sessions, err = all_old_sessions:get(session.username);
|
||||
module:log("debug", "Old sessions: %q", old_sessions)
|
||||
session.log("debug", "Old sessions: %q", old_sessions)
|
||||
if old_sessions then
|
||||
local keep, count = {}, 0;
|
||||
for token, info in it.sorted_pairs(old_sessions, function(a, b)
|
||||
|
@ -296,54 +319,73 @@ function handle_enable(session, stanza, xmlns_sm)
|
|||
end
|
||||
all_old_sessions:set(session.username, keep);
|
||||
elseif err then
|
||||
module:log("error", "Unable to retrieve old resumption counters: %s", err);
|
||||
session.log("error", "Unable to retrieve old resumption counters: %s", err);
|
||||
end
|
||||
end
|
||||
|
||||
module:log("debug", "Enabling stream management");
|
||||
session.smacks = xmlns_sm;
|
||||
|
||||
wrap_session(session, false);
|
||||
|
||||
local resume_max;
|
||||
local resume_token;
|
||||
local resume = stanza.attr.resume;
|
||||
if (resume == "true" or resume == "1") and session.username then
|
||||
-- resumption on s2s is not currently supported
|
||||
resume_token = new_id();
|
||||
session_registry[jid.join(session.username, session.host, resume_token)] = session;
|
||||
session.resumption_token = resume_token;
|
||||
resume_max = tostring(resume_timeout);
|
||||
end
|
||||
(session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume, max = resume_max }));
|
||||
|
||||
return {
|
||||
type = "enabled";
|
||||
id = resume_token;
|
||||
resume_max = resume_token and tostring(resume_timeout) or nil;
|
||||
session = session;
|
||||
finish = function ()
|
||||
session.log("debug", "Enabling stream management");
|
||||
|
||||
session.smacks = stanza.attr.xmlns;
|
||||
if resume_token then
|
||||
track_session(session, resume_token);
|
||||
end
|
||||
wrap_session(session, false);
|
||||
end;
|
||||
};
|
||||
end
|
||||
|
||||
function handle_enable(session, stanza, xmlns_sm)
|
||||
local enabled, err = do_enable(session, stanza);
|
||||
if not enabled then
|
||||
(session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):add_error(err));
|
||||
return true;
|
||||
end
|
||||
|
||||
(session.sends2s or session.send)(st.stanza("enabled", {
|
||||
xmlns = xmlns_sm;
|
||||
id = enabled.id;
|
||||
resume = enabled.id and "true" or nil; -- COMPAT w/ Conversations 2.10.10 requires 'true' not '1'
|
||||
max = enabled.resume_max;
|
||||
}));
|
||||
|
||||
session.smacks = xmlns_sm;
|
||||
enabled.finish();
|
||||
|
||||
return true;
|
||||
end
|
||||
module:hook_tag(xmlns_sm2, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm2); end, 100);
|
||||
module:hook_tag(xmlns_sm3, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm3); end, 100);
|
||||
|
||||
module:hook_tag("http://etherx.jabber.org/streams", "features",
|
||||
function (session, stanza)
|
||||
-- Needs to be done after flushing sendq since those aren't stored as
|
||||
-- stanzas and counting them is weird.
|
||||
-- TODO unify sendq and smqueue
|
||||
timer.add_task(1e-6, function ()
|
||||
if can_do_smacks(session) then
|
||||
if stanza:get_child("sm", xmlns_sm3) then
|
||||
session.sends2s(st.stanza("enable", sm3_attr));
|
||||
session.smacks = xmlns_sm3;
|
||||
elseif stanza:get_child("sm", xmlns_sm2) then
|
||||
session.sends2s(st.stanza("enable", sm2_attr));
|
||||
session.smacks = xmlns_sm2;
|
||||
else
|
||||
return;
|
||||
end
|
||||
wrap_session_out(session, false);
|
||||
end
|
||||
end);
|
||||
end);
|
||||
module:hook_tag("http://etherx.jabber.org/streams", "features", function(session, stanza)
|
||||
if can_do_smacks(session) then
|
||||
session.smacks_feature = stanza:get_child("sm", xmlns_sm3) or stanza:get_child("sm", xmlns_sm2);
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook("s2sout-established", function (event)
|
||||
local session = event.session;
|
||||
if not session.smacks_feature then return end
|
||||
|
||||
session.smacks = session.smacks_feature.attr.xmlns;
|
||||
wrap_session_out(session, false);
|
||||
session.sends2s(st.stanza("enable", { xmlns = session.smacks }));
|
||||
end);
|
||||
|
||||
function handle_enabled(session, stanza, xmlns_sm) -- luacheck: ignore 212/stanza
|
||||
module:log("debug", "Enabling stream management");
|
||||
session.log("debug", "Enabling stream management");
|
||||
session.smacks = xmlns_sm;
|
||||
|
||||
wrap_session_in(session, false);
|
||||
|
@ -357,10 +399,10 @@ module:hook_tag(xmlns_sm3, "enabled", function (session, stanza) return handle_e
|
|||
|
||||
function handle_r(origin, stanza, xmlns_sm) -- luacheck: ignore 212/stanza
|
||||
if not origin.smacks then
|
||||
module:log("debug", "Received ack request from non-smack-enabled session");
|
||||
origin.log("debug", "Received ack request from non-smack-enabled session");
|
||||
return;
|
||||
end
|
||||
module:log("debug", "Received ack request, acking for %d", origin.handled_stanza_count);
|
||||
origin.log("debug", "Received ack request, acking for %d", origin.handled_stanza_count);
|
||||
-- Reply with <a>
|
||||
(origin.sends2s or origin.send)(st.stanza("a", { xmlns = xmlns_sm, h = format_h(origin.handled_stanza_count) }));
|
||||
-- piggyback our own ack request if needed (see request_ack_if_needed() for explanation of last_requested_h)
|
||||
|
@ -413,13 +455,14 @@ local function handle_unacked_stanzas(session)
|
|||
local queue = session.outgoing_stanza_queue;
|
||||
local unacked = queue:count_unacked()
|
||||
if unacked > 0 then
|
||||
local error_from = jid.join(session.username, session.host or module.host);
|
||||
tx_dropped_stanzas:sample(unacked);
|
||||
session.smacks = false; -- Disable queueing
|
||||
session.outgoing_stanza_queue = nil;
|
||||
for stanza in queue._queue:consume() do
|
||||
if not module:fire_event("delivery/failure", { session = session, stanza = stanza }) then
|
||||
if stanza.attr.type ~= "error" and stanza.attr.from ~= session.full_jid then
|
||||
local reply = st.error_reply(stanza, "cancel", "recipient-unavailable");
|
||||
local reply = st.error_reply(stanza, "cancel", "recipient-unavailable", nil, error_from);
|
||||
module:send(reply);
|
||||
end
|
||||
end
|
||||
|
@ -486,11 +529,8 @@ module:hook("pre-resource-unbind", function (event)
|
|||
end
|
||||
|
||||
session.log("debug", "Destroying session for hibernating too long");
|
||||
session_registry[jid.join(session.username, session.host, session.resumption_token)] = nil;
|
||||
old_session_registry:set(session.username, session.resumption_token,
|
||||
{ h = session.handled_stanza_count; t = os.time() });
|
||||
save_old_session(session);
|
||||
session.resumption_token = nil;
|
||||
session.resending_unacked = true; -- stop outgoing_stanza_filter from re-queueing anything anymore
|
||||
sessionmanager.destroy_session(session, "Hibernating too long");
|
||||
sessions_expired(1);
|
||||
end);
|
||||
|
@ -523,17 +563,10 @@ end
|
|||
module:hook("s2sout-destroyed", handle_s2s_destroyed);
|
||||
module:hook("s2sin-destroyed", handle_s2s_destroyed);
|
||||
|
||||
local function get_session_id(session)
|
||||
return session.id or (tostring(session):match("[a-f0-9]+$"));
|
||||
end
|
||||
|
||||
function handle_resume(session, stanza, xmlns_sm)
|
||||
function do_resume(session, stanza)
|
||||
if session.full_jid then
|
||||
session.log("warn", "Tried to resume after resource binding");
|
||||
session.send(st.stanza("failed", { xmlns = xmlns_sm })
|
||||
:tag("unexpected-request", { xmlns = xmlns_errors })
|
||||
);
|
||||
return true;
|
||||
return nil, enable_errors.new("already_bound");
|
||||
end
|
||||
|
||||
local id = stanza.attr.previd;
|
||||
|
@ -542,112 +575,98 @@ function handle_resume(session, stanza, xmlns_sm)
|
|||
local old_session = old_session_registry:get(session.username, id);
|
||||
if old_session then
|
||||
session.log("debug", "Tried to resume old expired session with id %s", id);
|
||||
session.send(st.stanza("failed", { xmlns = xmlns_sm, h = format_h(old_session.h) })
|
||||
:tag("item-not-found", { xmlns = xmlns_errors })
|
||||
);
|
||||
old_session_registry:set(session.username, id, nil);
|
||||
clear_old_session(session, id);
|
||||
resumption_expired(1);
|
||||
else
|
||||
session.log("debug", "Tried to resume non-existent session with id %s", id);
|
||||
session.send(st.stanza("failed", { xmlns = xmlns_sm })
|
||||
:tag("item-not-found", { xmlns = xmlns_errors })
|
||||
);
|
||||
end;
|
||||
else
|
||||
if original_session.hibernating_watchdog then
|
||||
original_session.log("debug", "Letting the watchdog go");
|
||||
original_session.hibernating_watchdog:cancel();
|
||||
original_session.hibernating_watchdog = nil;
|
||||
elseif session.hibernating then
|
||||
original_session.log("error", "Hibernating session has no watchdog!")
|
||||
return nil, enable_errors.new("expired", { h = old_session.h });
|
||||
end
|
||||
-- zero age = was not hibernating yet
|
||||
local age = 0;
|
||||
if original_session.hibernating then
|
||||
local now = os_time();
|
||||
age = now - original_session.hibernating;
|
||||
end
|
||||
session.log("debug", "mod_smacks resuming existing session %s...", get_session_id(original_session));
|
||||
original_session.log("debug", "mod_smacks session resumed from %s...", get_session_id(session));
|
||||
-- TODO: All this should move to sessionmanager (e.g. session:replace(new_session))
|
||||
if original_session.conn then
|
||||
original_session.log("debug", "mod_smacks closing an old connection for this session");
|
||||
local conn = original_session.conn;
|
||||
c2s_sessions[conn] = nil;
|
||||
conn:close();
|
||||
end
|
||||
|
||||
local migrated_session_log = session.log;
|
||||
original_session.ip = session.ip;
|
||||
original_session.conn = session.conn;
|
||||
original_session.rawsend = session.rawsend;
|
||||
original_session.rawsend.session = original_session;
|
||||
original_session.rawsend.conn = original_session.conn;
|
||||
original_session.send = session.send;
|
||||
original_session.send.session = original_session;
|
||||
original_session.close = session.close;
|
||||
original_session.filter = session.filter;
|
||||
original_session.filter.session = original_session;
|
||||
original_session.filters = session.filters;
|
||||
original_session.send.filter = original_session.filter;
|
||||
original_session.stream = session.stream;
|
||||
original_session.secure = session.secure;
|
||||
original_session.hibernating = nil;
|
||||
original_session.resumption_counter = (original_session.resumption_counter or 0) + 1;
|
||||
session.log = original_session.log;
|
||||
session.type = original_session.type;
|
||||
wrap_session(original_session, true);
|
||||
-- Inform xmppstream of the new session (passed to its callbacks)
|
||||
original_session.stream:set_session(original_session);
|
||||
-- Similar for connlisteners
|
||||
c2s_sessions[session.conn] = original_session;
|
||||
|
||||
local queue = original_session.outgoing_stanza_queue;
|
||||
local h = tonumber(stanza.attr.h);
|
||||
|
||||
original_session.log("debug", "Pre-resumption #queue = %d", queue:count_unacked())
|
||||
local acked, err = ack_errors.coerce(queue:ack(h)); -- luacheck: ignore 211/acked
|
||||
|
||||
if not err and not queue:resumable() then
|
||||
err = ack_errors.new("overflow");
|
||||
end
|
||||
|
||||
if err or not queue:resumable() then
|
||||
original_session.send(st.stanza("failed",
|
||||
{ xmlns = xmlns_sm; h = format_h(original_session.handled_stanza_count); previd = id }));
|
||||
original_session:close(err);
|
||||
return false;
|
||||
end
|
||||
|
||||
original_session.send(st.stanza("resumed", { xmlns = xmlns_sm,
|
||||
h = format_h(original_session.handled_stanza_count), previd = id }));
|
||||
|
||||
-- Ok, we need to re-send any stanzas that the client didn't see
|
||||
-- ...they are what is now left in the outgoing stanza queue
|
||||
-- We have to use the send of "session" because we don't want to add our resent stanzas
|
||||
-- to the outgoing queue again
|
||||
|
||||
session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", queue:count_unacked());
|
||||
-- FIXME Which session is it that the queue filter sees?
|
||||
session.resending_unacked = true;
|
||||
original_session.resending_unacked = true;
|
||||
for _, queued_stanza in queue:resume() do
|
||||
session.send(queued_stanza);
|
||||
end
|
||||
session.resending_unacked = nil;
|
||||
original_session.resending_unacked = nil;
|
||||
session.log("debug", "all stanzas resent, now disabling send() in this migrated session, #queue = %d", queue:count_unacked());
|
||||
function session.send(stanza) -- luacheck: ignore 432
|
||||
migrated_session_log("error", "Tried to send stanza on old session migrated by smacks resume (maybe there is a bug?): %s", tostring(stanza));
|
||||
return false;
|
||||
end
|
||||
module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue:table()});
|
||||
original_session.awaiting_ack = nil; -- Don't wait for acks from before the resumption
|
||||
request_ack_now_if_needed(original_session, true, "handle_resume", nil);
|
||||
resumption_age:sample(age);
|
||||
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);
|
||||
|
||||
|
@ -702,8 +721,7 @@ module:hook_global("server-stopping", function(event)
|
|||
for _, user in pairs(local_sessions) do
|
||||
for _, session in pairs(user.sessions) do
|
||||
if session.resumption_token then
|
||||
if old_session_registry:set(session.username, session.resumption_token,
|
||||
{ h = session.handled_stanza_count; t = os.time() }) then
|
||||
if save_old_session(session) then
|
||||
session.resumption_token = nil;
|
||||
|
||||
-- Deal with unacked stanzas
|
||||
|
|
|
@ -13,7 +13,7 @@ local is_stanza = require"util.stanza".is_stanza;
|
|||
local t_concat = table.concat;
|
||||
|
||||
local noop = function() end
|
||||
local unpack = table.unpack or unpack; -- luacheck: ignore 113
|
||||
local unpack = table.unpack;
|
||||
local function iterator(result)
|
||||
return function(result_)
|
||||
local row = result_();
|
||||
|
@ -321,7 +321,8 @@ function archive_store:append(username, key, value, when, with)
|
|||
end
|
||||
end
|
||||
|
||||
when = when or os.time();
|
||||
-- FIXME update the schema to allow precision timestamps
|
||||
when = when and math.floor(when) or os.time();
|
||||
with = with or "";
|
||||
local ok, ret = engine:transaction(function()
|
||||
local delete_sql = [[
|
||||
|
@ -354,12 +355,12 @@ end
|
|||
local function archive_where(query, args, where)
|
||||
-- Time range, inclusive
|
||||
if query.start then
|
||||
args[#args+1] = query.start
|
||||
args[#args+1] = math.floor(query.start);
|
||||
where[#where+1] = "\"when\" >= ?"
|
||||
end
|
||||
|
||||
if query["end"] then
|
||||
args[#args+1] = query["end"];
|
||||
args[#args+1] = math.floor(query["end"]);
|
||||
if query.start then
|
||||
where[#where] = "\"when\" BETWEEN ? AND ?" -- is this inclusive?
|
||||
else
|
||||
|
@ -382,8 +383,7 @@ local function archive_where(query, args, where)
|
|||
-- Set of ids
|
||||
if query.ids then
|
||||
local nids, nargs = #query.ids, #args;
|
||||
-- COMPAT Lua 5.1: No separator argument to string.rep
|
||||
where[#where + 1] = "\"key\" IN (" .. string.rep("?,", nids):sub(1,-2) .. ")";
|
||||
where[#where + 1] = "\"key\" IN (" .. string.rep("?", nids, ",") .. ")";
|
||||
for i, id in ipairs(query.ids) do
|
||||
args[nargs+i] = id;
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
local ipairs, pairs = ipairs, pairs;
|
||||
local setmetatable = setmetatable;
|
||||
local tostring = tostring;
|
||||
local next, unpack = next, table.unpack or unpack; --luacheck: ignore 113/unpack
|
||||
local next, unpack = next, table.unpack;
|
||||
local os_remove = os.remove;
|
||||
local io_open = io.open;
|
||||
local jid_bare = require "util.jid".bare;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
local st = require "util.stanza";
|
||||
local datetime = require "util.datetime".datetime;
|
||||
local legacy = require "util.datetime".legacy;
|
||||
local now = require "util.time".now;
|
||||
|
||||
-- XEP-0202: Entity Time
|
||||
|
||||
|
@ -18,23 +18,10 @@ local function time_handler(event)
|
|||
local origin, stanza = event.origin, event.stanza;
|
||||
origin.send(st.reply(stanza):tag("time", {xmlns="urn:xmpp:time"})
|
||||
:tag("tzo"):text("+00:00"):up() -- TODO get the timezone in a platform independent fashion
|
||||
:tag("utc"):text(datetime()));
|
||||
:tag("utc"):text(datetime(now())));
|
||||
return true;
|
||||
end
|
||||
|
||||
module:hook("iq-get/bare/urn:xmpp:time:time", time_handler);
|
||||
module:hook("iq-get/host/urn:xmpp:time:time", time_handler);
|
||||
|
||||
-- XEP-0090: Entity Time (deprecated)
|
||||
|
||||
module:add_feature("jabber:iq:time");
|
||||
|
||||
local function legacy_time_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
origin.send(st.reply(stanza):tag("query", {xmlns="jabber:iq:time"})
|
||||
:tag("utc"):text(legacy()));
|
||||
return true;
|
||||
end
|
||||
|
||||
module:hook("iq-get/bare/jabber:iq:time:query", legacy_time_handler);
|
||||
module:hook("iq-get/host/jabber:iq:time:query", legacy_time_handler);
|
||||
|
|
|
@ -80,6 +80,9 @@ end
|
|||
module:hook_global("config-reloaded", module.load);
|
||||
|
||||
local function can_do_tls(session)
|
||||
if session.secure then
|
||||
return false;
|
||||
end
|
||||
if session.conn and not session.conn.starttls then
|
||||
if not session.secure then
|
||||
session.log("debug", "Underlying connection does not support STARTTLS");
|
||||
|
@ -125,7 +128,15 @@ end);
|
|||
-- Hook <starttls/>
|
||||
module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event)
|
||||
local origin = event.origin;
|
||||
origin.starttls = "requested";
|
||||
if can_do_tls(origin) then
|
||||
if origin.conn.block_reads then
|
||||
-- we need to ensure that no data is read anymore, otherwise we could end up in a situation where
|
||||
-- <proceed/> is sent and the socket receives the TLS handshake (and passes the data to lua) before
|
||||
-- it is asked to initiate TLS
|
||||
-- (not with the classical single-threaded server backends)
|
||||
origin.conn:block_reads()
|
||||
end
|
||||
(origin.sends2s or origin.send)(starttls_proceed);
|
||||
if origin.destroyed then return end
|
||||
origin:reset_stream();
|
||||
|
@ -166,6 +177,7 @@ module:hook_tag("http://etherx.jabber.org/streams", "features", function (sessio
|
|||
module:log("debug", "%s is not offering TLS", session.to_host);
|
||||
return;
|
||||
end
|
||||
session.starttls = "initiated";
|
||||
session.sends2s(starttls_initiate);
|
||||
return true;
|
||||
end
|
||||
|
@ -183,7 +195,8 @@ module:hook_tag(xmlns_starttls, "proceed", function (session, stanza) -- luachec
|
|||
if session.type == "s2sout_unauthed" and can_do_tls(session) then
|
||||
module:log("debug", "Proceeding with TLS on s2sout...");
|
||||
session:reset_stream();
|
||||
session.conn:starttls(session.ssl_ctx);
|
||||
session.starttls = "proceeding"
|
||||
session.conn:starttls(session.ssl_ctx, session.to_host);
|
||||
session.secure = false;
|
||||
return true;
|
||||
end
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
local id = require "util.id";
|
||||
local jid = require "util.jid";
|
||||
local base64 = require "util.encodings".base64;
|
||||
local usermanager = require "core.usermanager";
|
||||
local generate_identifier = require "util.id".short;
|
||||
|
||||
local token_store = module:open_store("auth_tokens", "map");
|
||||
|
||||
function create_jid_token(actor_jid, token_jid, token_scope, token_ttl)
|
||||
local function select_role(username, host, role)
|
||||
if role then
|
||||
return prosody.hosts[host].authz.get_role_by_name(role);
|
||||
end
|
||||
return usermanager.get_user_role(username, host);
|
||||
end
|
||||
|
||||
function create_jid_token(actor_jid, token_jid, token_role, token_ttl, token_data)
|
||||
token_jid = jid.prep(token_jid);
|
||||
if not actor_jid or token_jid ~= actor_jid and not jid.compare(token_jid, actor_jid) then
|
||||
return nil, "not-authorized";
|
||||
|
@ -21,13 +30,10 @@ function create_jid_token(actor_jid, token_jid, token_scope, token_ttl)
|
|||
created = os.time();
|
||||
expires = token_ttl and (os.time() + token_ttl) or nil;
|
||||
jid = token_jid;
|
||||
session = {
|
||||
username = token_username;
|
||||
host = token_host;
|
||||
resource = token_resource;
|
||||
|
||||
auth_scope = token_scope;
|
||||
};
|
||||
resource = token_resource;
|
||||
role = token_role;
|
||||
data = token_data;
|
||||
};
|
||||
|
||||
local token_id = id.long();
|
||||
|
@ -46,11 +52,7 @@ local function parse_token(encoded_token)
|
|||
return token_id, token_user, token_host;
|
||||
end
|
||||
|
||||
function get_token_info(token)
|
||||
local token_id, token_user, token_host = parse_token(token);
|
||||
if not token_id then
|
||||
return nil, "invalid-token-format";
|
||||
end
|
||||
local function _get_parsed_token_info(token_id, token_user, token_host)
|
||||
if token_host ~= module.host then
|
||||
return nil, "invalid-host";
|
||||
end
|
||||
|
@ -64,12 +66,47 @@ function get_token_info(token)
|
|||
end
|
||||
|
||||
if token_info.expires and token_info.expires < os.time() then
|
||||
token_store:set(token_user, token_id, nil);
|
||||
return nil, "not-authorized";
|
||||
end
|
||||
|
||||
local account_info = usermanager.get_account_info(token_user, module.host);
|
||||
local password_updated_at = account_info and account_info.password_updated;
|
||||
if password_updated_at and password_updated_at > token_info.created then
|
||||
token_store:set(token_user, token_id, nil);
|
||||
return nil, "not-authorized";
|
||||
end
|
||||
|
||||
return token_info
|
||||
end
|
||||
|
||||
function get_token_info(token)
|
||||
local token_id, token_user, token_host = parse_token(token);
|
||||
if not token_id then
|
||||
return nil, "invalid-token-format";
|
||||
end
|
||||
return _get_parsed_token_info(token_id, token_user, token_host);
|
||||
end
|
||||
|
||||
function get_token_session(token, resource)
|
||||
local token_id, token_user, token_host = parse_token(token);
|
||||
if not token_id then
|
||||
return nil, "invalid-token-format";
|
||||
end
|
||||
|
||||
local token_info, err = _get_parsed_token_info(token_id, token_user, token_host);
|
||||
if not token_info then return nil, err; end
|
||||
|
||||
return {
|
||||
username = token_user;
|
||||
host = token_host;
|
||||
resource = token_info.resource or resource or generate_identifier();
|
||||
|
||||
role = select_role(token_user, token_host, token_info.role);
|
||||
};
|
||||
end
|
||||
|
||||
|
||||
function revoke_token(token)
|
||||
local token_id, token_user, token_host = parse_token(token);
|
||||
if not token_id then
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
--
|
||||
|
||||
local restrict_public = not module:get_option_boolean("muc_room_allow_public", true);
|
||||
local um_is_admin = require "core.usermanager".is_admin;
|
||||
module:default_permission(restrict_public and "prosody:admin" or "prosody:user", ":create-public-room");
|
||||
|
||||
local function get_hidden(room)
|
||||
return room._data.hidden;
|
||||
|
@ -22,8 +22,8 @@ local function set_hidden(room, hidden)
|
|||
end
|
||||
|
||||
module:hook("muc-config-form", function(event)
|
||||
if restrict_public and not um_is_admin(event.actor, module.host) then
|
||||
-- Don't show option if public rooms are restricted and user is not admin of this host
|
||||
if not module:may(":create-public-room", event.actor) then
|
||||
-- Hide config option if this user is not allowed to create public rooms
|
||||
return;
|
||||
end
|
||||
table.insert(event.form, {
|
||||
|
@ -36,7 +36,7 @@ module:hook("muc-config-form", function(event)
|
|||
end, 100-9);
|
||||
|
||||
module:hook("muc-config-submitted/muc#roomconfig_publicroom", function(event)
|
||||
if restrict_public and not um_is_admin(event.actor, module.host) then
|
||||
if not module:may(":create-public-room", event.actor) then
|
||||
return; -- Not allowed
|
||||
end
|
||||
if set_hidden(event.room, not event.value) then
|
||||
|
|
|
@ -100,7 +100,6 @@ local jid_prep = require "util.jid".prep;
|
|||
local jid_bare = require "util.jid".bare;
|
||||
local st = require "util.stanza";
|
||||
local cache = require "util.cache";
|
||||
local um_is_admin = require "core.usermanager".is_admin;
|
||||
|
||||
module:require "muc/config_form_sections";
|
||||
|
||||
|
@ -111,21 +110,23 @@ module:depends "muc_unique"
|
|||
module:require "muc/hats";
|
||||
module:require "muc/lock";
|
||||
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
module:default_permissions("prosody:admin", {
|
||||
":automatic-ownership";
|
||||
":create-room";
|
||||
":recreate-destroyed-room";
|
||||
});
|
||||
|
||||
if module:get_option_boolean("component_admins_as_room_owners", true) then
|
||||
-- Monkey patch to make server admins room owners
|
||||
local _get_affiliation = room_mt.get_affiliation;
|
||||
function room_mt:get_affiliation(jid)
|
||||
if is_admin(jid) then return "owner"; end
|
||||
if module:may(":automatic-ownership", jid) then return "owner"; end
|
||||
return _get_affiliation(self, jid);
|
||||
end
|
||||
|
||||
local _set_affiliation = room_mt.set_affiliation;
|
||||
function room_mt:set_affiliation(actor, jid, affiliation, reason, data)
|
||||
if affiliation ~= "owner" and is_admin(jid) then return nil, "modify", "not-acceptable"; end
|
||||
if affiliation ~= "owner" and module:may(":automatic-ownership", jid) then return nil, "modify", "not-acceptable"; end
|
||||
return _set_affiliation(self, actor, jid, affiliation, reason, data);
|
||||
end
|
||||
end
|
||||
|
@ -412,26 +413,15 @@ if module:get_option_boolean("muc_tombstones", true) then
|
|||
end, -10);
|
||||
end
|
||||
|
||||
do
|
||||
local restrict_room_creation = module:get_option("restrict_room_creation");
|
||||
if restrict_room_creation == true then
|
||||
restrict_room_creation = "admin";
|
||||
local restrict_room_creation = module:get_option("restrict_room_creation");
|
||||
module:default_permission(restrict_room_creation == true and "prosody:admin" or "prosody:user", ":create-room");
|
||||
module:hook("muc-room-pre-create", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
if restrict_room_creation ~= false and not module:may(":create-room", event) then
|
||||
origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted", module.host));
|
||||
return true;
|
||||
end
|
||||
if restrict_room_creation then
|
||||
local host_suffix = module.host:gsub("^[^%.]+%.", "");
|
||||
module:hook("muc-room-pre-create", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local user_jid = stanza.attr.from;
|
||||
if not is_admin(user_jid) and not (
|
||||
restrict_room_creation == "local" and
|
||||
select(2, jid_split(user_jid)) == host_suffix
|
||||
) then
|
||||
origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted", module.host));
|
||||
return true;
|
||||
end
|
||||
end);
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
for event_name, method in pairs {
|
||||
-- Normal room interactions
|
||||
|
@ -465,7 +455,7 @@ for event_name, method in pairs {
|
|||
|
||||
if room and room._data.destroyed then
|
||||
if room._data.locked < os.time()
|
||||
or (is_admin(stanza.attr.from) and stanza.name == "presence" and stanza.attr.type == nil) then
|
||||
or (module:may(":recreate-destroyed-room", event) and stanza.name == "presence" and stanza.attr.type == nil) then
|
||||
-- Allow the room to be recreated by admin or after time has passed
|
||||
delete_room(room);
|
||||
room = nil;
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
--
|
||||
|
||||
local restrict_persistent = not module:get_option_boolean("muc_room_allow_persistent", true);
|
||||
local um_is_admin = require "core.usermanager".is_admin;
|
||||
module:default_permission(
|
||||
restrict_persistent and "prosody:admin" or "prosody:user",
|
||||
":create-persistent-room"
|
||||
);
|
||||
|
||||
local function get_persistent(room)
|
||||
return room._data.persistent;
|
||||
|
@ -22,8 +25,8 @@ local function set_persistent(room, persistent)
|
|||
end
|
||||
|
||||
module:hook("muc-config-form", function(event)
|
||||
if restrict_persistent and not um_is_admin(event.actor, module.host) then
|
||||
-- Don't show option if hidden rooms are restricted and user is not admin of this host
|
||||
if not module:may(":create-persistent-room", event.actor) then
|
||||
-- Hide config option if this user is not allowed to create persistent rooms
|
||||
return;
|
||||
end
|
||||
table.insert(event.form, {
|
||||
|
@ -36,7 +39,7 @@ module:hook("muc-config-form", function(event)
|
|||
end, 100-5);
|
||||
|
||||
module:hook("muc-config-submitted/muc#roomconfig_persistentroom", function(event)
|
||||
if restrict_persistent and not um_is_admin(event.actor, module.host) then
|
||||
if not module:may(":create-persistent-room", event.actor) then
|
||||
return; -- Not allowed
|
||||
end
|
||||
if set_persistent(event.room, event.value) then
|
||||
|
|
6
prosody
6
prosody
|
@ -44,6 +44,12 @@ if CFG_DATADIR then
|
|||
end
|
||||
|
||||
|
||||
-- Check before first require, to preempt the probable failure
|
||||
if _VERSION < "Lua 5.2" then
|
||||
io.stderr:write("Prosody is no longer compatible with Lua 5.1\n")
|
||||
io.stderr:write("See https://prosody.im/doc/depends#lua for more information\n")
|
||||
return os.exit(1);
|
||||
end
|
||||
|
||||
local startup = require "util.startup";
|
||||
local async = require "util.async";
|
||||
|
|
|
@ -44,6 +44,13 @@ end
|
|||
|
||||
-----------
|
||||
|
||||
-- Check before first require, to preempt the probable failure
|
||||
if _VERSION < "Lua 5.2" then
|
||||
io.stderr:write("Prosody is no longer compatible with Lua 5.1\n")
|
||||
io.stderr:write("See https://prosody.im/doc/depends#lua for more information\n")
|
||||
return os.exit(1);
|
||||
end
|
||||
|
||||
local startup = require "util.startup";
|
||||
startup.prosodyctl();
|
||||
|
||||
|
@ -573,7 +580,7 @@ function commands.reload(arg)
|
|||
end
|
||||
-- ejabberdctl compatibility
|
||||
|
||||
local unpack = table.unpack or unpack; -- luacheck: ignore 113
|
||||
local unpack = table.unpack;
|
||||
|
||||
function commands.register(arg)
|
||||
local user, host, password = unpack(arg);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
local unpack = table.unpack or unpack; -- luacheck: ignore 113
|
||||
local unpack = table.unpack;
|
||||
local server = require "net.server_select";
|
||||
package.loaded["net.server"] = server;
|
||||
|
||||
|
|
179
spec/inputs/test_keys.lua
Normal file
179
spec/inputs/test_keys.lua
Normal file
|
@ -0,0 +1,179 @@
|
|||
local test_keys = {
|
||||
-- ECDSA keypair from jwt.io
|
||||
ecdsa_private_pem = [[
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
|
||||
OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
|
||||
1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
|
||||
-----END PRIVATE KEY-----
|
||||
]];
|
||||
|
||||
ecdsa_public_pem = [[
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
|
||||
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
|
||||
-----END PUBLIC KEY-----
|
||||
]];
|
||||
|
||||
-- Self-generated ECDSA keypair
|
||||
alt_ecdsa_private_pem = [[
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQnn4AHz2Zy+JMAgp
|
||||
AZfKAm9F3s6791PstPf5XjHtETKhRANCAAScv9jI3+BOXXlCOXwmQYosIbl9mf4V
|
||||
uOwfIoCYSLylAghyxO0n2of8Kji+D+4C1zxNKmZIQa4s8neaIIzXnMY1
|
||||
-----END PRIVATE KEY-----
|
||||
]];
|
||||
|
||||
alt_ecdsa_public_pem = [[
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnL/YyN/gTl15Qjl8JkGKLCG5fZn+
|
||||
FbjsHyKAmEi8pQIIcsTtJ9qH/Co4vg/uAtc8TSpmSEGuLPJ3miCM15zGNQ==
|
||||
-----END PUBLIC KEY-----
|
||||
]];
|
||||
|
||||
-- JWT reference keys for ES512
|
||||
|
||||
ecdsa_521_public_pem = [[
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ
|
||||
PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47
|
||||
6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM
|
||||
Al8G7CqwoJOsW7Kddns=
|
||||
-----END PUBLIC KEY-----
|
||||
]];
|
||||
|
||||
ecdsa_521_private_pem = [[
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiyAa7aRHFDCh2qga
|
||||
9sTUGINE5jHAFnmM8xWeT/uni5I4tNqhV5Xx0pDrmCV9mbroFtfEa0XVfKuMAxxf
|
||||
Z6LM/yKhgYkDgYYABAGBzgdnP798FsLuWYTDDQA7c0r3BVk8NnRUSexpQUsRilPN
|
||||
v3SchO0lRw9Ru86x1khnVDx+duq4BiDFcvlSAcyjLACJvjvoyTLJiA+TQFdmrear
|
||||
jMiZNE25pT2yWP1NUndJxPcvVtfBW48kPOmvkY4WlqP5bAwCXwbsKrCgk6xbsp12
|
||||
ew==
|
||||
-----END PRIVATE KEY-----
|
||||
]];
|
||||
|
||||
-- Self-generated keys for ES512
|
||||
|
||||
alt_ecdsa_521_public_pem = [[
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBIxV0ecG/+qFc/kVPKs8Z6tjJEuRe
|
||||
dzrEaqABY6THu7BhCjEoxPr6iRYdiFPzNruFORsCAKf/NFLSoCqyrw9S0YMA1xc+
|
||||
uW01145oxT7Sp8BOH1MyOh7xNh+LFLi6X4lV6j5GQrM1sKSa3O5m0+VJmLy5b7cy
|
||||
oxNCzXrnEByz+EO2nYI=
|
||||
-----END PUBLIC KEY-----
|
||||
]];
|
||||
|
||||
alt_ecdsa_521_private_pem = [[
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MIHcAgEBBEIAV2XJQ4/5Pa5m43/AJdL4XzrRV/l7eQ1JObqmI95YDs3zxM5Mfygz
|
||||
DivhvuPdZCZUR+TdZQEdYN4LpllCzrDwmTCgBwYFK4EEACOhgYkDgYYABAEjFXR5
|
||||
wb/6oVz+RU8qzxnq2MkS5F53OsRqoAFjpMe7sGEKMSjE+vqJFh2IU/M2u4U5GwIA
|
||||
p/80UtKgKrKvD1LRgwDXFz65bTXXjmjFPtKnwE4fUzI6HvE2H4sUuLpfiVXqPkZC
|
||||
szWwpJrc7mbT5UmYvLlvtzKjE0LNeucQHLP4Q7adgg==
|
||||
-----END EC PRIVATE KEY-----
|
||||
]];
|
||||
|
||||
-- Self-generated EdDSA (Ed25519) keypair
|
||||
eddsa_private_pem = [[
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIOmrajEfnqdzdJzkJ4irQMCGbYRqrl0RlwPHIw+a5b7M
|
||||
-----END PRIVATE KEY-----
|
||||
]];
|
||||
|
||||
eddsa_public_pem = [[
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEAFipbSXeGvPVK7eA4+hIOdutZTUUyXswVSbMGi0j1QKE=
|
||||
-----END PUBLIC KEY-----
|
||||
]];
|
||||
|
||||
-- RSA keypair from jwt.io
|
||||
rsa_private_pem = [[
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
|
||||
MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
|
||||
NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
|
||||
qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
|
||||
p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
|
||||
ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
|
||||
VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
|
||||
laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
|
||||
sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
|
||||
mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
|
||||
dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
|
||||
ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
|
||||
DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
|
||||
N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
|
||||
0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
|
||||
t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
|
||||
AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
|
||||
48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
|
||||
DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
|
||||
xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
|
||||
mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
|
||||
2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
|
||||
et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
|
||||
VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
|
||||
TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
|
||||
dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
||||
-----END PRIVATE KEY-----
|
||||
]];
|
||||
|
||||
rsa_public_pem = [[
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
|
||||
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
|
||||
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
|
||||
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
|
||||
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
|
||||
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
|
||||
mwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
]];
|
||||
|
||||
|
||||
-- Self-generated RSA keypair
|
||||
alt_rsa_private_pem = [[
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA4bt6kor2TomqRXfjCFe6T42ibatloyHntZCUdlDDAkUh4oJ/
|
||||
4jDCXAUMYqmEsZKCPXxUGQgrmSmNnJPEDMTq3XLDsjhyN4stxEi0UVAiqqBkcEnk
|
||||
qbQIJSc9v5gpQF8IuJFWRvSNic0uClFL5W9R2s5AHcOhdFYKeDuitqHT5r+dC7cy
|
||||
WZs5YleKaESxmK6i6wMVhL9adAilTuETyMH0yLSh+aXsPYhjns4AbjGmiKOjqd5w
|
||||
sPwllEg6rGcIUi/o79z9HN8yLMXq3XNFCCA8RI4Zh3cADI1I5fe6wk1ETN+30cDw
|
||||
dGQ+uQbaQrzqmKVRNjZcorMwBjsOX5AMQBFx7wIDAQABAoIBAGxj5pZpTZ4msnEL
|
||||
ASQnY9oBS4ZXr8UmaamgU/mADDOR2JR4T0ngWeNvtSPG/GV70TgO9B7U8oJoFoyh
|
||||
05jCEXjmO5vfSNDs7rv6oUMONKczvybABKGMRgD5F8hhGyXCvGBLwV7u3OvXbw0b
|
||||
PlNcIbTsJpNkNam0CvDyyc3iZOq+HjIqituREV7lDw0rFeAR2YfEWn4VjZsQRZUZ
|
||||
XkpQJ5silrXgGemIEGqVA4YyM7i2HmTiLozfVYaVckMc02VFgOaoK9Z/wGlBxtS5
|
||||
evc/IGErSA4dc7uXBEeVjhtZoBkof2JV9BNt4hl4KN9wX3tkEX5Aq1K2lirSmg2r
|
||||
k+UEtwkCgYEA/5uYg25OR+jCFY/7uNS8e32Re1lgDeO+TeT1m+hcF1gCb2GBLifL
|
||||
yprnuytaz1/mPqawfwbilaxntLBoa5cmNKB3zDsgv4sM451yGZ0oxU0dXpDVHblu
|
||||
3nhxcaOXtb8jiSsr2MqgMbFlu7m8OupIliS+s8Pq72s6HUQQRKbJ+9MCgYEA4hQl
|
||||
1W/7nDI2SR4Q3UapQnaUjmDVxX5OD+E4RpKuRF6xF7Ao2CLZusMVo8WN8YiSQP2c
|
||||
RnzQNKgAVy/1zlhaaQDTs2TmSy9iStbuNZ8P+Gh6kmQXuHxwPyURSmwdpgZdL3+D
|
||||
8tt6pQNQ0vsLjA9VwHmzIT+rsxPmTxKNvBdNK/UCgYByP6zqyioJMDtYAfRkiAn7
|
||||
NIQLW0Z4ztvn2zgAyNoowPjNqgpgg/8t/xEm8tjzKg0y4bSwAnbSqa3s8JCrznKQ
|
||||
QU1qpt8bXl6TenNeiYWIstA2zYvEbnbkz3b9cT7FSLrse7RsgR0bOQyc3QcKWl+5
|
||||
ZJEsrpxbCVV/cUXIObi8awKBgQDOI8rfk+0bXhlrkBOWf/CjnpYUQK2LF4C8MALt
|
||||
Lp/hzWmyjLihYx2eknUv0Fl966ZXxidxiisaaDlvRlbeIGfHqK5fu9fUpE7+qH2p
|
||||
vPCF81YYF1YdrLF4kiby8iQSl2juf1nj3kY1IhHXXnsH6Y+qIg24emLntXRhkyxT
|
||||
XffK5QKBgGbzEvVgDkerw1SiefAaZnLumJJXBlKjJ00Sq8YLeViyFC/sr4EfG/cV
|
||||
7VYRhBw3e7RcYSBAA7uv8i3iIeCFjFooIZUARqXk4+yW753tY5nSJTWfkR7Bp5Pa
|
||||
9jKloxckbZKMjH23a+ABOxomY3l93KOBvjLvMYqccuREOwaT12cn
|
||||
-----END RSA PRIVATE KEY-----
|
||||
]];
|
||||
|
||||
alt_rsa_public_pem = [[
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4bt6kor2TomqRXfjCFe6
|
||||
T42ibatloyHntZCUdlDDAkUh4oJ/4jDCXAUMYqmEsZKCPXxUGQgrmSmNnJPEDMTq
|
||||
3XLDsjhyN4stxEi0UVAiqqBkcEnkqbQIJSc9v5gpQF8IuJFWRvSNic0uClFL5W9R
|
||||
2s5AHcOhdFYKeDuitqHT5r+dC7cyWZs5YleKaESxmK6i6wMVhL9adAilTuETyMH0
|
||||
yLSh+aXsPYhjns4AbjGmiKOjqd5wsPwllEg6rGcIUi/o79z9HN8yLMXq3XNFCCA8
|
||||
RI4Zh3cADI1I5fe6wk1ETN+30cDwdGQ+uQbaQrzqmKVRNjZcorMwBjsOX5AMQBFx
|
||||
7wIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
]];
|
||||
};
|
||||
|
||||
return test_keys;
|
241
spec/net_resolvers_service_spec.lua
Normal file
241
spec/net_resolvers_service_spec.lua
Normal file
|
@ -0,0 +1,241 @@
|
|||
local set = require "util.set";
|
||||
|
||||
insulate("net.resolvers.service", function ()
|
||||
local adns = {
|
||||
resolver = function ()
|
||||
return {
|
||||
lookup = function (_, cb, qname, qtype, qclass)
|
||||
if qname == "_xmpp-server._tcp.example.com"
|
||||
and (qtype or "SRV") == "SRV"
|
||||
and (qclass or "IN") == "IN" then
|
||||
cb({
|
||||
{ -- 60+35+60
|
||||
srv = { target = "xmpp0-a.example.com", port = 5228, priority = 0, weight = 60 };
|
||||
};
|
||||
{
|
||||
srv = { target = "xmpp0-b.example.com", port = 5216, priority = 0, weight = 35 };
|
||||
};
|
||||
{
|
||||
srv = { target = "xmpp0-c.example.com", port = 5200, priority = 0, weight = 0 };
|
||||
};
|
||||
{
|
||||
srv = { target = "xmpp0-d.example.com", port = 5256, priority = 0, weight = 120 };
|
||||
};
|
||||
|
||||
{
|
||||
srv = { target = "xmpp1-a.example.com", port = 5273, priority = 1, weight = 30 };
|
||||
};
|
||||
{
|
||||
srv = { target = "xmpp1-b.example.com", port = 5274, priority = 1, weight = 30 };
|
||||
};
|
||||
|
||||
{
|
||||
srv = { target = "xmpp2.example.com", port = 5275, priority = 2, weight = 0 };
|
||||
};
|
||||
});
|
||||
elseif qname == "_xmpp-server._tcp.single.example.com"
|
||||
and (qtype or "SRV") == "SRV"
|
||||
and (qclass or "IN") == "IN" then
|
||||
cb({
|
||||
{
|
||||
srv = { target = "xmpp0-a.example.com", port = 5269, priority = 0, weight = 0 };
|
||||
};
|
||||
});
|
||||
elseif qname == "_xmpp-server._tcp.half.example.com"
|
||||
and (qtype or "SRV") == "SRV"
|
||||
and (qclass or "IN") == "IN" then
|
||||
cb({
|
||||
{
|
||||
srv = { target = "xmpp0-a.example.com", port = 5269, priority = 0, weight = 0 };
|
||||
};
|
||||
{
|
||||
srv = { target = "xmpp0-b.example.com", port = 5270, priority = 0, weight = 1 };
|
||||
};
|
||||
});
|
||||
elseif qtype == "A" then
|
||||
local l = qname:match("%-(%a)%.example.com$") or "1";
|
||||
local d = ("%d"):format(l:byte())
|
||||
cb({
|
||||
{
|
||||
a = "127.0.0."..d;
|
||||
};
|
||||
});
|
||||
elseif qtype == "AAAA" then
|
||||
local l = qname:match("%-(%a)%.example.com$") or "1";
|
||||
local d = ("%04d"):format(l:byte())
|
||||
cb({
|
||||
{
|
||||
aaaa = "fdeb:9619:649e:c7d9::"..d;
|
||||
};
|
||||
});
|
||||
else
|
||||
cb(nil);
|
||||
end
|
||||
end;
|
||||
};
|
||||
end;
|
||||
};
|
||||
package.loaded["net.adns"] = mock(adns);
|
||||
local resolver = require "net.resolvers.service";
|
||||
math.randomseed(os.time());
|
||||
it("works for 99% of deployments", function ()
|
||||
-- Most deployments only have a single SRV record, let's make
|
||||
-- sure that works okay
|
||||
|
||||
local expected_targets = set.new({
|
||||
-- xmpp0-a
|
||||
"tcp4 127.0.0.97 5269";
|
||||
"tcp6 fdeb:9619:649e:c7d9::0097 5269";
|
||||
});
|
||||
local received_targets = set.new({});
|
||||
|
||||
local r = resolver.new("single.example.com", "xmpp-server");
|
||||
local done = false;
|
||||
local function handle_target(...)
|
||||
if ... == nil then
|
||||
done = true;
|
||||
-- No more targets
|
||||
return;
|
||||
end
|
||||
received_targets:add(table.concat({ ... }, " ", 1, 3));
|
||||
end
|
||||
r:next(handle_target);
|
||||
while not done do
|
||||
r:next(handle_target);
|
||||
end
|
||||
|
||||
-- We should have received all expected targets, and no unexpected
|
||||
-- ones:
|
||||
assert.truthy(set.xor(received_targets, expected_targets):empty());
|
||||
end);
|
||||
|
||||
it("supports A/AAAA fallback", function ()
|
||||
-- Many deployments don't have any SRV records, so we should
|
||||
-- fall back to A/AAAA records instead when that is the case
|
||||
|
||||
local expected_targets = set.new({
|
||||
-- xmpp0-a
|
||||
"tcp4 127.0.0.97 5269";
|
||||
"tcp6 fdeb:9619:649e:c7d9::0097 5269";
|
||||
});
|
||||
local received_targets = set.new({});
|
||||
|
||||
local r = resolver.new("xmpp0-a.example.com", "xmpp-server", "tcp", { default_port = 5269 });
|
||||
local done = false;
|
||||
local function handle_target(...)
|
||||
if ... == nil then
|
||||
done = true;
|
||||
-- No more targets
|
||||
return;
|
||||
end
|
||||
received_targets:add(table.concat({ ... }, " ", 1, 3));
|
||||
end
|
||||
r:next(handle_target);
|
||||
while not done do
|
||||
r:next(handle_target);
|
||||
end
|
||||
|
||||
-- We should have received all expected targets, and no unexpected
|
||||
-- ones:
|
||||
assert.truthy(set.xor(received_targets, expected_targets):empty());
|
||||
end);
|
||||
|
||||
|
||||
it("works", function ()
|
||||
local expected_targets = set.new({
|
||||
-- xmpp0-a
|
||||
"tcp4 127.0.0.97 5228";
|
||||
"tcp6 fdeb:9619:649e:c7d9::0097 5228";
|
||||
"tcp4 127.0.0.97 5273";
|
||||
"tcp6 fdeb:9619:649e:c7d9::0097 5273";
|
||||
|
||||
-- xmpp0-b
|
||||
"tcp4 127.0.0.98 5274";
|
||||
"tcp6 fdeb:9619:649e:c7d9::0098 5274";
|
||||
"tcp4 127.0.0.98 5216";
|
||||
"tcp6 fdeb:9619:649e:c7d9::0098 5216";
|
||||
|
||||
-- xmpp0-c
|
||||
"tcp4 127.0.0.99 5200";
|
||||
"tcp6 fdeb:9619:649e:c7d9::0099 5200";
|
||||
|
||||
-- xmpp0-d
|
||||
"tcp4 127.0.0.100 5256";
|
||||
"tcp6 fdeb:9619:649e:c7d9::0100 5256";
|
||||
|
||||
-- xmpp2
|
||||
"tcp4 127.0.0.49 5275";
|
||||
"tcp6 fdeb:9619:649e:c7d9::0049 5275";
|
||||
|
||||
});
|
||||
local received_targets = set.new({});
|
||||
|
||||
local r = resolver.new("example.com", "xmpp-server");
|
||||
local done = false;
|
||||
local function handle_target(...)
|
||||
if ... == nil then
|
||||
done = true;
|
||||
-- No more targets
|
||||
return;
|
||||
end
|
||||
received_targets:add(table.concat({ ... }, " ", 1, 3));
|
||||
end
|
||||
r:next(handle_target);
|
||||
while not done do
|
||||
r:next(handle_target);
|
||||
end
|
||||
|
||||
-- We should have received all expected targets, and no unexpected
|
||||
-- ones:
|
||||
assert.truthy(set.xor(received_targets, expected_targets):empty());
|
||||
end);
|
||||
|
||||
it("balances across weights correctly #slow", function ()
|
||||
-- This mimics many repeated connections to 'example.com' (mock
|
||||
-- records defined above), and records the port number of the
|
||||
-- first target. Therefore it (should) only return priority
|
||||
-- 0 records, and the input data is constructed such that the
|
||||
-- last two digits of the port number represent the percentage
|
||||
-- of times that record should (on average) be picked first.
|
||||
|
||||
-- To prevent random test failures, we test across a handful
|
||||
-- of fixed (randomly selected) seeds.
|
||||
for _, seed in ipairs({ 8401877, 3943829, 7830992 }) do
|
||||
math.randomseed(seed);
|
||||
|
||||
local results = {};
|
||||
local function run()
|
||||
local run_results = {};
|
||||
local r = resolver.new("example.com", "xmpp-server");
|
||||
local function record_target(...)
|
||||
if ... == nil then
|
||||
-- No more targets
|
||||
return;
|
||||
end
|
||||
run_results = { ... };
|
||||
end
|
||||
r:next(record_target);
|
||||
return run_results[3];
|
||||
end
|
||||
|
||||
for _ = 1, 1000 do
|
||||
local port = run();
|
||||
results[port] = (results[port] or 0) + 1;
|
||||
end
|
||||
|
||||
local ports = {};
|
||||
for port in pairs(results) do
|
||||
table.insert(ports, port);
|
||||
end
|
||||
table.sort(ports);
|
||||
for _, port in ipairs(ports) do
|
||||
--print("PORT", port, tostring((results[port]/1000) * 100).."% hits (expected "..tostring(port-5200).."%)");
|
||||
local hit_pct = (results[port]/1000) * 100;
|
||||
local expected_pct = port - 5200;
|
||||
--print(hit_pct, expected_pct, math.abs(hit_pct - expected_pct));
|
||||
assert.is_true(math.abs(hit_pct - expected_pct) < 5);
|
||||
end
|
||||
--print("---");
|
||||
end
|
||||
end);
|
||||
end);
|
|
@ -45,8 +45,8 @@ Romeo sends:
|
|||
Romeo receives:
|
||||
<iq type="result" id="mamextmeta">
|
||||
<metadata xmlns="urn:xmpp:mam:2">
|
||||
<start timestamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:mam:2" id="{scansion:any}"/>
|
||||
<end timestamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:mam:2" id="{scansion:any}"/>
|
||||
<start timestamp="2008-08-22T21:09:04.500000Z" xmlns="urn:xmpp:mam:2" id="{scansion:any}"/>
|
||||
<end timestamp="2008-08-22T21:09:04.500000Z" xmlns="urn:xmpp:mam:2" id="{scansion:any}"/>
|
||||
</metadata>
|
||||
</iq>
|
||||
|
||||
|
@ -59,7 +59,7 @@ Romeo receives:
|
|||
<message to="${Romeo's full JID}">
|
||||
<result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:any}">
|
||||
<forwarded xmlns="urn:xmpp:forward:0">
|
||||
<delay stamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:delay"/>
|
||||
<delay stamp="2008-08-22T21:09:04.500000Z" xmlns="urn:xmpp:delay"/>
|
||||
<message to="someone@localhost" xmlns="jabber:client" type="chat" xml:lang="en" id="chat01" from="${Romeo's full JID}">
|
||||
<body>Hello</body>
|
||||
</message>
|
||||
|
@ -71,7 +71,7 @@ Romeo receives:
|
|||
<message to="${Romeo's full JID}">
|
||||
<result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:any}">
|
||||
<forwarded xmlns="urn:xmpp:forward:0">
|
||||
<delay stamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:delay"/>
|
||||
<delay stamp="2008-08-22T21:09:04.500000Z" xmlns="urn:xmpp:delay"/>
|
||||
<message to="someone@localhost" xmlns="jabber:client" type="chat" xml:lang="en" id="chat02" from="${Romeo's full JID}">
|
||||
<body>U there?</body>
|
||||
</message>
|
||||
|
@ -98,7 +98,7 @@ Romeo receives:
|
|||
<message to="${Romeo's full JID}">
|
||||
<result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:any}">
|
||||
<forwarded xmlns="urn:xmpp:forward:0">
|
||||
<delay stamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:delay"/>
|
||||
<delay stamp="2008-08-22T21:09:04.500000Z" xmlns="urn:xmpp:delay"/>
|
||||
<message to="someone@localhost" xmlns="jabber:client" type="chat" xml:lang="en" id="chat02" from="${Romeo's full JID}">
|
||||
<body>U there?</body>
|
||||
</message>
|
||||
|
@ -110,7 +110,7 @@ Romeo receives:
|
|||
<message to="${Romeo's full JID}">
|
||||
<result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:any}">
|
||||
<forwarded xmlns="urn:xmpp:forward:0">
|
||||
<delay stamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:delay"/>
|
||||
<delay stamp="2008-08-22T21:09:04.500000Z" xmlns="urn:xmpp:delay"/>
|
||||
<message to="someone@localhost" xmlns="jabber:client" type="chat" xml:lang="en" id="chat01" from="${Romeo's full JID}">
|
||||
<body>Hello</body>
|
||||
</message>
|
||||
|
|
|
@ -6,8 +6,8 @@ function _G.os.time()
|
|||
end
|
||||
package.preload["util.time"] = function ()
|
||||
return {
|
||||
now = function () return 1219439344.1; end;
|
||||
monotonic = function () return 0.1; end;
|
||||
now = function () return 1219439344.5; end;
|
||||
monotonic = function () return 0.5; end;
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -4,6 +4,20 @@ local cache = require "util.cache";
|
|||
describe("util.cache", function()
|
||||
describe("#new()", function()
|
||||
it("should work", function()
|
||||
do
|
||||
local c = cache.new(1);
|
||||
assert.is_not_nil(c);
|
||||
|
||||
assert.has_error(function ()
|
||||
cache.new(0);
|
||||
end);
|
||||
assert.has_error(function ()
|
||||
cache.new(-1);
|
||||
end);
|
||||
assert.has_error(function ()
|
||||
cache.new("foo");
|
||||
end);
|
||||
end
|
||||
|
||||
local c = cache.new(5);
|
||||
|
||||
|
@ -314,7 +328,7 @@ describe("util.cache", function()
|
|||
|
||||
end);
|
||||
|
||||
(_VERSION=="Lua 5.1" and pending or it)(":table works", function ()
|
||||
it(":table works", function ()
|
||||
local t = cache.new(3):table();
|
||||
assert.is.table(t);
|
||||
t["a"] = "1";
|
||||
|
@ -336,5 +350,43 @@ describe("util.cache", function()
|
|||
assert.spy(i).was_called_with("c", "3");
|
||||
assert.spy(i).was_called_with("d", "4");
|
||||
end);
|
||||
|
||||
local function vs(t)
|
||||
local vs_ = {};
|
||||
for v in t:values() do
|
||||
vs_[#vs_+1] = v;
|
||||
end
|
||||
return vs_;
|
||||
end
|
||||
|
||||
it(":values works", function ()
|
||||
local t = cache.new(3);
|
||||
t:set("k1", "v1");
|
||||
t:set("k2", "v2");
|
||||
assert.same({"v2", "v1"}, vs(t));
|
||||
t:set("k3", "v3");
|
||||
assert.same({"v3", "v2", "v1"}, vs(t));
|
||||
t:set("k4", "v4");
|
||||
assert.same({"v4", "v3", "v2"}, vs(t));
|
||||
end);
|
||||
|
||||
it(":resize works", function ()
|
||||
local c = cache.new(5);
|
||||
for i = 1, 5 do
|
||||
c:set(("k%d"):format(i), ("v%d"):format(i));
|
||||
end
|
||||
assert.same({"v5", "v4", "v3", "v2", "v1"}, vs(c));
|
||||
assert.has_error(function ()
|
||||
c:resize(-1);
|
||||
end);
|
||||
assert.has_error(function ()
|
||||
c:resize(0);
|
||||
end);
|
||||
assert.has_error(function ()
|
||||
c:resize("foo");
|
||||
end);
|
||||
c:resize(3);
|
||||
assert.same({"v5", "v4", "v3"}, vs(c));
|
||||
end);
|
||||
end);
|
||||
end);
|
||||
|
|
167
spec/util_crypto_spec.lua
Normal file
167
spec/util_crypto_spec.lua
Normal file
|
@ -0,0 +1,167 @@
|
|||
local test_keys = require "spec.inputs.test_keys";
|
||||
|
||||
describe("util.crypto", function ()
|
||||
local crypto = require "util.crypto";
|
||||
local random = require "util.random";
|
||||
|
||||
describe("generate_ed25519_keypair", function ()
|
||||
local keypair = crypto.generate_ed25519_keypair();
|
||||
assert.is_not_nil(keypair);
|
||||
assert.equal("ED25519", keypair:get_type());
|
||||
end)
|
||||
|
||||
describe("import_private_pem", function ()
|
||||
it("can import ECDSA keys", function ()
|
||||
local ecdsa_key = crypto.import_private_pem(test_keys.ecdsa_private_pem);
|
||||
assert.equal("id-ecPublicKey", ecdsa_key:get_type());
|
||||
end);
|
||||
|
||||
it("can import EdDSA (Ed25519) keys", function ()
|
||||
local ed25519_key = crypto.import_private_pem(crypto.generate_ed25519_keypair():private_pem());
|
||||
assert.equal("ED25519", ed25519_key:get_type());
|
||||
end);
|
||||
|
||||
it("can import RSA keys", function ()
|
||||
-- TODO
|
||||
end);
|
||||
|
||||
it("rejects invalid keys", function ()
|
||||
assert.is_nil(crypto.import_private_pem(test_keys.eddsa_public_pem));
|
||||
assert.is_nil(crypto.import_private_pem(test_keys.ecdsa_public_pem));
|
||||
assert.is_nil(crypto.import_private_pem("foo"));
|
||||
assert.is_nil(crypto.import_private_pem(""));
|
||||
end);
|
||||
end);
|
||||
|
||||
describe("import_public_pem", function ()
|
||||
it("can import ECDSA public keys", function ()
|
||||
local ecdsa_key = crypto.import_public_pem(test_keys.ecdsa_public_pem);
|
||||
assert.equal("id-ecPublicKey", ecdsa_key:get_type());
|
||||
end);
|
||||
|
||||
it("can import EdDSA (Ed25519) public keys", function ()
|
||||
local ed25519_key = crypto.import_public_pem(test_keys.eddsa_public_pem);
|
||||
assert.equal("ED25519", ed25519_key:get_type());
|
||||
end);
|
||||
|
||||
it("can import RSA public keys", function ()
|
||||
-- TODO
|
||||
end);
|
||||
end);
|
||||
|
||||
describe("PEM export", function ()
|
||||
it("works", function ()
|
||||
local ecdsa_key = crypto.import_public_pem(test_keys.ecdsa_public_pem);
|
||||
assert.equal("id-ecPublicKey", ecdsa_key:get_type());
|
||||
assert.equal(test_keys.ecdsa_public_pem, ecdsa_key:public_pem());
|
||||
|
||||
assert.has_error(function ()
|
||||
-- Fails because private key is not available
|
||||
ecdsa_key:private_pem();
|
||||
end);
|
||||
|
||||
local ecdsa_private_key = crypto.import_private_pem(test_keys.ecdsa_private_pem);
|
||||
assert.equal(test_keys.ecdsa_private_pem, ecdsa_private_key:private_pem());
|
||||
end);
|
||||
end);
|
||||
|
||||
describe("sign/verify with", function ()
|
||||
local test_cases = {
|
||||
ed25519 = {
|
||||
crypto.ed25519_sign, crypto.ed25519_verify;
|
||||
key = crypto.import_private_pem(test_keys.eddsa_private_pem);
|
||||
sig_length = 64;
|
||||
};
|
||||
ecdsa = {
|
||||
crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify;
|
||||
key = crypto.import_private_pem(test_keys.ecdsa_private_pem);
|
||||
};
|
||||
};
|
||||
for test_name, test in pairs(test_cases) do
|
||||
local key = test.key;
|
||||
describe(test_name, function ()
|
||||
it("works", function ()
|
||||
local sign, verify = test[1], test[2];
|
||||
local sig = assert(sign(key, "Hello world"));
|
||||
assert.is_string(sig);
|
||||
if test.sig_length then
|
||||
assert.equal(test.sig_length, #sig);
|
||||
end
|
||||
|
||||
do
|
||||
local ok = verify(key, "Hello world", sig);
|
||||
assert.is_truthy(ok);
|
||||
end
|
||||
do -- Incorrect signature
|
||||
local ok = verify(key, "Hello world", sig:sub(1, -2)..string.char((sig:byte(-1)+1)%255));
|
||||
assert.is_falsy(ok);
|
||||
end
|
||||
do -- Incorrect message
|
||||
local ok = verify(key, "Hello earth", sig);
|
||||
assert.is_falsy(ok);
|
||||
end
|
||||
do -- Incorrect message (embedded NUL)
|
||||
local ok = verify(key, "Hello world\0foo", sig);
|
||||
assert.is_falsy(ok);
|
||||
end
|
||||
end);
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
describe("ECDSA signatures", function ()
|
||||
local hex = require "util.hex";
|
||||
local sig = hex.decode((([[
|
||||
304402203e936e7b0bc62887e0e9d675afd08531a930384cfcf301
|
||||
f25d13053a2ebf141d02205a5a7c7b7ac5878d004cb79b17b39346
|
||||
6b0cd1043718ffc31c153b971d213a8e
|
||||
]]):gsub("%s+", "")));
|
||||
it("can be parsed", function ()
|
||||
local r, s = crypto.parse_ecdsa_signature(sig, 32);
|
||||
assert.is_string(r);
|
||||
assert.is_string(s);
|
||||
assert.equal(32, #r);
|
||||
assert.equal(32, #s);
|
||||
end);
|
||||
it("fails to parse invalid signatures", function ()
|
||||
local invalid_sigs = {
|
||||
"";
|
||||
"\000";
|
||||
string.rep("\000", 64);
|
||||
string.rep("\000", 72);
|
||||
string.rep("\000", 256);
|
||||
string.rep("\255", 72);
|
||||
string.rep("\255", 3);
|
||||
};
|
||||
for _, invalid_sig in ipairs(invalid_sigs) do
|
||||
local r, s = crypto.parse_ecdsa_signature(invalid_sig, 32);
|
||||
assert.is_nil(r);
|
||||
assert.is_nil(s);
|
||||
end
|
||||
end);
|
||||
it("can be built", function ()
|
||||
local r, s = crypto.parse_ecdsa_signature(sig, 32);
|
||||
local rebuilt_sig = crypto.build_ecdsa_signature(r, s);
|
||||
assert.equal(sig, rebuilt_sig);
|
||||
end);
|
||||
end);
|
||||
|
||||
describe("AES-GCM encryption", function ()
|
||||
it("works", function ()
|
||||
local message = "foo\0bar";
|
||||
local key_128_bit = random.bytes(16);
|
||||
local key_256_bit = random.bytes(32);
|
||||
local test_cases = {
|
||||
{ crypto.aes_128_gcm_encrypt, crypto.aes_128_gcm_decrypt, key = key_128_bit };
|
||||
{ crypto.aes_256_gcm_encrypt, crypto.aes_256_gcm_decrypt, key = key_256_bit };
|
||||
};
|
||||
for _, params in pairs(test_cases) do
|
||||
local iv = params.iv or random.bytes(12);
|
||||
local encrypted = params[1](params.key, iv, message);
|
||||
assert.not_equal(message, encrypted);
|
||||
local decrypted = params[2](params.key, iv, encrypted);
|
||||
assert.equal(message, decrypted);
|
||||
end
|
||||
end);
|
||||
end);
|
||||
end);
|
|
@ -130,7 +130,7 @@ describe("util.dataforms", function ()
|
|||
assert.truthy(st.is_stanza(xform));
|
||||
assert.equal("x", xform.name);
|
||||
assert.equal("jabber:x:data", xform.attr.xmlns);
|
||||
assert.equal("FORM_TYPE", xform:find("field@var"));
|
||||
assert.equal("FORM_TYPE", xform:get_child_attr("field", nil, "var"));
|
||||
assert.equal("xmpp:prosody.im/spec/util.dataforms#1", xform:find("field/value#"));
|
||||
local allowed_direct_children = {
|
||||
title = true,
|
||||
|
|
|
@ -16,7 +16,10 @@ describe("util.datetime", function ()
|
|||
assert.truthy(string.find(date(), "^%d%d%d%d%-%d%d%-%d%d$"));
|
||||
end);
|
||||
it("should work", function ()
|
||||
assert.equals(date(1136239445), "2006-01-02");
|
||||
assert.equals("2006-01-02", date(1136239445));
|
||||
end);
|
||||
it("should ignore fractional parts", function ()
|
||||
assert.equals("2006-01-02", date(1136239445.5));
|
||||
end);
|
||||
end);
|
||||
describe("#time", function ()
|
||||
|
@ -32,8 +35,14 @@ describe("util.datetime", function ()
|
|||
assert.truthy(string.find(time(), "^%d%d:%d%d:%d%d"));
|
||||
end);
|
||||
it("should work", function ()
|
||||
assert.equals(time(1136239445), "22:04:05");
|
||||
assert.equals("22:04:05", time(1136239445));
|
||||
end);
|
||||
it("should handle precision", function ()
|
||||
assert.equal("14:46:31.158200", time(1660488391.1582))
|
||||
assert.equal("14:46:32.158200", time(1660488392.1582))
|
||||
assert.equal("14:46:33.158200", time(1660488393.1582))
|
||||
assert.equal("14:46:33.999900", time(1660488393.9999))
|
||||
end)
|
||||
end);
|
||||
describe("#datetime", function ()
|
||||
local datetime = util_datetime.datetime;
|
||||
|
@ -48,14 +57,23 @@ describe("util.datetime", function ()
|
|||
assert.truthy(string.find(datetime(), "^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%d"));
|
||||
end);
|
||||
it("should work", function ()
|
||||
assert.equals(datetime(1136239445), "2006-01-02T22:04:05Z");
|
||||
assert.equals("2006-01-02T22:04:05Z", datetime(1136239445));
|
||||
end);
|
||||
it("should handle precision", function ()
|
||||
assert.equal("2022-08-14T14:46:31.158200Z", datetime(1660488391.1582))
|
||||
assert.equal("2022-08-14T14:46:32.158200Z", datetime(1660488392.1582))
|
||||
assert.equal("2022-08-14T14:46:33.158200Z", datetime(1660488393.1582))
|
||||
assert.equal("2022-08-14T14:46:33.999900Z", datetime(1660488393.9999))
|
||||
end)
|
||||
end);
|
||||
describe("#legacy", function ()
|
||||
local legacy = util_datetime.legacy;
|
||||
it("should exist", function ()
|
||||
assert.is_function(legacy);
|
||||
end);
|
||||
it("should not add precision", function ()
|
||||
assert.equal("20220814T14:46:31", legacy(1660488391.1582));
|
||||
end);
|
||||
end);
|
||||
describe("#parse", function ()
|
||||
local parse = util_datetime.parse;
|
||||
|
@ -64,13 +82,23 @@ describe("util.datetime", function ()
|
|||
end);
|
||||
it("should work", function ()
|
||||
-- Timestamp used by Go
|
||||
assert.equals(parse("2017-11-19T17:58:13Z"), 1511114293);
|
||||
assert.equals(parse("2017-11-19T18:58:50+0100"), 1511114330);
|
||||
assert.equals(parse("2006-01-02T15:04:05-0700"), 1136239445);
|
||||
assert.equals(1511114293, parse("2017-11-19T17:58:13Z"));
|
||||
assert.equals(1511114330, parse("2017-11-19T18:58:50+0100"));
|
||||
assert.equals(1136239445, parse("2006-01-02T15:04:05-0700"));
|
||||
assert.equals(1136239445, parse("2006-01-02T15:04:05-07"));
|
||||
end);
|
||||
it("should handle timezones", function ()
|
||||
-- https://xmpp.org/extensions/xep-0082.html#example-2 and 3
|
||||
assert.equals(parse("1969-07-21T02:56:15Z"), parse("1969-07-20T21:56:15-05:00"));
|
||||
end);
|
||||
it("should handle precision", function ()
|
||||
-- floating point comparison is not an exact science
|
||||
assert.truthy(math.abs(1660488392.1582 - parse("2022-08-14T14:46:32.158200Z")) < 0.001)
|
||||
end)
|
||||
it("should return nil when given invalid inputs", function ()
|
||||
assert.is_nil(parse(nil));
|
||||
assert.is_nil(parse("hello world"));
|
||||
assert.is_nil(parse("2017-11-19T18:58:50$0100"));
|
||||
end);
|
||||
end);
|
||||
end);
|
||||
|
|
|
@ -6,6 +6,8 @@ describe("util.dbuffer", function ()
|
|||
end);
|
||||
it("can be created", function ()
|
||||
assert.truthy(dbuffer.new());
|
||||
assert.truthy(dbuffer.new(1));
|
||||
assert.truthy(dbuffer.new(1024));
|
||||
end);
|
||||
it("won't create an empty buffer", function ()
|
||||
assert.falsy(dbuffer.new(0));
|
||||
|
@ -15,10 +17,21 @@ describe("util.dbuffer", function ()
|
|||
end);
|
||||
end);
|
||||
describe(":write", function ()
|
||||
local b = dbuffer.new();
|
||||
local b = dbuffer.new(10, 3);
|
||||
it("works", function ()
|
||||
assert.truthy(b:write("hi"));
|
||||
end);
|
||||
it("fails when the buffer is full", function ()
|
||||
local ret = b:write(" there world, this is a long piece of data");
|
||||
assert.is_falsy(ret);
|
||||
end);
|
||||
it("works when max_chunks is reached", function ()
|
||||
-- Chunks are an optimization, dbuffer should collapse chunks when needed
|
||||
for _ = 1, 8 do
|
||||
assert.truthy(b:write("!"));
|
||||
end
|
||||
assert.falsy(b:write("!")); -- Length reached
|
||||
end);
|
||||
end);
|
||||
|
||||
describe(":read", function ()
|
||||
|
@ -34,6 +47,14 @@ describe("util.dbuffer", function ()
|
|||
assert.equal(" ", b:read());
|
||||
assert.equal("world", b:read());
|
||||
end);
|
||||
it("fails when there is not enough data in the buffer", function ()
|
||||
local b = dbuffer.new(12);
|
||||
b:write("hello");
|
||||
b:write(" ");
|
||||
b:write("world");
|
||||
assert.is_falsy(b:read(12));
|
||||
assert.is_falsy(b:read(13));
|
||||
end);
|
||||
end);
|
||||
|
||||
describe(":read_until", function ()
|
||||
|
@ -68,9 +89,46 @@ describe("util.dbuffer", function ()
|
|||
assert.equal(5, b:len());
|
||||
assert.equal("world", b:read(5));
|
||||
end);
|
||||
it("works across chunks", function ()
|
||||
assert.truthy(b:write("hello"));
|
||||
assert.truthy(b:write(" "));
|
||||
assert.truthy(b:write("world"));
|
||||
assert.truthy(b:discard(3));
|
||||
assert.equal(8, b:length());
|
||||
assert.truthy(b:discard(3));
|
||||
assert.equal(5, b:length());
|
||||
assert.equal("world", b:read(5));
|
||||
end);
|
||||
it("can discard the entire buffer", function ()
|
||||
assert.equal(b:len(), 0);
|
||||
assert.truthy(b:write("hello world"));
|
||||
assert.truthy(b:discard(11));
|
||||
assert.equal(0, b:len());
|
||||
assert.truthy(b:write("hello world"));
|
||||
assert.truthy(b:discard(12));
|
||||
assert.equal(0, b:len());
|
||||
assert.truthy(b:write("hello world"));
|
||||
assert.truthy(b:discard(128));
|
||||
assert.equal(0, b:len());
|
||||
end);
|
||||
it("works on an empty buffer", function ()
|
||||
assert.truthy(dbuffer.new():discard());
|
||||
assert.truthy(dbuffer.new():discard(0));
|
||||
assert.truthy(dbuffer.new():discard(1));
|
||||
end);
|
||||
end);
|
||||
|
||||
describe(":collapse()", function ()
|
||||
it("works", function ()
|
||||
local b = dbuffer.new();
|
||||
b:write("hello");
|
||||
b:write(" ");
|
||||
b:write("world");
|
||||
b:collapse(6);
|
||||
local ret, bytes = b:read_chunk();
|
||||
assert.equal("hello ", ret);
|
||||
assert.equal(6, bytes);
|
||||
end);
|
||||
it("works on an empty buffer", function ()
|
||||
local b = dbuffer.new();
|
||||
b:collapse();
|
||||
|
@ -115,6 +173,11 @@ describe("util.dbuffer", function ()
|
|||
end
|
||||
end
|
||||
end);
|
||||
|
||||
it("works on an empty buffer", function ()
|
||||
local b = dbuffer.new();
|
||||
assert.equal("", b:sub(1, 12));
|
||||
end);
|
||||
end);
|
||||
|
||||
describe(":byte", function ()
|
||||
|
@ -122,7 +185,11 @@ describe("util.dbuffer", function ()
|
|||
local s = "hello world"
|
||||
local function test_byte(b, x, y)
|
||||
local string_result, buffer_result = {s:byte(x, y)}, {b:byte(x, y)};
|
||||
assert.same(string_result, buffer_result, ("buffer:byte(%d, %s) does not match string:byte()"):format(x, y and ("%d"):format(y) or "nil"));
|
||||
assert.same(
|
||||
string_result,
|
||||
buffer_result,
|
||||
("buffer:byte(%s, %s) does not match string:byte()"):format(x and ("%d"):format(x) or "nil", y and ("%d"):format(y) or "nil")
|
||||
);
|
||||
end
|
||||
|
||||
it("is equivalent to string:byte", function ()
|
||||
|
@ -132,6 +199,7 @@ describe("util.dbuffer", function ()
|
|||
test_byte(b, 3);
|
||||
test_byte(b, -1);
|
||||
test_byte(b, -3);
|
||||
test_byte(b, nil, 5);
|
||||
for i = -13, 13 do
|
||||
for j = -13, 13 do
|
||||
test_byte(b, i, j);
|
||||
|
|
|
@ -333,29 +333,27 @@ describe("util.format", function()
|
|||
end);
|
||||
end);
|
||||
|
||||
if _VERSION > "Lua 5.1" then -- COMPAT no %a or %A in Lua 5.1
|
||||
describe("to %a", function ()
|
||||
it("works", function ()
|
||||
assert.equal("0x1.84p+6", format("%a", 97))
|
||||
assert.equal("-0x1.81c8p+13", format("%a", -12345))
|
||||
assert.equal("0x1.8p+0", format("%a", 1.5))
|
||||
assert.equal("0x1p+66", format("%a", 73786976294838206464))
|
||||
assert.equal("inf", format("%a", math.huge))
|
||||
assert.equal("0x1.fffffffcp+30", format("%a", 2147483647))
|
||||
end);
|
||||
describe("to %a", function ()
|
||||
it("works", function ()
|
||||
assert.equal("0x1.84p+6", format("%a", 97))
|
||||
assert.equal("-0x1.81c8p+13", format("%a", -12345))
|
||||
assert.equal("0x1.8p+0", format("%a", 1.5))
|
||||
assert.equal("0x1p+66", format("%a", 73786976294838206464))
|
||||
assert.equal("inf", format("%a", math.huge))
|
||||
assert.equal("0x1.fffffffcp+30", format("%a", 2147483647))
|
||||
end);
|
||||
end);
|
||||
|
||||
describe("to %A", function ()
|
||||
it("works", function ()
|
||||
assert.equal("0X1.84P+6", format("%A", 97))
|
||||
assert.equal("-0X1.81C8P+13", format("%A", -12345))
|
||||
assert.equal("0X1.8P+0", format("%A", 1.5))
|
||||
assert.equal("0X1P+66", format("%A", 73786976294838206464))
|
||||
assert.equal("INF", format("%A", math.huge))
|
||||
assert.equal("0X1.FFFFFFFCP+30", format("%A", 2147483647))
|
||||
end);
|
||||
describe("to %A", function ()
|
||||
it("works", function ()
|
||||
assert.equal("0X1.84P+6", format("%A", 97))
|
||||
assert.equal("-0X1.81C8P+13", format("%A", -12345))
|
||||
assert.equal("0X1.8P+0", format("%A", 1.5))
|
||||
assert.equal("0X1P+66", format("%A", 73786976294838206464))
|
||||
assert.equal("INF", format("%A", math.huge))
|
||||
assert.equal("0X1.FFFFFFFCP+30", format("%A", 2147483647))
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
describe("to %e", function ()
|
||||
it("works", function ()
|
||||
|
|
|
@ -53,3 +53,18 @@ describe("PBKDF2-HMAC-SHA256", function ()
|
|||
end);
|
||||
|
||||
|
||||
describe("SHA-3", function ()
|
||||
describe("256", function ()
|
||||
it("works", function ()
|
||||
local expected = "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"
|
||||
assert.equal(expected, hashes.sha3_256("", true));
|
||||
end);
|
||||
end);
|
||||
describe("512", function ()
|
||||
it("works", function ()
|
||||
local expected = "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
|
||||
assert.equal(expected, hashes.sha3_512("", true));
|
||||
end);
|
||||
end);
|
||||
end);
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
local hashring = require "util.hashring";
|
||||
|
||||
describe("util.hashring", function ()
|
||||
randomize(false);
|
||||
|
||||
local sha256 = require "util.hashes".sha256;
|
||||
|
||||
|
@ -82,4 +83,11 @@ describe("util.hashring", function ()
|
|||
end
|
||||
end);
|
||||
|
||||
it("should support values associated with nodes", function ()
|
||||
local r = hashring.new(128, sha256);
|
||||
r:add_node("node1", { a = 1 });
|
||||
local node, value = r:get_node("foo");
|
||||
assert.is_equal("node1", node);
|
||||
assert.same({ a = 1 }, value);
|
||||
end);
|
||||
end);
|
||||
|
|
|
@ -10,6 +10,14 @@ describe("util.iterators", function ()
|
|||
end
|
||||
assert.same(output, expect);
|
||||
end);
|
||||
it("should work with only a single iterator", function ()
|
||||
local expect = { "a", "b", "c" };
|
||||
local output = {};
|
||||
for x in iter.join(iter.values({"a", "b", "c"})) do
|
||||
table.insert(output, x);
|
||||
end
|
||||
assert.same(output, expect);
|
||||
end);
|
||||
end);
|
||||
|
||||
describe("sorted_pairs", function ()
|
||||
|
|
|
@ -48,6 +48,47 @@ describe("util.jid", function()
|
|||
end)
|
||||
end);
|
||||
|
||||
describe("#prepped_split()", function()
|
||||
local function test(input_jid, expected_node, expected_server, expected_resource)
|
||||
local rnode, rserver, rresource = jid.prepped_split(input_jid);
|
||||
assert.are.equal(expected_node, rnode, "split("..tostring(input_jid)..") failed");
|
||||
assert.are.equal(expected_server, rserver, "split("..tostring(input_jid)..") failed");
|
||||
assert.are.equal(expected_resource, rresource, "split("..tostring(input_jid)..") failed");
|
||||
end
|
||||
|
||||
it("should work", function()
|
||||
-- Valid JIDs
|
||||
test("node@server", "node", "server", nil );
|
||||
test("node@server/resource", "node", "server", "resource" );
|
||||
test("server", nil, "server", nil );
|
||||
test("server/resource", nil, "server", "resource" );
|
||||
test("server/resource@foo", nil, "server", "resource@foo" );
|
||||
test("server/resource@foo/bar", nil, "server", "resource@foo/bar");
|
||||
|
||||
-- Always invalid JIDs
|
||||
test(nil, nil, nil, nil);
|
||||
test("node@/server", nil, nil, nil);
|
||||
test("@server", nil, nil, nil);
|
||||
test("@server/resource", nil, nil, nil);
|
||||
test("@/resource", nil, nil, nil);
|
||||
test("@server/", nil, nil, nil);
|
||||
test("server/", nil, nil, nil);
|
||||
test("/resource", nil, nil, nil);
|
||||
end);
|
||||
it("should reject invalid arguments", function ()
|
||||
assert.has_error(function () jid.prepped_split(false) end)
|
||||
end)
|
||||
it("should strip empty root label", function ()
|
||||
test("node@server.", "node", "server", nil);
|
||||
end);
|
||||
it("should fail for JIDs that fail stringprep", function ()
|
||||
test("node@invalid-\128-server", nil, nil, nil);
|
||||
test("node@invalid-\194\128-server", nil, nil, nil);
|
||||
test("<invalid node>@server", nil, nil, nil);
|
||||
test("node@server/invalid-\000-resource", nil, nil, nil);
|
||||
end);
|
||||
end);
|
||||
|
||||
|
||||
describe("#bare()", function()
|
||||
it("should work", function()
|
||||
|
|
|
@ -21,9 +21,11 @@ describe("util.jsonpointer", function()
|
|||
}]])
|
||||
end)
|
||||
it("works", function()
|
||||
assert.is_nil(jp.resolve("string", "/string"))
|
||||
assert.same(example, jp.resolve(example, ""));
|
||||
assert.same({ "bar", "baz" }, jp.resolve(example, "/foo"));
|
||||
assert.same("bar", jp.resolve(example, "/foo/0"));
|
||||
assert.same(nil, jp.resolve(example, "/foo/-"));
|
||||
assert.same(0, jp.resolve(example, "/"));
|
||||
assert.same(1, jp.resolve(example, "/a~1b"));
|
||||
assert.same(2, jp.resolve(example, "/c%d"));
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
local jwt = require "util.jwt";
|
||||
local test_keys = require "spec.inputs.test_keys";
|
||||
|
||||
local array = require "util.array";
|
||||
local iter = require "util.iterators";
|
||||
local set = require "util.set";
|
||||
|
||||
-- Ignore long lines. We have some long tokens embedded here.
|
||||
--luacheck: ignore 631
|
||||
|
||||
describe("util.jwt", function ()
|
||||
it("validates", function ()
|
||||
|
@ -8,6 +16,9 @@ describe("util.jwt", function ()
|
|||
local ok, parsed = jwt.verify(key, token);
|
||||
assert.truthy(ok)
|
||||
assert.same({ payload = "this" }, parsed);
|
||||
|
||||
|
||||
|
||||
end);
|
||||
it("rejects invalid", function ()
|
||||
local key = "secret";
|
||||
|
@ -16,5 +27,233 @@ describe("util.jwt", function ()
|
|||
local ok = jwt.verify(key, token);
|
||||
assert.falsy(ok)
|
||||
end);
|
||||
|
||||
local function jwt_reference_token(token)
|
||||
return {
|
||||
name = "jwt.io reference";
|
||||
token;
|
||||
{ -- payload
|
||||
sub = "1234567890";
|
||||
name = "John Doe";
|
||||
admin = true;
|
||||
iat = 1516239022;
|
||||
};
|
||||
};
|
||||
end
|
||||
|
||||
local untested_algorithms = set.new(array.collect(iter.keys(jwt._algorithms)));
|
||||
|
||||
local test_cases = {
|
||||
{
|
||||
algorithm = "HS256";
|
||||
keys = {
|
||||
{ "your-256-bit-secret", "your-256-bit-secret" };
|
||||
{ "another-secret", "another-secret" };
|
||||
};
|
||||
|
||||
jwt_reference_token [[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhZG1pbiI6dHJ1ZX0.F-cvL2RcfQhUtCavIM7q7zYE8drmj2LJk0JRkrS6He4]];
|
||||
};
|
||||
{
|
||||
algorithm = "HS384";
|
||||
keys = {
|
||||
{ "your-384-bit-secret", "your-384-bit-secret" };
|
||||
{ "another-secret", "another-secret" };
|
||||
};
|
||||
|
||||
jwt_reference_token [[eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.bQTnz6AuMJvmXXQsVPrxeQNvzDkimo7VNXxHeSBfClLufmCVZRUuyTwJF311JHuh]];
|
||||
};
|
||||
{
|
||||
algorithm = "HS512";
|
||||
keys = {
|
||||
{ "your-512-bit-secret", "your-512-bit-secret" };
|
||||
{ "another-secret", "another-secret" };
|
||||
};
|
||||
|
||||
jwt_reference_token [[eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.VFb0qJ1LRg_4ujbZoRMXnVkUgiuKq5KxWqNdbKq_G9Vvz-S1zZa9LPxtHWKa64zDl2ofkT8F6jBt_K4riU-fPg]];
|
||||
};
|
||||
{
|
||||
algorithm = "ES256";
|
||||
keys = {
|
||||
{ test_keys.ecdsa_private_pem, test_keys.ecdsa_public_pem };
|
||||
{ test_keys.alt_ecdsa_private_pem, test_keys.alt_ecdsa_public_pem };
|
||||
};
|
||||
{
|
||||
name = "jwt.io reference";
|
||||
[[eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA]];
|
||||
{ -- payload
|
||||
sub = "1234567890";
|
||||
name = "John Doe";
|
||||
admin = true;
|
||||
iat = 1516239022;
|
||||
};
|
||||
};
|
||||
};
|
||||
{
|
||||
algorithm = "ES512";
|
||||
keys = {
|
||||
{ test_keys.ecdsa_521_private_pem, test_keys.ecdsa_521_public_pem };
|
||||
{ test_keys.alt_ecdsa_521_private_pem, test_keys.alt_ecdsa_521_public_pem };
|
||||
};
|
||||
{
|
||||
name = "jwt.io reference";
|
||||
[[eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.AbVUinMiT3J_03je8WTOIl-VdggzvoFgnOsdouAs-DLOtQzau9valrq-S6pETyi9Q18HH-EuwX49Q7m3KC0GuNBJAc9Tksulgsdq8GqwIqZqDKmG7hNmDzaQG1Dpdezn2qzv-otf3ZZe-qNOXUMRImGekfQFIuH_MjD2e8RZyww6lbZk]];
|
||||
{ -- payload
|
||||
sub = "1234567890";
|
||||
name = "John Doe";
|
||||
admin = true;
|
||||
iat = 1516239022;
|
||||
};
|
||||
};
|
||||
};
|
||||
{
|
||||
algorithm = "RS256";
|
||||
keys = {
|
||||
{ test_keys.rsa_private_pem, test_keys.rsa_public_pem };
|
||||
{ test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
|
||||
};
|
||||
{
|
||||
name = "jwt.io reference";
|
||||
[[eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ]];
|
||||
{ -- payload
|
||||
sub = "1234567890";
|
||||
name = "John Doe";
|
||||
admin = true;
|
||||
iat = 1516239022;
|
||||
};
|
||||
};
|
||||
};
|
||||
{
|
||||
algorithm = "RS384";
|
||||
keys = {
|
||||
{ test_keys.rsa_private_pem, test_keys.rsa_public_pem };
|
||||
{ test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
|
||||
};
|
||||
|
||||
jwt_reference_token [[eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.o1hC1xYbJolSyh0-bOY230w22zEQSk5TiBfc-OCvtpI2JtYlW-23-8B48NpATozzMHn0j3rE0xVUldxShzy0xeJ7vYAccVXu2Gs9rnTVqouc-UZu_wJHkZiKBL67j8_61L6SXswzPAQu4kVDwAefGf5hyYBUM-80vYZwWPEpLI8K4yCBsF6I9N1yQaZAJmkMp_Iw371Menae4Mp4JusvBJS-s6LrmG2QbiZaFaxVJiW8KlUkWyUCns8-qFl5OMeYlgGFsyvvSHvXCzQrsEXqyCdS4tQJd73ayYA4SPtCb9clz76N1zE5WsV4Z0BYrxeb77oA7jJhh994RAPzCG0hmQ]];
|
||||
};
|
||||
{
|
||||
algorithm = "RS512";
|
||||
keys = {
|
||||
{ test_keys.rsa_private_pem, test_keys.rsa_public_pem };
|
||||
{ test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
|
||||
};
|
||||
|
||||
jwt_reference_token [[eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.jYW04zLDHfR1v7xdrW3lCGZrMIsVe0vWCfVkN2DRns2c3MN-mcp_-RE6TN9umSBYoNV-mnb31wFf8iun3fB6aDS6m_OXAiURVEKrPFNGlR38JSHUtsFzqTOj-wFrJZN4RwvZnNGSMvK3wzzUriZqmiNLsG8lktlEn6KA4kYVaM61_NpmPHWAjGExWv7cjHYupcjMSmR8uMTwN5UuAwgW6FRstCJEfoxwb0WKiyoaSlDuIiHZJ0cyGhhEmmAPiCwtPAwGeaL1yZMcp0p82cpTQ5Qb-7CtRov3N4DcOHgWYk6LomPR5j5cCkePAz87duqyzSMpCB0mCOuE3CU2VMtGeQ]];
|
||||
};
|
||||
{
|
||||
algorithm = "PS256";
|
||||
keys = {
|
||||
{ test_keys.rsa_private_pem, test_keys.rsa_public_pem };
|
||||
{ test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
|
||||
};
|
||||
|
||||
jwt_reference_token [[eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.iOeNU4dAFFeBwNj6qdhdvm-IvDQrTa6R22lQVJVuWJxorJfeQww5Nwsra0PjaOYhAMj9jNMO5YLmud8U7iQ5gJK2zYyepeSuXhfSi8yjFZfRiSkelqSkU19I-Ja8aQBDbqXf2SAWA8mHF8VS3F08rgEaLCyv98fLLH4vSvsJGf6ueZSLKDVXz24rZRXGWtYYk_OYYTVgR1cg0BLCsuCvqZvHleImJKiWmtS0-CymMO4MMjCy_FIl6I56NqLE9C87tUVpo1mT-kbg5cHDD8I7MjCW5Iii5dethB4Vid3mZ6emKjVYgXrtkOQ-JyGMh6fnQxEFN1ft33GX2eRHluK9eg]];
|
||||
};
|
||||
{
|
||||
algorithm = "PS384";
|
||||
keys = {
|
||||
{ test_keys.rsa_private_pem, test_keys.rsa_public_pem };
|
||||
{ test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
|
||||
};
|
||||
|
||||
jwt_reference_token [[eyJhbGciOiJQUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.Lfe_aCQme_gQpUk9-6l9qesu0QYZtfdzfy08w8uqqPH_gnw-IVyQwyGLBHPFBJHMbifdSMxPjJjkCD0laIclhnBhowILu6k66_5Y2z78GHg8YjKocAvB-wSUiBhuV6hXVxE5emSjhfVz2OwiCk2bfk2hziRpkdMvfcITkCx9dmxHU6qcEIsTTHuH020UcGayB1-IoimnjTdCsV1y4CMr_ECDjBrqMdnontkqKRIM1dtmgYFsJM6xm7ewi_ksG_qZHhaoBkxQ9wq9OVQRGiSZYowCp73d2BF3jYMhdmv2JiaUz5jRvv6lVU7Quq6ylVAlSPxeov9voYHO1mgZFCY1kQ]];
|
||||
};
|
||||
{
|
||||
algorithm = "PS512";
|
||||
keys = {
|
||||
{ test_keys.rsa_private_pem, test_keys.rsa_public_pem };
|
||||
{ test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem };
|
||||
};
|
||||
|
||||
jwt_reference_token [[eyJhbGciOiJQUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.J5W09-rNx0pt5_HBiydR-vOluS6oD-RpYNa8PVWwMcBDQSXiw6-EPW8iSsalXPspGj3ouQjAnOP_4-zrlUUlvUIt2T79XyNeiKuooyIFvka3Y5NnGiOUBHWvWcWp4RcQFMBrZkHtJM23sB5D7Wxjx0-HFeNk-Y3UJgeJVhg5NaWXypLkC4y0ADrUBfGAxhvGdRdULZivfvzuVtv6AzW6NRuEE6DM9xpoWX_4here-yvLS2YPiBTZ8xbB3axdM99LhES-n52lVkiX5AWg2JJkEROZzLMpaacA_xlbUz_zbIaOaoqk8gB5oO7kI6sZej3QAdGigQy-hXiRnW_L98d4GQ]];
|
||||
};
|
||||
};
|
||||
|
||||
local function do_verify_test(algorithm, verifying_key, token, expect_payload)
|
||||
local verify = jwt.new_verifier(algorithm, verifying_key);
|
||||
|
||||
assert.is_string(token);
|
||||
local result = {verify(token)};
|
||||
if expect_payload then
|
||||
assert.same({
|
||||
true; -- success
|
||||
expect_payload; -- payload
|
||||
}, result);
|
||||
else
|
||||
assert.same({
|
||||
false;
|
||||
"signature-mismatch";
|
||||
}, result);
|
||||
end
|
||||
end
|
||||
|
||||
local function do_sign_verify_test(algorithm, signing_key, verifying_key, expect_success, expect_token)
|
||||
local sign = jwt.new_signer(algorithm, signing_key);
|
||||
|
||||
local test_payload = {
|
||||
sub = "1234567890";
|
||||
name = "John Doe";
|
||||
admin = true;
|
||||
iat = 1516239022;
|
||||
};
|
||||
|
||||
local token = sign(test_payload);
|
||||
|
||||
if expect_token then
|
||||
assert.equal(expect_token, token);
|
||||
end
|
||||
|
||||
do_verify_test(algorithm, verifying_key, token, expect_success and test_payload or false);
|
||||
end
|
||||
|
||||
|
||||
for _, algorithm_tests in ipairs(test_cases) do
|
||||
local algorithm = algorithm_tests.algorithm;
|
||||
local keypairs = algorithm_tests.keys;
|
||||
|
||||
untested_algorithms:remove(algorithm);
|
||||
|
||||
describe(algorithm, function ()
|
||||
describe("can do basic sign and verify", function ()
|
||||
for keypair_n, keypair in ipairs(keypairs) do
|
||||
local signing_key, verifying_key = keypair[1], keypair[2];
|
||||
it(("(test key pair %d)"):format(keypair_n), function ()
|
||||
do_sign_verify_test(algorithm, signing_key, verifying_key, true);
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
if #keypairs >= 2 then
|
||||
it("rejects invalid tokens", function ()
|
||||
do_sign_verify_test(algorithm, keypairs[1][1], keypairs[2][2], false);
|
||||
end);
|
||||
else
|
||||
pending("rejects invalid tokens", function ()
|
||||
error("Needs at least 2 key pairs");
|
||||
end);
|
||||
end
|
||||
|
||||
if #algorithm_tests > 0 then
|
||||
for test_n, test_case in ipairs(algorithm_tests) do
|
||||
it("can verify "..(test_case.name or (("test case %d"):format(test_n))), function ()
|
||||
do_verify_test(
|
||||
algorithm,
|
||||
test_case.verifying_key or keypairs[1][2],
|
||||
test_case[1],
|
||||
test_case[2]
|
||||
);
|
||||
end);
|
||||
end
|
||||
else
|
||||
pending("can verify reference tokens", function ()
|
||||
error("No test tokens provided");
|
||||
end);
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
for algorithm in untested_algorithms do
|
||||
pending(algorithm.." tests", function () end);
|
||||
end
|
||||
end);
|
||||
|
||||
|
|
118
spec/util_paseto_spec.lua
Normal file
118
spec/util_paseto_spec.lua
Normal file
|
@ -0,0 +1,118 @@
|
|||
-- Ignore long lines in this file
|
||||
--luacheck: ignore 631
|
||||
|
||||
describe("util.paseto", function ()
|
||||
local paseto = require "util.paseto";
|
||||
local json = require "util.json";
|
||||
|
||||
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
|
||||
|
||||
describe("v4.public", function ()
|
||||
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);
|
|
@ -1,6 +1,35 @@
|
|||
describe("util.poll", function ()
|
||||
it("loads", function ()
|
||||
require "util.poll"
|
||||
describe("util.poll", function()
|
||||
local poll;
|
||||
setup(function()
|
||||
poll = require "util.poll";
|
||||
end);
|
||||
it("loads", function()
|
||||
assert.is_table(poll);
|
||||
assert.is_function(poll.new);
|
||||
assert.is_string(poll.api);
|
||||
end);
|
||||
describe("new", function()
|
||||
local p;
|
||||
setup(function()
|
||||
p = poll.new();
|
||||
end)
|
||||
it("times out", function ()
|
||||
local fd, err = p:wait(0);
|
||||
assert.falsy(fd);
|
||||
assert.equal("timeout", err);
|
||||
end);
|
||||
it("works", function()
|
||||
-- stdout should be writable, right?
|
||||
assert.truthy(p:add(1, false, true));
|
||||
local fd, r, w = p:wait(1);
|
||||
assert.is_number(fd);
|
||||
assert.is_boolean(r);
|
||||
assert.is_boolean(w);
|
||||
assert.equal(1, fd);
|
||||
assert.falsy(r);
|
||||
assert.truthy(w);
|
||||
assert.truthy(p:del(1));
|
||||
end);
|
||||
end)
|
||||
end);
|
||||
|
||||
|
|
|
@ -7,6 +7,11 @@ describe("util.promise", function ()
|
|||
assert(promise.new());
|
||||
end);
|
||||
end);
|
||||
it("supplies a sensible tostring()", function ()
|
||||
local s = tostring(promise.new());
|
||||
assert.truthy(s:find("promise", 1, true));
|
||||
assert.truthy(s:find("pending", 1, true));
|
||||
end);
|
||||
it("notifies immediately for fulfilled promises", function ()
|
||||
local p = promise.new(function (resolve)
|
||||
resolve("foo");
|
||||
|
@ -30,6 +35,27 @@ describe("util.promise", function ()
|
|||
r("foo");
|
||||
assert.spy(cb).was_called(1);
|
||||
end);
|
||||
it("ignores resolve/reject of settled promises", function ()
|
||||
local res, rej;
|
||||
local p = promise.new(function (resolve, reject)
|
||||
res, rej = resolve, reject;
|
||||
end);
|
||||
local cb = spy.new(function (v)
|
||||
assert.equal("foo", v);
|
||||
end);
|
||||
p:next(cb, cb);
|
||||
assert.spy(cb).was_called(0);
|
||||
res("foo");
|
||||
assert.spy(cb).was_called(1);
|
||||
rej("bar");
|
||||
assert.spy(cb).was_called(1);
|
||||
rej(promise.resolve("bar"));
|
||||
assert.spy(cb).was_called(1);
|
||||
res(promise.reject("bar"));
|
||||
assert.spy(cb).was_called(1);
|
||||
res(promise.resolve("bar"));
|
||||
assert.spy(cb).was_called(1);
|
||||
end);
|
||||
it("allows chaining :next() calls", function ()
|
||||
local r;
|
||||
local result;
|
||||
|
@ -438,6 +464,26 @@ describe("util.promise", function ()
|
|||
{ status = "rejected", reason = "this fails" };
|
||||
}, result);
|
||||
end);
|
||||
it("works when all promises reject", function ()
|
||||
local r1, r2;
|
||||
local p1, p2 = promise.new(function (_, reject) r1 = reject end), promise.new(function (_, reject) r2 = reject end);
|
||||
local p = promise.all_settled({ p1, p2 });
|
||||
|
||||
local result;
|
||||
local cb = spy.new(function (v)
|
||||
result = v;
|
||||
end);
|
||||
p:next(cb);
|
||||
assert.spy(cb).was_called(0);
|
||||
r2("this fails");
|
||||
assert.spy(cb).was_called(0);
|
||||
r1("this fails too");
|
||||
assert.spy(cb).was_called(1);
|
||||
assert.same({
|
||||
{ status = "rejected", reason = "this fails too" };
|
||||
{ status = "rejected", reason = "this fails" };
|
||||
}, result);
|
||||
end);
|
||||
it("works with non-numeric keys", function ()
|
||||
local r1, r2;
|
||||
local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end);
|
||||
|
|
134
spec/util_roles_spec.lua
Normal file
134
spec/util_roles_spec.lua
Normal file
|
@ -0,0 +1,134 @@
|
|||
describe("util.roles", function ()
|
||||
randomize(false);
|
||||
local roles;
|
||||
it("can be loaded", function ()
|
||||
roles = require "util.roles";
|
||||
end);
|
||||
local test_role;
|
||||
it("can create a new role", function ()
|
||||
test_role = roles.new();
|
||||
assert.is_not_nil(test_role);
|
||||
assert.is_truthy(roles.is_role(test_role));
|
||||
end);
|
||||
describe("role object", function ()
|
||||
it("can be initialized with permissions", function ()
|
||||
local test_role_2 = roles.new({
|
||||
permissions = {
|
||||
perm1 = true;
|
||||
perm2 = false;
|
||||
};
|
||||
});
|
||||
assert.truthy(test_role_2:may("perm1"));
|
||||
assert.falsy(test_role_2:may("perm2"));
|
||||
end);
|
||||
it("has a sensible tostring", function ()
|
||||
local test_role_2 = roles.new({
|
||||
id = "test-role-2";
|
||||
name = "Test Role 2";
|
||||
});
|
||||
assert.truthy(tostring(test_role_2):find(test_role_2.id, 1, true));
|
||||
assert.truthy(tostring(test_role_2):find("Test Role 2", 1, true));
|
||||
end);
|
||||
it("is restrictive by default", function ()
|
||||
assert.falsy(test_role:may("my-permission"));
|
||||
end);
|
||||
it("allows you to set permissions", function ()
|
||||
test_role:set_permission("my-permission", true);
|
||||
assert.truthy(test_role:may("my-permission"));
|
||||
end);
|
||||
it("allows you to set negative permissions", function ()
|
||||
test_role:set_permission("my-other-permission", false);
|
||||
assert.falsy(test_role:may("my-other-permission"));
|
||||
end);
|
||||
it("does not allows you to override previously set permissions by default", function ()
|
||||
local ok, err = test_role:set_permission("my-permission", false);
|
||||
assert.falsy(ok);
|
||||
assert.is_equal("policy-already-exists", err);
|
||||
-- Confirm old permission still in place
|
||||
assert.truthy(test_role:may("my-permission"));
|
||||
end);
|
||||
it("allows you to explicitly override previously set permissions", function ()
|
||||
assert.truthy(test_role:set_permission("my-permission", false, true));
|
||||
assert.falsy(test_role:may("my-permission"));
|
||||
end);
|
||||
describe("inheritance", function ()
|
||||
local child_role;
|
||||
it("works", function ()
|
||||
test_role:set_permission("inherited-permission", true);
|
||||
child_role = roles.new({
|
||||
inherits = { test_role };
|
||||
});
|
||||
assert.truthy(child_role:may("inherited-permission"));
|
||||
assert.falsy(child_role:may("my-permission"));
|
||||
end);
|
||||
it("allows listing policies", function ()
|
||||
local expected = {
|
||||
["my-permission"] = false;
|
||||
["my-other-permission"] = false;
|
||||
["inherited-permission"] = true;
|
||||
};
|
||||
local received = {};
|
||||
for permission_name, permission_policy in child_role:policies() do
|
||||
received[permission_name] = permission_policy;
|
||||
end
|
||||
assert.same(expected, received);
|
||||
end);
|
||||
it("supports multiple depths of inheritance", function ()
|
||||
local grandchild_role = roles.new({
|
||||
inherits = { child_role };
|
||||
});
|
||||
assert.truthy(grandchild_role:may("inherited-permission"));
|
||||
end);
|
||||
describe("supports ordered inheritance from multiple roles", function ()
|
||||
local parent_role = roles.new();
|
||||
local final_role = roles.new({
|
||||
-- Yes, the names are getting confusing.
|
||||
-- btw, test_role is inherited through child_role.
|
||||
inherits = { parent_role, child_role };
|
||||
});
|
||||
|
||||
local test_cases = {
|
||||
-- { <final_role policy>, <parent_role policy>, <test_role policy> }
|
||||
{ true, nil, false, result = true };
|
||||
{ nil, false, true, result = false };
|
||||
{ nil, true, false, result = true };
|
||||
{ nil, nil, false, result = false };
|
||||
{ nil, nil, true, result = true };
|
||||
};
|
||||
|
||||
for n, test_case in ipairs(test_cases) do
|
||||
it("(case "..n..")", function ()
|
||||
local perm_name = ("multi-inheritance-perm-%d"):format(n);
|
||||
assert.truthy(final_role:set_permission(perm_name, test_case[1]));
|
||||
assert.truthy(parent_role:set_permission(perm_name, test_case[2]));
|
||||
assert.truthy(test_role:set_permission(perm_name, test_case[3]));
|
||||
assert.equal(test_case.result, final_role:may(perm_name));
|
||||
end);
|
||||
end
|
||||
end);
|
||||
it("updates child roles when parent roles change", function ()
|
||||
assert.truthy(child_role:may("inherited-permission"));
|
||||
assert.truthy(test_role:set_permission("inherited-permission", false, true));
|
||||
assert.falsy(child_role:may("inherited-permission"));
|
||||
end);
|
||||
end);
|
||||
describe("cloning", function ()
|
||||
local cloned_role;
|
||||
it("works", function ()
|
||||
assert.truthy(test_role:set_permission("perm-1", true));
|
||||
cloned_role = test_role:clone();
|
||||
assert.truthy(cloned_role:may("perm-1"));
|
||||
end);
|
||||
it("isolates changes", function ()
|
||||
-- After cloning, changes in either the original or the clone
|
||||
-- should not appear in the other.
|
||||
assert.truthy(test_role:set_permission("perm-1", false, true));
|
||||
assert.truthy(test_role:set_permission("perm-2", true));
|
||||
assert.truthy(cloned_role:set_permission("perm-3", true));
|
||||
assert.truthy(cloned_role:may("perm-1"));
|
||||
assert.falsy(cloned_role:may("perm-2"));
|
||||
assert.falsy(test_role:may("perm-3"));
|
||||
end);
|
||||
end);
|
||||
end);
|
||||
end);
|
|
@ -5,6 +5,9 @@ describe("util.smqueue", function()
|
|||
|
||||
describe("#new()", function()
|
||||
it("should work", function()
|
||||
assert.has_error(function () smqueue.new(-1) end);
|
||||
assert.has_error(function () smqueue.new(0) end);
|
||||
assert.not_has_error(function () smqueue.new(1) end);
|
||||
local q = smqueue.new(10);
|
||||
assert.truthy(q);
|
||||
end)
|
||||
|
|
|
@ -314,6 +314,20 @@ describe("util.stanza", function()
|
|||
end)
|
||||
end)
|
||||
|
||||
describe("#add_error()", function ()
|
||||
describe("basics", function ()
|
||||
local s = st.stanza("custom", { xmlns = "urn:example:foo" });
|
||||
local e = s:add_error("cancel", "not-acceptable", "UNACCEPTABLE!!!! ONE MILLION YEARS DUNGEON!")
|
||||
:tag("dungeon", { xmlns = "urn:uuid:c9026187-5b05-4e70-b265-c3b6338a7d0f", period="1000000years"});
|
||||
assert.equal(s, e);
|
||||
local typ, cond, text, extra = e:get_error();
|
||||
assert.equal("cancel", typ);
|
||||
assert.equal("not-acceptable", cond);
|
||||
assert.equal("UNACCEPTABLE!!!! ONE MILLION YEARS DUNGEON!", text);
|
||||
assert.is_nil(extra);
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("should reject #invalid", function ()
|
||||
local invalid_names = {
|
||||
["empty string"] = "", ["characters"] = "<>";
|
||||
|
|
|
@ -12,6 +12,17 @@ describe("util.table", function ()
|
|||
assert.same({ "lorem", "ipsum", "dolor", "sit", "amet", n = 5 }, u_table.pack("lorem", "ipsum", "dolor", "sit", "amet"));
|
||||
end);
|
||||
end);
|
||||
|
||||
describe("move()", function ()
|
||||
it("works", function ()
|
||||
local t1 = { "apple", "banana", "carrot" };
|
||||
local t2 = { "cat", "donkey", "elephant" };
|
||||
local t3 = {};
|
||||
u_table.move(t1, 1, 3, 1, t3);
|
||||
u_table.move(t2, 1, 3, 3, t3);
|
||||
assert.same({ "apple", "banana", "cat", "donkey", "elephant" }, t3);
|
||||
end);
|
||||
end);
|
||||
end);
|
||||
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ local uuid = require "util.uuid";
|
|||
describe("util.uuid", function()
|
||||
describe("#generate()", function()
|
||||
it("should work follow the UUID pattern", function()
|
||||
-- https://tools.ietf.org/html/rfc4122#section-4.4
|
||||
-- https://www.rfc-editor.org/rfc/rfc4122.html#section-4.4
|
||||
|
||||
local pattern = "^" .. table.concat({
|
||||
string.rep("%x", 8),
|
||||
|
|
74
teal-src/core/storagemanager.d.tl
Normal file
74
teal-src/core/storagemanager.d.tl
Normal file
|
@ -0,0 +1,74 @@
|
|||
-- Storage local record API Description
|
||||
--
|
||||
-- This is written as a TypedLua description
|
||||
|
||||
-- Key-Value stores (the default)
|
||||
|
||||
local stanza = require"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
|
|
@ -62,7 +62,12 @@ global record moduleapi
|
|||
send_iq : function (moduleapi, st.stanza_t, util_session, number)
|
||||
broadcast : function (moduleapi, { string }, st.stanza_t, function)
|
||||
type timer_callback = function (number, ... : any) : number
|
||||
add_timer : function (moduleapi, number, timer_callback, ... : any)
|
||||
record timer_wrapper
|
||||
stop : function (timer_wrapper)
|
||||
disarm : function (timer_wrapper)
|
||||
reschedule : function (timer_wrapper, number)
|
||||
end
|
||||
add_timer : function (moduleapi, number, timer_callback, ... : any) : timer_wrapper
|
||||
get_directory : function (moduleapi) : string
|
||||
enum file_mode
|
||||
"r" "w" "a" "r+" "w+" "a+"
|
||||
|
@ -121,6 +126,11 @@ global record moduleapi
|
|||
path : string
|
||||
resource_path : string
|
||||
|
||||
-- access control
|
||||
may : function (moduleapi, string, table|string)
|
||||
default_permission : function (string, string)
|
||||
default_permissions : function (string, { string })
|
||||
|
||||
-- methods the module can add
|
||||
load : function ()
|
||||
add_host : function (moduleapi)
|
||||
|
|
86
teal-src/net/http.d.tl
Normal file
86
teal-src/net/http.d.tl
Normal file
|
@ -0,0 +1,86 @@
|
|||
local Promise = require "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
|
2
teal-src/net/http/codes.d.tl
Normal file
2
teal-src/net/http/codes.d.tl
Normal file
|
@ -0,0 +1,2 @@
|
|||
local type response_codes = { integer : string }
|
||||
return response_codes
|
22
teal-src/net/http/errors.d.tl
Normal file
22
teal-src/net/http/errors.d.tl
Normal file
|
@ -0,0 +1,22 @@
|
|||
local record http_errors
|
||||
enum known_conditions
|
||||
"cancelled"
|
||||
"connection-closed"
|
||||
"certificate-chain-invalid"
|
||||
"certificate-verify-failed"
|
||||
"connection failed"
|
||||
"invalid-url"
|
||||
"unable to resolve service"
|
||||
end
|
||||
type registry_keys = known_conditions | integer
|
||||
record error
|
||||
type : string
|
||||
condition : string
|
||||
code : integer
|
||||
text : string
|
||||
end
|
||||
registry : { registry_keys : error }
|
||||
new : function (integer, known_conditions, table)
|
||||
new : function (integer, string, table)
|
||||
end
|
||||
return http_errors
|
14
teal-src/net/http/files.d.tl
Normal file
14
teal-src/net/http/files.d.tl
Normal file
|
@ -0,0 +1,14 @@
|
|||
local record serve_options
|
||||
path : string
|
||||
mime_map : { string : string }
|
||||
cache_size : integer
|
||||
cache_max_file_size : integer
|
||||
index_files : { string }
|
||||
directory_index : boolean
|
||||
end
|
||||
|
||||
local record http_files
|
||||
serve : function(serve_options|string) : function
|
||||
end
|
||||
|
||||
return http_files
|
58
teal-src/net/http/parser.d.tl
Normal file
58
teal-src/net/http/parser.d.tl
Normal file
|
@ -0,0 +1,58 @@
|
|||
local record httpstream
|
||||
feed : function(httpstream, string)
|
||||
end
|
||||
|
||||
local type sink_cb = function ()
|
||||
|
||||
local record httppacket
|
||||
enum http_method
|
||||
"HEAD"
|
||||
"GET"
|
||||
"POST"
|
||||
"PUT"
|
||||
"DELETE"
|
||||
"OPTIONS"
|
||||
-- etc
|
||||
end
|
||||
method : http_method
|
||||
record url_details
|
||||
path : string
|
||||
query : string
|
||||
end
|
||||
url : url_details
|
||||
path : string
|
||||
enum http_version
|
||||
"1.0"
|
||||
"1.1"
|
||||
end
|
||||
httpversion : http_version
|
||||
headers : { string : string }
|
||||
body : string | boolean
|
||||
body_sink : sink_cb
|
||||
chunked : boolean
|
||||
partial : boolean
|
||||
end
|
||||
|
||||
local enum error_conditions
|
||||
"cancelled"
|
||||
"connection-closed"
|
||||
"certificate-chain-invalid"
|
||||
"certificate-verify-failed"
|
||||
"connection failed"
|
||||
"invalid-url"
|
||||
"unable to resolve service"
|
||||
end
|
||||
|
||||
local type success_cb = function (httppacket)
|
||||
local type error_cb = function (error_conditions)
|
||||
|
||||
local enum stream_mode
|
||||
"client"
|
||||
"server"
|
||||
end
|
||||
|
||||
local record lib
|
||||
new : function (success_cb, error_cb, stream_mode) : httpstream
|
||||
end
|
||||
|
||||
return lib
|
6
teal-src/net/http/server.d.tl
Normal file
6
teal-src/net/http/server.d.tl
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
local record http_server
|
||||
-- TODO
|
||||
end
|
||||
|
||||
return http_server
|
65
teal-src/net/server.d.tl
Normal file
65
teal-src/net/server.d.tl
Normal file
|
@ -0,0 +1,65 @@
|
|||
local record server
|
||||
record LuaSocketTCP
|
||||
end
|
||||
record LuaSecCTX
|
||||
end
|
||||
|
||||
record extra_settings
|
||||
end
|
||||
|
||||
record interface
|
||||
end
|
||||
enum socket_type
|
||||
"tcp"
|
||||
"tcp6"
|
||||
"tcp4"
|
||||
end
|
||||
|
||||
record listeners
|
||||
onconnect : function (interface)
|
||||
ondetach : function (interface)
|
||||
onattach : function (interface, string)
|
||||
onincoming : function (interface, string, string)
|
||||
ondrain : function (interface)
|
||||
onreadtimeout : function (interface)
|
||||
onstarttls : function (interface)
|
||||
onstatus : function (interface, string)
|
||||
ondisconnect : function (interface, string)
|
||||
end
|
||||
|
||||
get_backend : function () : string
|
||||
|
||||
type port = string | integer
|
||||
enum read_mode
|
||||
"*a"
|
||||
"*l"
|
||||
end
|
||||
type read_size = read_mode | integer
|
||||
addserver : function (string, port, listeners, read_size, LuaSecCTX) : interface
|
||||
addclient : function (string, port, listeners, read_size, LuaSecCTX, socket_type, extra_settings) : interface
|
||||
record listen_config
|
||||
read_size : read_size
|
||||
tls_ctx : LuaSecCTX
|
||||
tls_direct : boolean
|
||||
sni_hosts : { string : LuaSecCTX }
|
||||
end
|
||||
listen : function (string, port, listeners, listen_config) : interface
|
||||
enum quitting
|
||||
"quitting"
|
||||
end
|
||||
loop : function () : quitting
|
||||
closeall : function ()
|
||||
setquitting : function (boolean | quitting)
|
||||
|
||||
wrapclient : function (LuaSocketTCP, string, port, listeners, read_size, LuaSecCTX, extra_settings) : interface
|
||||
wrapserver : function (LuaSocketTCP, string, port, listeners, listen_config) : interface
|
||||
watchfd : function (integer | LuaSocketTCP, function (interface), function (interface)) : interface
|
||||
link : function ()
|
||||
|
||||
record config
|
||||
end
|
||||
set_config : function (config)
|
||||
|
||||
end
|
||||
|
||||
return server
|
|
@ -88,8 +88,8 @@ local function run_task(task : task_spec)
|
|||
task:save(started_at);
|
||||
end
|
||||
|
||||
local task_runner = async.runner(run_task);
|
||||
module:add_timer(1, function() : integer
|
||||
local task_runner : async.runner_t<task_spec> = async.runner(run_task);
|
||||
scheduled = module:add_timer(1, function() : integer
|
||||
module:log("info", "Running periodic tasks");
|
||||
local delay = 3600;
|
||||
for host in pairs(active_hosts) do
|
||||
|
|
9
teal-src/util/array.d.tl
Normal file
9
teal-src/util/array.d.tl
Normal file
|
@ -0,0 +1,9 @@
|
|||
local record array_t<T>
|
||||
{ T }
|
||||
end
|
||||
|
||||
local record lib
|
||||
metamethod __call : function () : array_t
|
||||
end
|
||||
|
||||
return lib
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue