prosody.version
config.unix
*.patch
-*.diff
*.orig
*.rej
*.save
*.log
*.err
*.debug
-*.dll
-*.exp
-*.lib
-*.obj
install -d $(MODULES)/muc
install -m644 plugins/muc/* $(MODULES)/muc
install -m644 certs/* $(CONFIG)/certs
- install -d $(MODULES)/adhoc
- install -m644 plugins/adhoc/*.lua $(MODULES)/adhoc
+ install -m644 plugins/*.lua $(MODULES)
install -m644 man/prosodyctl.man $(MAN)/man1/prosodyctl.1
test -e $(CONFIG)/prosody.cfg.lua || install -m644 prosody.cfg.lua.install $(CONFIG)/prosody.cfg.lua
test -e prosody.version && install prosody.version $(SOURCE)/prosody.version || true
+== 0.8 ==
+- Ad-hoc commands:
+ http://code.google.com/p/prosody-modules/wiki/mod_adhoc
+ http://code.google.com/p/prosody-modules/wiki/mod_adhoc_cmd_admin
+ http://code.google.com/p/prosody-modules/wiki/mod_adhoc_cmd_ping
+ http://code.google.com/p/prosody-modules/wiki/mod_adhoc_cmd_uptime
+
+- Pubsub
+- Data storage backend abstraction
+
== 0.9 ==
-- IPv6
-- SASL EXTERNAL
-- Roster providers
-- Web interface
+- Clustering
== 1.0 ==
-- Clustering
+- Web interface?
- World domination
LUA_INCDIR="/usr/include"
LUA_LIBDIR="/usr/lib"
IDN_LIB=idn
-ICU_FLAGS="-licui18n -licudata -licuuc"
OPENSSL_LIB=crypto
CC=gcc
-CXX=g++
LD=gcc
CFLAGS="-fPIC -Wall"
LDFLAGS="-shared"
-IDN_LIBRARY=idn
# Help
show_help() {
--help This help.
--ostype=OS Use one of the OS presets.
- May be one of: debian, macosx, linux, freebsd
+ May be one of: debian, macosx, linux
--prefix=DIR Prefix where Prosody should be installed.
Default is $PREFIX
--sysconfdir=DIR Location where the config file should be installed.
Default is \$LUA_DIR/lib
--with-idn=LIB The name of the IDN library to link with.
Default is $IDN_LIB
---idn-library=(idn|icu) Select library to use for IDNA functionality.
- idn: use GNU libidn (default)
- icu: use ICU from IBM
--with-ssl=LIB The name of the SSL to link with.
Default is $OPENSSL_LIB
--cflags=FLAGS Flags to pass to the compiler
--ostype=*)
OSTYPE="$value"
OSTYPE_SET=yes
- if [ "$OSTYPE" = "debian" ]
- then LUA_SUFFIX="5.1";
- LUA_SUFFIX_SET=yes
- LUA_INCDIR=/usr/include/lua5.1;
- LUA_INCDIR_SET=yes
- fi
- if [ "$OSTYPE" = "macosx" ]
- then LUA_INCDIR=/usr/local/include;
- LUA_INCDIR_SET=yes
- LUA_LIBDIR=/usr/local/lib
- LUA_LIBDIR_SET=yes
- LDFLAGS="-bundle -undefined dynamic_lookup"
- fi
- if [ "$OSTYPE" = "linux" ]
- then LUA_INCDIR=/usr/local/include;
- LUA_INCDIR_SET=yes
- LUA_LIBDIR=/usr/local/lib
- LUA_LIBDIR_SET=yes
- CFLAGS="-Wall -fPIC"
- LDFLAGS="-shared"
- fi
- if [ "$OSTYPE" = "freebsd" ]
- then LUA_INCDIR="/usr/local/include/lua51"
- LUA_INCDIR_SET=yes
- CFLAGS="-Wall -fPIC -I/usr/local/include"
- LDFLAGS="-I/usr/local/include -L/usr/local/lib -shared"
- LUA_SUFFIX="-5.1"
- LUA_SUFFIX_SET=yes
- LUA_DIR=/usr/local
- LUA_DIR_SET=yes
- fi
;;
--datadir=*)
DATADIR="$value"
--with-idn=*)
IDN_LIB="$value"
;;
- --idn-library=*)
- IDN_LIBRARY="$value"
- ;;
--with-ssl=*)
OPENSSL_LIB="$value"
;;
shift
done
+if [ "$OSTYPE_SET" = "yes" ]
+then
+ if [ "$OSTYPE" = "debian" ]
+ then LUA_SUFFIX="5.1";
+ LUA_SUFFIX_SET=yes
+ LUA_INCDIR=/usr/include/lua5.1;
+ LUA_INCDIR_SET=yes
+ fi
+ if [ "$OSTYPE" = "macosx" ]
+ then LUA_INCDIR=/usr/local/include;
+ LUA_INCDIR_SET=yes
+ LUA_LIBDIR=/usr/local/lib
+ LUA_LIBDIR_SET=yes
+ CFLAGS="-Wall"
+ LDFLAGS="-bundle -undefined dynamic_lookup"
+ fi
+ if [ "$OSTYPE" = "linux" ]
+ then LUA_INCDIR=/usr/local/include;
+ LUA_INCDIR_SET=yes
+ LUA_LIBDIR=/usr/local/lib
+ LUA_LIBDIR_SET=yes
+ CFLAGS="-Wall -fPIC"
+ LDFLAGS="-shared"
+ fi
+fi
+
if [ "$PREFIX_SET" = "yes" -a ! "$SYSCONFDIR_SET" = "yes" ]
then
if [ "$PREFIX" = "/usr" ]
LUA_BINDIR="$LUA_DIR/bin"
fi
-if [ "$IDN_LIBRARY" = "icu" ]
-then
- IDNA_LIBS="$ICU_FLAGS"
- CFLAGS="$CFLAGS -DUSE_STRINGPREP_ICU"
-fi
-if [ "$IDN_LIBRARY" = "idn" ]
-then
- IDNA_LIBS="-l$IDN_LIB"
-fi
-
echo -n "Checking Lua includes... "
lua_h="$LUA_INCDIR/lua.h"
if [ -e "$lua_h" ]
LUA_BINDIR=$LUA_BINDIR
REQUIRE_CONFIG=$REQUIRE_CONFIG
IDN_LIB=$IDN_LIB
-IDNA_LIBS=$IDNA_LIBS
OPENSSL_LIB=$OPENSSL_LIB
CFLAGS=$CFLAGS
LDFLAGS=$LDFLAGS
CC=$CC
-CXX=$CXX
LD=$LD
EOF
--- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
local configmanager = require "core.configmanager";
local log = require "util.logger".init("certmanager");
local ssl = ssl;
local setmetatable, tostring = setmetatable, tostring;
local prosody = prosody;
-local resolve_path = configmanager.resolve_relative_path;
-local config_path = prosody.paths.config;
module "certmanager"
--- Global SSL options if not overridden per-host
-local default_ssl_config = configmanager.get("*", "core", "ssl");
-local default_capath = "/etc/ssl/certs";
-local default_verify = (ssl and ssl.x509 and { "peer", "client_once", "continue", "ignore_purpose" }) or "none";
-local default_options = { "no_sslv2" };
+-- These are the defaults if not overridden in the config
+local default_ssl_ctx = { mode = "client", protocol = "sslv23", capath = "/etc/ssl/certs", verify = "none", options = "no_sslv2"; };
+local default_ssl_ctx_in = { mode = "server", protocol = "sslv23", capath = "/etc/ssl/certs", verify = "none", options = "no_sslv2"; };
-function create_context(host, mode, user_ssl_config)
- user_ssl_config = user_ssl_config or default_ssl_config;
+local default_ssl_ctx_mt = { __index = default_ssl_ctx };
+local default_ssl_ctx_in_mt = { __index = default_ssl_ctx_in };
- if not ssl then return nil, "LuaSec (required for encryption) was not found"; end
- if not user_ssl_config then return nil, "No SSL/TLS configuration present for "..host; end
-
- local ssl_config = {
- mode = mode;
- protocol = user_ssl_config.protocol or "sslv23";
- key = resolve_path(config_path, user_ssl_config.key);
- password = user_ssl_config.password;
- certificate = resolve_path(config_path, user_ssl_config.certificate);
- capath = resolve_path(config_path, user_ssl_config.capath or default_capath);
- cafile = resolve_path(config_path, user_ssl_config.cafile);
- verify = user_ssl_config.verify or default_verify;
- options = user_ssl_config.options or default_options;
- ciphers = user_ssl_config.ciphers;
- depth = user_ssl_config.depth;
- };
+-- Global SSL options if not overridden per-host
+local default_ssl_config = configmanager.get("*", "core", "ssl");
- local ctx, err = ssl_newcontext(ssl_config);
- if not ctx then
- err = err or "invalid ssl config"
- local file = err:match("^error loading (.-) %(");
- if file then
- if file == "private key" then
- file = ssl_config.key or "your private key";
- elseif file == "certificate" then
- file = ssl_config.certificate or "your certificate file";
- end
- local reason = err:match("%((.+)%)$") or "some reason";
- if reason == "Permission denied" then
- reason = "Check that the permissions allow Prosody to read this file.";
- elseif reason == "No such file or directory" then
- reason = "Check that the path is correct, and the file exists.";
- elseif reason == "system lib" then
- reason = "Previous error (see logs), or other system error.";
- elseif reason == "(null)" or not reason then
- reason = "Check that the file exists and the permissions are correct";
+function create_context(host, mode, config)
+ local ssl_config = config and config.core.ssl or default_ssl_config;
+ if ssl and ssl_config then
+ local ctx, err = ssl_newcontext(setmetatable(ssl_config, mode == "client" and default_ssl_ctx_mt or default_ssl_ctx_in_mt));
+ if not ctx then
+ err = err or "invalid ssl config"
+ local file = err:match("^error loading (.-) %(");
+ if file then
+ if file == "private key" then
+ file = ssl_config.key or "your private key";
+ elseif file == "certificate" then
+ file = ssl_config.certificate or "your certificate file";
+ end
+ local reason = err:match("%((.+)%)$") or "some reason";
+ if reason == "Permission denied" then
+ reason = "Check that the permissions allow Prosody to read this file.";
+ elseif reason == "No such file or directory" then
+ reason = "Check that the path is correct, and the file exists.";
+ elseif reason == "system lib" then
+ reason = "Previous error (see logs), or other system error.";
+ elseif reason == "(null)" or not reason then
+ reason = "Check that the file exists and the permissions are correct";
+ else
+ reason = "Reason: "..tostring(reason):lower();
+ end
+ log("error", "SSL/TLS: Failed to load %s: %s", file, reason);
else
- reason = "Reason: "..tostring(reason):lower();
+ log("error", "SSL/TLS: Error initialising for host %s: %s", host, err );
end
- log("error", "SSL/TLS: Failed to load %s: %s", file, reason);
- else
- log("error", "SSL/TLS: Error initialising for host %s: %s", host, err );
- end
+ end
+ return ctx, err;
+ elseif not ssl then
+ return nil, "LuaSec (required for encryption) was not found";
end
- return ctx, err;
+ return nil, "No SSL/TLS configuration present for "..host;
end
function reload_ssl_config()
--- /dev/null
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local prosody = _G.prosody;
+local log = require "util.logger".init("componentmanager");
+local certmanager = require "core.certmanager";
+local configmanager = require "core.configmanager";
+local modulemanager = require "core.modulemanager";
+local jid_split = require "util.jid".split;
+local fire_event = require "core.eventmanager".fire_event;
+local events_new = require "util.events".new;
+local st = require "util.stanza";
+local prosody, hosts = prosody, prosody.hosts;
+local ssl = ssl;
+local uuid_gen = require "util.uuid".generate;
+
+local pairs, setmetatable, type, tostring = pairs, setmetatable, type, tostring;
+
+local components = {};
+
+local disco_items = require "util.multitable".new();
+local NULL = {};
+
+module "componentmanager"
+
+local function default_component_handler(origin, stanza)
+ log("warn", "Stanza being handled by default component; bouncing error for: %s", stanza:top_tag());
+ if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
+ origin.send(st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable"));
+ end
+end
+
+function load_enabled_components(config)
+ local defined_hosts = config or configmanager.getconfig();
+
+ for host, host_config in pairs(defined_hosts) do
+ if host ~= "*" and ((host_config.core.enabled == nil or host_config.core.enabled) and type(host_config.core.component_module) == "string") then
+ hosts[host] = create_component(host);
+ hosts[host].connected = false;
+ components[host] = default_component_handler;
+ local ok, err = modulemanager.load(host, host_config.core.component_module);
+ if not ok then
+ log("error", "Error loading %s component %s: %s", tostring(host_config.core.component_module), tostring(host), tostring(err));
+ else
+ fire_event("component-activated", host, host_config);
+ log("debug", "Activated %s component: %s", host_config.core.component_module, host);
+ end
+ end
+ end
+end
+
+if prosody and prosody.events then
+ prosody.events.add_handler("server-starting", load_enabled_components);
+end
+
+function handle_stanza(origin, stanza)
+ local node, host = jid_split(stanza.attr.to);
+ local component = nil;
+ if host then
+ if node then component = components[node.."@"..host]; end -- hack to allow hooking node@server
+ if not component then component = components[host]; end
+ end
+ if component then
+ log("debug", "%s stanza being handled by component: %s", stanza.name, host);
+ component(origin, stanza, hosts[host]);
+ else
+ log("error", "Component manager recieved a stanza for a non-existing component: "..tostring(stanza));
+ default_component_handler(origin, stanza);
+ end
+end
+
+function create_component(host, component, events)
+ -- TODO check for host well-formedness
+ local ssl_ctx, ssl_ctx_in;
+ if host and ssl then
+ -- We need to find SSL context to use...
+ -- Discussion in prosody@ concluded that
+ -- 1 level back is usually enough by default
+ local base_host = host:gsub("^[^%.]+%.", "");
+ if hosts[base_host] then
+ ssl_ctx = hosts[base_host].ssl_ctx;
+ ssl_ctx_in = hosts[base_host].ssl_ctx_in;
+ else
+ -- We have no cert, and no parent host to borrow a cert from
+ -- Use global/default cert if there is one
+ ssl_ctx = certmanager.create_context(host, "client");
+ ssl_ctx_in = certmanager.create_context(host, "server");
+ end
+ end
+ return { type = "component", host = host, connected = true, s2sout = {},
+ ssl_ctx = ssl_ctx, ssl_ctx_in = ssl_ctx_in, events = events or events_new(),
+ dialback_secret = configmanager.get(host, "core", "dialback_secret") or uuid_gen() };
+end
+
+function register_component(host, component, session)
+ if not hosts[host] or (hosts[host].type == 'component' and not hosts[host].connected) then
+ local old_events = hosts[host] and hosts[host].events;
+
+ components[host] = component;
+ hosts[host] = session or create_component(host, component, old_events);
+
+ -- Add events object if not already one
+ if not hosts[host].events then
+ hosts[host].events = old_events or events_new();
+ end
+
+ if not hosts[host].dialback_secret then
+ hosts[host].dialback_secret = configmanager.get(host, "core", "dialback_secret") or uuid_gen();
+ end
+
+ -- add to disco_items
+ if not(host:find("@", 1, true) or host:find("/", 1, true)) and host:find(".", 1, true) then
+ disco_items:set(host:sub(host:find(".", 1, true)+1), host, true);
+ end
+ modulemanager.load(host, "dialback");
+ modulemanager.load(host, "tls");
+ log("debug", "component added: "..host);
+ return session or hosts[host];
+ else
+ log("error", "Attempt to set component for existing host: "..host);
+ end
+end
+
+function deregister_component(host)
+ if components[host] then
+ modulemanager.unload(host, "tls");
+ modulemanager.unload(host, "dialback");
+ hosts[host].connected = nil;
+ local host_config = configmanager.getconfig()[host];
+ if host_config and ((host_config.core.enabled == nil or host_config.core.enabled) and type(host_config.core.component_module) == "string") then
+ -- Set default handler
+ components[host] = default_component_handler;
+ else
+ -- Component not in config, or disabled, remove
+ hosts[host] = nil; -- FIXME do proper unload of all modules and other cleanup before removing
+ components[host] = nil;
+ end
+ -- remove from disco_items
+ if not(host:find("@", 1, true) or host:find("/", 1, true)) and host:find(".", 1, true) then
+ disco_items:remove(host:sub(host:find(".", 1, true)+1), host);
+ end
+ log("debug", "component removed: "..host);
+ return true;
+ else
+ log("error", "Attempt to remove component for non-existing host: "..host);
+ end
+end
+
+function set_component_handler(host, handler)
+ components[host] = handler;
+end
+
+function get_children(host)
+ return disco_items:get(host) or NULL;
+end
+
+return _M;
-- COPYING file in the source package for more information.
--
+
+
local _G = _G;
-local setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table =
- setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table;
-local format, math_max = string.format, math.max;
+local setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table, format =
+ setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table, string.format;
-local fire_event = prosody and prosody.events.fire_event or function () end;
-local lfs = require "lfs";
-local path_sep = package.config:sub(1,1);
+local eventmanager = require "core.eventmanager";
module "configmanager"
local parsers = {};
-local config_mt = { __index = function (t, k) return rawget(t, "*"); end};
-local config = setmetatable({ ["*"] = { core = {} } }, config_mt);
+local config = { ["*"] = { core = {} } };
+
+local global_config = config["*"];
-- When host not found, use global
-local host_mt = { };
+setmetatable(config, { __index = function () return global_config; end});
+local host_mt = { __index = global_config };
-- When key not found in section, check key in global's section
function section_mt(section_name)
return { __index = function (t, k)
- local section = rawget(config["*"], section_name);
- if not section then return nil; end
- return section[k];
- end
- };
+ local section = rawget(global_config, section_name);
+ if not section then return nil; end
+ return section[k];
+ end };
end
function getconfig()
end
return nil;
end
-function _M.rawget(host, section, key)
- local hostconfig = rawget(config, host);
- if hostconfig then
- local sectionconfig = rawget(hostconfig, section);
- if sectionconfig then
- return rawget(sectionconfig, key);
- end
- end
-end
-local function set(config, host, section, key, value)
+function set(host, section, key, value)
if host and section and key then
local hostconfig = rawget(config, host);
if not hostconfig then
return false;
end
-function _M.set(host, section, key, value)
- return set(config, host, section, key, value);
-end
-
--- Helper function to resolve relative paths (needed by config)
-do
- local rel_path_start = ".."..path_sep;
- function resolve_relative_path(parent_path, path)
- if path then
- -- Some normalization
- parent_path = parent_path:gsub("%"..path_sep.."+$", "");
- path = path:gsub("^%.%"..path_sep.."+", "");
-
- local is_relative;
- if path_sep == "/" and path:sub(1,1) ~= "/" then
- is_relative = true;
- elseif path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\") then
- is_relative = true;
- end
- if is_relative then
- return parent_path..path_sep..path;
- end
- end
- return path;
- end
-end
-
--- Helper function to convert a glob to a Lua pattern
-local function glob_to_pattern(glob)
- return "^"..glob:gsub("[%p*?]", function (c)
- if c == "*" then
- return ".*";
- elseif c == "?" then
- return ".";
- else
- return "%"..c;
- end
- end).."$";
-end
-
function load(filename, format)
format = format or filename:match("%w+$");
if parsers[format] and parsers[format].load then
local f, err = io.open(filename);
if f then
- local new_config = setmetatable({ ["*"] = { core = {} } }, config_mt);
- local ok, err = parsers[format].load(f:read("*a"), filename, new_config);
+ local ok, err = parsers[format].load(f:read("*a"), filename);
f:close();
if ok then
- config = new_config;
- fire_event("config-reloaded", {
- filename = filename,
- format = format,
- config = config
- });
+ eventmanager.fire_event("config-reloaded", { filename = filename, format = format });
end
return ok, "parser", err;
end
local loadstring, pcall, setmetatable = _G.loadstring, _G.pcall, _G.setmetatable;
local setfenv, rawget, tostring = _G.setfenv, _G.rawget, _G.tostring;
parsers.lua = {};
- function parsers.lua.load(data, config_file, config)
+ function parsers.lua.load(data, filename)
local env;
-- The ' = true' are needed so as not to set off __newindex when we assign the functions below
- env = setmetatable({
- Host = true, host = true, VirtualHost = true,
- Component = true, component = true,
- Include = true, include = true, RunScript = true }, {
- __index = function (t, k)
- return rawget(_G, k) or
- function (settings_table)
- config[__currenthost or "*"][k] = settings_table;
- end;
- end,
- __newindex = function (t, k, v)
- set(config, env.__currenthost or "*", "core", k, v);
- end
- });
+ env = setmetatable({ Host = true, host = true, VirtualHost = true, Component = true, component = true,
+ Include = true, include = true, RunScript = dofile }, { __index = function (t, k)
+ return rawget(_G, k) or
+ function (settings_table)
+ config[__currenthost or "*"][k] = settings_table;
+ end;
+ end,
+ __newindex = function (t, k, v)
+ set(env.__currenthost or "*", "core", k, v);
+ end});
rawset(env, "__currenthost", "*") -- Default is global
function env.VirtualHost(name)
end
rawset(env, "__currenthost", name);
-- Needs at least one setting to logically exist :)
- set(config, name or "*", "core", "defined", true);
- return function (config_options)
- rawset(env, "__currenthost", "*"); -- Return to global scope
- for option_name, option_value in pairs(config_options) do
- set(config, name or "*", "core", option_name, option_value);
- end
- end;
+ set(name or "*", "core", "defined", true);
end
env.Host, env.host = env.VirtualHost, env.VirtualHost;
error(format("Component %q clashes with previously defined Host %q, for services use a sub-domain like conference.%s",
name, name, name), 0);
end
- set(config, name, "core", "component_module", "component");
+ set(name, "core", "component_module", "component");
-- Don't load the global modules by default
- set(config, name, "core", "load_global_modules", false);
+ set(name, "core", "load_global_modules", false);
rawset(env, "__currenthost", name);
- local function handle_config_options(config_options)
- rawset(env, "__currenthost", "*"); -- Return to global scope
- for option_name, option_value in pairs(config_options) do
- set(config, name or "*", "core", option_name, option_value);
- end
- end
return function (module)
if type(module) == "string" then
- set(config, name, "core", "component_module", module);
- return handle_config_options;
+ set(name, "core", "component_module", module);
end
- return handle_config_options(module);
end
end
env.component = env.Component;
- function env.Include(file, wildcard)
- if file:match("[*?]") then
- local path_pos, glob = file:match("()([^"..path_sep.."]+)$");
- local path = file:sub(1, math_max(path_pos-2,0));
- local config_path = config_file:gsub("[^"..path_sep.."]+$", "");
- if #path > 0 then
- path = resolve_relative_path(config_path, path);
- else
- path = config_path;
- end
- local patt = glob_to_pattern(glob);
- for f in lfs.dir(path) do
- if f:sub(1,1) ~= "." and f:match(patt) then
- env.Include(path..path_sep..f);
- end
- end
- else
- local f, err = io.open(file);
- if f then
- local data = f:read("*a");
- local file = resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file);
- local ret, err = parsers.lua.load(data, file, config);
- if not ret then error(err:gsub("%[string.-%]", file), 0); end
- end
- if not f then error("Error loading included "..file..": "..err, 0); end
- return f, err;
+ function env.Include(file)
+ local f, err = io.open(file);
+ if f then
+ local data = f:read("*a");
+ local ok, err = parsers.lua.load(data, file);
+ if not ok then error(err:gsub("%[string.-%]", file), 0); end
end
+ if not f then error("Error loading included "..file..": "..err, 0); end
+ return f, err;
end
env.include = env.Include;
- function env.RunScript(file)
- return dofile(resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file));
- end
-
- local chunk, err = loadstring(data, "@"..config_file);
+ local chunk, err = loadstring(data, "@"..filename);
if not chunk then
return nil, err;
-- COPYING file in the source package for more information.
--
+local ssl = ssl
+
+local hosts = hosts;
+local certmanager = require "core.certmanager";
local configmanager = require "core.configmanager";
+local eventmanager = require "core.eventmanager";
local modulemanager = require "core.modulemanager";
local events_new = require "util.events".new;
-local disco_items = require "util.multitable".new();
-local NULL = {};
local uuid_gen = require "util.uuid".generate;
-local log = require "util.logger".init("hostmanager");
-
-local hosts = hosts;
-local prosody_events = prosody.events;
if not _G.prosody.incoming_s2s then
require "core.s2smanager";
end
local incoming_s2s = _G.prosody.incoming_s2s;
+local log = require "util.logger".init("hostmanager");
+
local pairs, setmetatable = pairs, setmetatable;
-local tostring, type = tostring, type;
module "hostmanager"
local activated_any_host;
for host, host_config in pairs(defined_hosts) do
- if host ~= "*" and host_config.core.enabled ~= false then
- if not host_config.core.component_module then
- activated_any_host = true;
- end
+ if host ~= "*" and host_config.core.enabled ~= false and not host_config.core.component_module then
+ activated_any_host = true;
activate(host, host_config);
end
end
log("error", "No active VirtualHost entries in the config file. This may cause unexpected behaviour as no modules will be loaded.");
end
- prosody_events.fire_event("hosts-activated", defined_hosts);
+ eventmanager.fire_event("hosts-activated", defined_hosts);
hosts_loaded_once = true;
end
-prosody_events.add_handler("server-starting", load_enabled_hosts);
+eventmanager.add_event_hook("server-starting", load_enabled_hosts);
function activate(host, host_config)
- if hosts[host] then return nil, "The host "..host.." is already activated"; end
- host_config = host_config or configmanager.getconfig()[host];
- if not host_config then return nil, "Couldn't find the host "..tostring(host).." defined in the current config"; end
- local host_session = {
- host = host;
- s2sout = {};
- events = events_new();
- dialback_secret = configmanager.get(host, "core", "dialback_secret") or uuid_gen();
- disallow_s2s = configmanager.get(host, "core", "disallow_s2s");
- };
- if not host_config.core.component_module then -- host
- host_session.type = "local";
- host_session.sessions = {};
- else -- component
- host_session.type = "component";
- end
- hosts[host] = host_session;
- if not host:match("[@/]") then
- disco_items:set(host:match("%.(.*)") or "*", host, true);
- end
+ hosts[host] = {type = "local", connected = true, sessions = {},
+ host = host, s2sout = {}, events = events_new(),
+ disallow_s2s = configmanager.get(host, "core", "disallow_s2s")
+ or (configmanager.get(host, "core", "anonymous_login")
+ and (configmanager.get(host, "core", "disallow_s2s") ~= false));
+ dialback_secret = configmanager.get(host, "core", "dialback_secret") or uuid_gen();
+ };
for option_name in pairs(host_config.core) do
if option_name:match("_ports$") or option_name:match("_interface$") then
log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in the server-wide section instead", host, option_name);
end
end
+ hosts[host].ssl_ctx = certmanager.create_context(host, "client", host_config); -- for outgoing connections
+ hosts[host].ssl_ctx_in = certmanager.create_context(host, "server", host_config); -- for incoming connections
+
log((hosts_loaded_once and "info") or "debug", "Activated host: %s", host);
- prosody_events.fire_event("host-activated", host, host_config);
- return true;
+ eventmanager.fire_event("host-activated", host, host_config);
end
function deactivate(host, reason)
local host_session = hosts[host];
- if not host_session then return nil, "The host "..tostring(host).." is not activated"; end
log("info", "Deactivating host: %s", host);
- prosody_events.fire_event("host-deactivating", host, host_session);
+ eventmanager.fire_event("host-deactivating", host, host_session);
- if type(reason) ~= "table" then
- reason = { condition = "host-gone", text = tostring(reason or "This server has stopped serving "..host) };
- end
+ reason = reason or { condition = "host-gone", text = "This server has stopped serving "..host };
-- Disconnect local users, s2s connections
if host_session.sessions then
end
hosts[host] = nil;
- if not host:match("[@/]") then
- disco_items:remove(host:match("%.(.*)") or "*", host);
- end
- prosody_events.fire_event("host-deactivated", host);
+ eventmanager.fire_event("host-deactivated", host);
log("info", "Deactivated host: %s", host);
- return true;
end
-function get_children(host)
- return disco_items:get(host) or NULL;
+function getconfig(name)
end
return _M;
local format, rep = string.format, string.rep;
local pcall = pcall;
local debug = debug;
-local tostring, setmetatable, rawset, pairs, ipairs, type =
+local tostring, setmetatable, rawset, pairs, ipairs, type =
tostring, setmetatable, rawset, pairs, ipairs, type;
local io_open, io_write = io.open, io.write;
local math_max, rep = math.max, string.rep;
local os_date, os_getenv = os.date, os.getenv;
-local getstyle, setstyle = require "util.termcolours".getstyle, require "util.termcolours".setstyle;
+local getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring;
if os.getenv("__FLUSH_LOG") then
local io_flush = io.flush;
end
local config = require "core.configmanager";
+local eventmanager = require "core.eventmanager";
local logger = require "util.logger";
-local prosody = prosody;
+local debug_mode = config.get("*", "core", "debug");
_G.log = logger.init("general");
module "loggingmanager"
--- The log config used if none specified in the config file (see reload_logging for initialization)
-local default_logging;
-local default_file_logging;
+-- The log config used if none specified in the config file
+local default_logging = { { to = "console" , levels = { min = (debug_mode and "debug") or "info" } } };
+local default_file_logging = { { to = "file", levels = { min = (debug_mode and "debug") or "info" }, timestamps = true } };
local default_timestamp = "%b %d %H:%M:%S";
-- The actual config loggingmanager is using
-local logging_config;
+local logging_config = config.get("*", "core", "log") or default_logging;
local apply_sink_rules;
local log_sink_types = setmetatable({}, { __newindex = function (t, k, v) rawset(t, k, v); apply_sink_rules(k); end; });
-- the log_sink_types table.
function apply_sink_rules(sink_type)
if type(logging_config) == "table" then
-
- for _, level in ipairs(logging_levels) do
- if type(logging_config[level]) == "string" then
- local value = logging_config[level];
- if sink_type == "file" then
- add_rule({
- to = sink_type;
- filename = value;
- timestamps = true;
- levels = { min = level };
- });
- elseif value == "*"..sink_type then
- add_rule({
- to = sink_type;
- levels = { min = level };
- });
- end
- end
- end
-
- for _, sink_config in ipairs(logging_config) do
- if (type(sink_config) == "table" and sink_config.to == sink_type) then
+ for _, sink_config in pairs(logging_config) do
+ if sink_config.to == sink_type then
add_rule(sink_config);
- elseif (type(sink_config) == "string" and sink_config:match("^%*(.+)") == sink_type) then
- add_rule({ levels = { min = "debug" }, to = sink_type });
end
end
elseif type(logging_config) == "string" and (not logging_config:match("^%*")) and sink_type == "file" then
return set;
end
--- Initialize config, etc. --
-function reload_logging()
- local old_sink_types = {};
-
- for name, sink_maker in pairs(log_sink_types) do
- old_sink_types[name] = sink_maker;
- log_sink_types[name] = nil;
- end
-
- logger.reset();
-
- local debug_mode = config.get("*", "core", "debug");
-
- default_logging = { { to = "console" , levels = { min = (debug_mode and "debug") or "info" } } };
- default_file_logging = {
- { to = "file", levels = { min = (debug_mode and "debug") or "info" }, timestamps = true }
- };
- default_timestamp = "%b %d %H:%M:%S";
-
- logging_config = config.get("*", "core", "log") or default_logging;
-
-
- for name, sink_maker in pairs(old_sink_types) do
- log_sink_types[name] = sink_maker;
- end
-
- prosody.events.fire_event("logging-reloaded");
-end
-
-reload_logging();
-prosody.events.add_handler("config-reloaded", reload_logging);
-
--- Definition of built-in logging sinks ---
-- Null sink, must enter log_sink_types *first*
-- Column width for "source" (used by stdout and console)
local sourcewidth = 20;
-function log_sink_types.stdout(config)
+function log_sink_types.stdout()
local timestamps = config.timestamps;
if timestamps == true then
end
do
- local do_pretty_printing = true;
+ local do_pretty_printing = not os_getenv("WINDIR");
local logstyles = {};
if do_pretty_printing then
if timestamps then
io_write(os_date(timestamps), " ");
end
- io_write(name, rep(" ", sourcewidth-namelen));
- setstyle(logstyles[level]);
- io_write(level);
- setstyle();
if ... then
- io_write("\t", format(message, ...), "\n");
+ io_write(name, rep(" ", sourcewidth-namelen), getstring(logstyles[level], level), "\t", format(message, ...), "\n");
else
- io_write("\t", message, "\n");
+ io_write(name, rep(" ", sourcewidth-namelen), getstring(logstyles[level], level), "\t", message, "\n");
end
end
end
end
local write, flush = logfile.write, logfile.flush;
+ eventmanager.add_event_hook("reopen-log-files", function ()
+ if logfile then
+ logfile:close();
+ end
+ logfile = io_open(log, "a+");
+ if not logfile then
+ write, flush = empty_function, empty_function;
+ else
+ write, flush = logfile.write, logfile.flush;
+ end
+ end);
+
local timestamps = config.timestamps;
if timestamps == nil or timestamps == true then
-- COPYING file in the source package for more information.
--
+local plugin_dir = CFG_PLUGINDIR or "./plugins/";
+
local logger = require "util.logger";
local log = logger.init("modulemanager");
+local eventmanager = require "core.eventmanager";
local config = require "core.configmanager";
local multitable_new = require "util.multitable".new;
local st = require "util.stanza";
local hosts = hosts;
local prosody = prosody;
-local prosody_events = prosody.events;
local loadfile, pcall, xpcall = loadfile, pcall, xpcall;
local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv;
pcall = function(f, ...)
local n = select("#", ...);
local params = {...};
- return xpcall(function() return f(unpack(params, 1, n)) end, function(e) return tostring(e).."\n"..debug_traceback(); end);
+ return xpcall(function() f(unpack(params, 1, n)) end, function(e) return tostring(e).."\n"..debug_traceback(); end);
end
local array, set = require "util.array", require "util.set";
-local autoload_modules = {"presence", "message", "iq", "offline"};
-local component_inheritable_modules = {"tls", "dialback", "iq"};
+local autoload_modules = {"presence", "message", "iq"};
-- We need this to let modules access the real global namespace
local _G = _G;
local modulemap = { ["*"] = {} };
+local stanza_handlers = multitable_new();
+local handler_info = {};
+
local modulehelpers = setmetatable({}, { __index = _G });
+local handler_table = multitable_new();
+local hooked = multitable_new();
local hooks = multitable_new();
+local event_hooks = multitable_new();
local NULL = {};
-- Load modules when a host is activated
function load_modules_for_host(host)
- local component = config.get(host, "core", "component_module");
-
- local global_modules_enabled = config.get("*", "core", "modules_enabled");
- local global_modules_disabled = config.get("*", "core", "modules_disabled");
- local host_modules_enabled = config.get(host, "core", "modules_enabled");
- local host_modules_disabled = config.get(host, "core", "modules_disabled");
-
- if host_modules_enabled == global_modules_enabled then host_modules_enabled = nil; end
- if host_modules_disabled == global_modules_disabled then host_modules_disabled = nil; end
-
- local global_modules = set.new(autoload_modules) + set.new(global_modules_enabled) - set.new(global_modules_disabled);
- if component then
- global_modules = set.intersection(set.new(component_inheritable_modules), global_modules);
+ local disabled_set = {};
+ local modules_disabled = config.get(host, "core", "modules_disabled");
+ if modules_disabled then
+ for _, module in ipairs(modules_disabled) do
+ disabled_set[module] = true;
+ end
end
- local modules = (global_modules + set.new(host_modules_enabled)) - set.new(host_modules_disabled);
-
- -- COMPAT w/ pre 0.8
- if modules:contains("console") then
- log("error", "The mod_console plugin has been renamed to mod_admin_telnet. Please update your config.");
- modules:remove("console");
- modules:add("admin_telnet");
+
+ -- Load auto-loaded modules for this host
+ if hosts[host].type == "local" then
+ for _, module in ipairs(autoload_modules) do
+ if not disabled_set[module] then
+ load(host, module);
+ end
+ end
end
-
- if component then
- load(host, component);
+
+ -- Load modules from global section
+ if config.get(host, "core", "load_global_modules") ~= false then
+ local modules_enabled = config.get("*", "core", "modules_enabled");
+ if modules_enabled then
+ for _, module in ipairs(modules_enabled) do
+ if not disabled_set[module] and not is_loaded(host, module) then
+ load(host, module);
+ end
+ end
+ end
end
- for module in modules do
- load(host, module);
+
+ -- Load modules from just this host
+ local modules_enabled = config.get(host, "core", "modules_enabled");
+ if modules_enabled and modules_enabled ~= config.get("*", "core", "modules_enabled") then
+ for _, module in pairs(modules_enabled) do
+ if not is_loaded(host, module) then
+ load(host, module);
+ end
+ end
end
end
-prosody_events.add_handler("host-activated", load_modules_for_host);
+eventmanager.add_event_hook("host-activated", load_modules_for_host);
+eventmanager.add_event_hook("component-activated", load_modules_for_host);
--
function load(host, module_name, config)
if not (host and module_name) then
return nil, "insufficient-parameters";
- elseif not hosts[host] then
- return nil, "unknown-host";
end
if not modulemap[host] then
end
local _log = logger.init(host..":"..module_name);
- local api_instance = setmetatable({ name = module_name, host = host, path = err, config = config, _log = _log, log = function (self, ...) return _log(...); end }, { __index = api });
+ local api_instance = setmetatable({ name = module_name, host = host, config = config, _log = _log, log = function (self, ...) return _log(...); end }, { __index = api });
local pluginenv = setmetatable({ module = api_instance }, { __index = _G });
api_instance.environment = pluginenv;
setfenv(mod, pluginenv);
+ if not hosts[host] then
+ local create_component = _G.require "core.componentmanager".create_component;
+ hosts[host] = create_component(host);
+ hosts[host].connected = false;
+ log("debug", "Created new component: %s", host);
+ end
hosts[host].modules = modulemap[host];
modulemap[host][module_name] = pluginenv;
log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
end
end
+ local params = handler_table:get(host, name); -- , {module.host, origin_type, tag, xmlns}
+ for _, param in pairs(params or NULL) do
+ local handlers = stanza_handlers:get(param[1], param[2], param[3], param[4]);
+ if handlers then
+ handler_info[handlers[1]] = nil;
+ stanza_handlers:remove(param[1], param[2], param[3], param[4]);
+ end
+ end
+ event_hooks:remove(host, name);
-- unhook event handlers hooked by module:hook
for event, handlers in pairs(hooks:get(host, name) or NULL) do
for handler in pairs(handlers or NULL) do
return ok, err;
end
+function handle_stanza(host, origin, stanza)
+ local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns or "jabber:client", origin.type;
+ if name == "iq" and xmlns == "jabber:client" then
+ if stanza.attr.type == "get" or stanza.attr.type == "set" then
+ xmlns = stanza.tags[1].attr.xmlns or "jabber:client";
+ log("debug", "Stanza of type %s from %s has xmlns: %s", name, origin_type, xmlns);
+ else
+ log("debug", "Discarding %s from %s of type: %s", name, origin_type, stanza.attr.type);
+ return true;
+ end
+ end
+ local handlers = stanza_handlers:get(host, origin_type, name, xmlns);
+ if not handlers then handlers = stanza_handlers:get("*", origin_type, name, xmlns); end
+ if handlers then
+ log("debug", "Passing stanza to mod_%s", handler_info[handlers[1]].name);
+ (handlers[1])(origin, stanza);
+ return true;
+ else
+ if stanza.attr.xmlns == nil then
+ log("debug", "Unhandled %s stanza: %s; xmlns=%s", origin.type, stanza.name, xmlns); -- we didn't handle it
+ if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end
+ elseif not((name == "features" or name == "error") and xmlns == "http://etherx.jabber.org/streams") then -- FIXME remove check once we handle S2S features
+ log("warn", "Unhandled %s stream element: %s; xmlns=%s: %s", origin.type, stanza.name, xmlns, tostring(stanza)); -- we didn't handle it
+ origin:close("unsupported-stanza-type");
+ end
+ end
+end
+
function module_has_method(module, method)
return type(module.module[method]) == "function";
end
self._log = _log;
end
+local function _add_handler(module, origin_type, tag, xmlns, handler)
+ local handlers = stanza_handlers:get(module.host, origin_type, tag, xmlns);
+ local msg = (tag == "iq") and "namespace" or "payload namespace";
+ if not handlers then
+ stanza_handlers:add(module.host, origin_type, tag, xmlns, handler);
+ handler_info[handler] = module;
+ handler_table:add(module.host, module.name, {module.host, origin_type, tag, xmlns});
+ --module:log("debug", "I now handle tag '%s' [%s] with %s '%s'", tag, origin_type, msg, xmlns);
+ else
+ module:log("warn", "I wanted to handle tag '%s' [%s] with %s '%s' but mod_%s already handles that", tag, origin_type, msg, xmlns, handler_info[handlers[1]].module.name);
+ end
+end
+
+function api:add_handler(origin_type, tag, xmlns, handler)
+ if not (origin_type and tag and xmlns and handler) then return false; end
+ if type(origin_type) == "table" then
+ for _, origin_type in ipairs(origin_type) do
+ _add_handler(self, origin_type, tag, xmlns, handler);
+ end
+ else
+ _add_handler(self, origin_type, tag, xmlns, handler);
+ end
+end
+function api:add_iq_handler(origin_type, xmlns, handler)
+ self:add_handler(origin_type, "iq", xmlns, handler);
+end
+
function api:add_feature(xmlns)
self:add_item("feature", xmlns);
end
self:add_item("identity", {category = category, type = type, name = name});
end
+local event_hook = function(host, mod_name, event_name, ...)
+ if type((...)) == "table" and (...).host and (...).host ~= host then return; end
+ for handler in pairs(event_hooks:get(host, mod_name, event_name) or NULL) do
+ handler(...);
+ end
+end;
+function api:add_event_hook(name, handler)
+ if not hooked:get(self.host, self.name, name) then
+ eventmanager.add_event_hook(name, function(...) event_hook(self.host, self.name, name, ...); end);
+ hooked:set(self.host, self.name, name, true);
+ end
+ event_hooks:set(self.host, self.name, name, handler, true);
+end
+
function api:fire_event(...)
return (hosts[self.host] or prosody).events.fire_event(...);
end
end
end
-local function _get_online_roster_subscription(jidA, jidB)
- local user = bare_sessions[jidA];
- local item = user and (user.roster[jidB] or { subscription = "none" });
- return item and item.subscription;
-end
function is_contact_subscribed(username, host, jid)
- do
- local selfjid = username.."@"..host;
- local subscription = _get_online_roster_subscription(selfjid, jid);
- if subscription then return (subscription == "both" or subscription == "from"); end
- local subscription = _get_online_roster_subscription(jid, selfjid);
- if subscription then return (subscription == "both" or subscription == "to"); end
- end
local roster, err = load_roster(username, host);
local item = roster[jid];
return item and (item.subscription == "from" or item.subscription == "both"), err;
local format = string.format;
local t_insert, t_sort = table.insert, table.sort;
local get_traceback = debug.traceback;
-local tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber, setmetatable
- = tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber, setmetatable;
+local tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber,
+ setmetatable
+ = tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber,
+ setmetatable;
local idna_to_ascii = require "util.encodings".idna.to_ascii;
local connlisteners_get = require "net.connlisteners".get;
-local initialize_filters = require "util.filters".initialize;
local wrapclient = require "net.server".wrapclient;
local modulemanager = require "core.modulemanager";
local st = require "stanza";
local stanza = st.stanza;
local nameprep = require "util.encodings".stringprep.nameprep;
-local cert_verify_identity = require "util.x509".verify_identity;
-local fire_event = prosody.events.fire_event;
+local fire_event = require "core.eventmanager".fire_event;
local uuid_gen = require "util.uuid".generate;
local logger_init = require "util.logger".init;
local adns, dns = require "net.adns", require "net.dns";
local config = require "core.configmanager";
local connect_timeout = config.get("*", "core", "s2s_timeout") or 60;
-local dns_timeout = config.get("*", "core", "dns_timeout") or 15;
+local dns_timeout = config.get("*", "core", "dns_timeout") or 60;
local max_dns_depth = config.get("*", "core", "dns_max_depth") or 3;
-dns.settimeout(dns_timeout);
-
-local prosody = _G.prosody;
incoming_s2s = {};
-prosody.incoming_s2s = incoming_s2s;
+_G.prosody.incoming_s2s = incoming_s2s;
local incoming_s2s = incoming_s2s;
module "s2smanager"
return a.priority < b.priority or (a.priority == b.priority and a.weight > b.weight);
end
-local bouncy_stanzas = { message = true, presence = true, iq = true };
local function bounce_sendq(session, reason)
local sendq = session.sendq;
if sendq then
};
for i, data in ipairs(sendq) do
local reply = data[2];
- if reply and not(reply.attr.xmlns) and bouncy_stanzas[reply.name] then
+ local xmlns = reply.attr.xmlns;
+ if not xmlns then
reply.attr.type = "error";
reply:tag("error", {type = "cancel"})
:tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
if reason then
- reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})
- :text("Server-to-server connection failed: "..reason):up();
+ reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):text("Connection failed: "..reason):up();
end
core_process_stanza(dummy, reply);
end
(host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host);
-- Queue stanza until we are able to send it
- if host.sendq then t_insert(host.sendq, {tostring(data), data.attr.type ~= "error" and data.attr.type ~= "result" and st.reply(data)});
- else host.sendq = { {tostring(data), data.attr.type ~= "error" and data.attr.type ~= "result" and st.reply(data)} }; end
+ if host.sendq then t_insert(host.sendq, {tostring(data), st.reply(data)});
+ else host.sendq = { {tostring(data), st.reply(data)} }; end
host.log("debug", "stanza [%s] queued ", data.name);
elseif host.type == "local" or host.type == "component" then
log("error", "Trying to send a stanza to ourselves??")
log("error", "Traceback: %s", get_traceback());
log("error", "Stanza: %s", tostring(data));
- return false;
else
(host.log or log)("debug", "going to send stanza to "..to_host.." from "..from_host);
-- FIXME
local host_session = new_outgoing(from_host, to_host);
-- Store in buffer
- host_session.sendq = { {tostring(data), data.attr.type ~= "error" and data.attr.type ~= "result" and st.reply(data)} };
+ host_session.sendq = { {tostring(data), st.reply(data)} };
log("debug", "stanza [%s] queued until connection complete", tostring(data.name));
if (not host_session.connecting) and (not host_session.conn) then
log("warn", "Connection to %s failed already, destroying session...", to_host);
- if not destroy_session(host_session, "Connection failed") then
- -- Already destroyed, we need to bounce our stanza
- bounce_sendq(host_session, host_session.destruction_reason);
- end
- return false;
+ destroy_session(host_session);
end
end
- return true;
end
local open_sessions = 0;
open_sessions = open_sessions + 1;
local w, log = conn.write, logger_init("s2sin"..tostring(conn):match("[a-f0-9]+$"));
session.log = log;
- local filter = initialize_filters(session);
- session.sends2s = function (t)
- log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)"));
- if t.name then
- t = filter("stanzas/out", t);
- end
- if t then
- t = filter("bytes/out", tostring(t));
- if t then
- return w(conn, t);
- end
- end
- end
+ session.sends2s = function (t) log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)")); w(conn, tostring(t)); end
incoming_s2s[session] = true;
add_task(connect_timeout, function ()
if session.conn ~= conn or
return; -- Ok, we're connect[ed|ing]
end
-- Not connected, need to close session and clean up
- (session.log or log)("debug", "Destroying incomplete session %s->%s due to inactivity",
+ (session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity",
session.from_host or "(unknown)", session.to_host or "(unknown)");
session:close("connection-timeout");
end);
hosts[from_host].s2sout[to_host] = host_session;
- host_session.close = destroy_session; -- This gets replaced by xmppserver_listener later
-
local log;
do
local conn_name = "s2sout"..tostring(host_session):match("[a-f0-9]*$");
host_session.log = log;
end
- initialize_filters(host_session);
-
if connect ~= false then
-- Kick the connection attempting machine into life
- if not attempt_connection(host_session) then
- -- Intentionally not returning here, the
- -- session is needed, connected or not
- destroy_session(host_session);
- end
+ attempt_connection(host_session);
end
if not host_session.sends2s then
end
end, "_xmpp-server._tcp."..connect_host..".", "SRV");
+ -- Set handler for DNS timeout
+ add_task(dns_timeout, function ()
+ if handle then
+ adns.cancel(handle, true);
+ end
+ end);
+
return true; -- Attempt in progress
elseif host_session.srv_hosts and #host_session.srv_hosts > host_session.srv_choice then -- Not our first attempt, and we also have SRV
host_session.srv_choice = host_session.srv_choice + 1;
function try_connect(host_session, connect_host, connect_port)
host_session.connecting = true;
local handle;
- handle = adns.lookup(function (reply, err)
+ handle = adns.lookup(function (reply)
handle = nil;
host_session.connecting = nil;
if reply and reply[#reply] and reply[#reply].a then
log("debug", "DNS reply for %s gives us %s", connect_host, reply[#reply].a);
- local ok, err = make_connect(host_session, reply[#reply].a, connect_port);
- if not ok then
- if not attempt_connection(host_session, err or "closed") then
- err = err and (": "..err) or "";
- destroy_session(host_session, "Connection failed"..err);
- end
- end
+ return make_connect(host_session, reply[#reply].a, connect_port);
else
log("debug", "DNS lookup failed to get a response for %s", connect_host);
if not attempt_connection(host_session, "name resolution failed") then -- Retry if we can
log("debug", "No other records to try for %s - destroying", host_session.to_host);
- err = err and (": "..err) or "";
- destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't
+ destroy_session(host_session, "DNS resolution failed"); -- End of the line, we can't
end
end
end, connect_host, "A", "IN");
+ -- Set handler for DNS timeout
+ add_task(dns_timeout, function ()
+ if handle then
+ adns.cancel(handle, true);
+ end
+ end);
+
return true;
end
local from_host, to_host = host_session.from_host, host_session.to_host;
- local conn, handler = socket.tcp();
+ local conn, handler = socket.tcp()
if not conn then
log("warn", "Failed to create outgoing connection, system error: %s", handler);
conn = wrapclient(conn, connect_host, connect_port, cl, cl.default_mode or 1 );
host_session.conn = conn;
- local filter = initialize_filters(host_session);
- local w, log = conn.write, host_session.log;
- host_session.sends2s = function (t)
- log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
- if t.name then
- t = filter("stanzas/out", t);
- end
- if t then
- t = filter("bytes/out", tostring(t));
- if t then
- return w(conn, tostring(t));
- end
- end
- end
-
-- Register this outgoing connection so that xmppserver_listener knows about it
-- otherwise it will assume it is a new incoming connection
cl.register_outgoing(conn, host_session);
+ local w, log = conn.write, host_session.log;
+ host_session.sends2s = function (t) log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?")); w(conn, tostring(t)); end
+
host_session:open_stream(from_host, to_host);
log("debug", "Connection attempt in progress...");
from=from, to=to, version='1.0', ["xml:lang"]='en'}):top_tag());
end
-local function check_cert_status(session)
- local conn = session.conn:socket()
- local cert
- if conn.getpeercertificate then
- cert = conn:getpeercertificate()
- end
-
- if cert then
- local chain_valid, err = conn:getpeerchainvalid()
- if not chain_valid then
- session.cert_chain_status = "invalid";
- (session.log or log)("debug", "certificate chain validation result: %s", err);
- else
- session.cert_chain_status = "valid";
-
- local host = session.direction == "incoming" and session.from_host or session.to_host
-
- -- We'll go ahead and verify the asserted identity if the
- -- connecting server specified one.
- if host then
- if cert_verify_identity(host, "xmpp-server", cert) then
- session.cert_identity_status = "valid"
- else
- session.cert_identity_status = "invalid"
- end
- end
- end
- end
-end
-
function streamopened(session, attr)
local send = session.sends2s;
-- TODO: #29: SASL/TLS on s2s streams
session.version = tonumber(attr.version) or 0;
- -- TODO: Rename session.secure to session.encrypted
if session.secure == false then
session.secure = true;
end
-
+
if session.direction == "incoming" then
-- Send a reply stream header
session.to_host = attr.to and nameprep(attr.to);
session.streamid = uuid_gen();
(session.log or log)("debug", "incoming s2s received <stream:stream>");
- if session.to_host then
- if not hosts[session.to_host] then
- -- Attempting to connect to a host we don't serve
- session:close({
- condition = "host-unknown";
- text = "This host does not serve "..session.to_host
- });
- return;
- elseif hosts[session.to_host].disallow_s2s then
- -- Attempting to connect to a host that disallows s2s
- session:close({
- condition = "policy-violation";
- text = "Server-to-server communication is not allowed to this host";
- });
- return;
- end
+ if session.to_host and not hosts[session.to_host] then
+ -- Attempting to connect to a host we don't serve
+ session:close({ condition = "host-unknown"; text = "This host does not serve "..session.to_host });
+ return;
end
-
- if session.secure and not session.cert_chain_status then check_cert_status(session); end
-
send("<?xml version='1.0'?>");
send(stanza("stream:stream", { xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.to_host, to=session.from_host, version=(session.version > 0 and "1.0" or nil) }):top_tag());
-- If we are just using the connection for verifying dialback keys, we won't try and auth it
if not attr.id then error("stream response did not give us a streamid!!!"); end
session.streamid = attr.id;
-
- if session.secure and not session.cert_chain_status then check_cert_status(session); end
-
+
-- Send unauthed buffer
-- (stanzas which are fine to send before dialback)
-- Note that this is *not* the stanza queue (which
elseif session.type == "s2sin_unauthed" then
session.type = "s2sin";
if host then
- if not session.hosts[host] then session.hosts[host] = {}; end
session.hosts[host].authed = true;
end
elseif session.type == "s2sin" and host then
- if not session.hosts[host] then session.hosts[host] = {}; end
session.hosts[host].authed = true;
else
return false;
session.log("info", session.direction.." s2s connection "..from.."->"..to.." complete");
local send_to_host = send_to_host;
- function session.send(data) return send_to_host(to, from, data); end
+ function session.send(data) send_to_host(to, from, data); end
- local event_data = { session = session };
- if session.type == "s2sout" then
- prosody.events.fire_event("s2sout-established", event_data);
- hosts[session.from_host].events.fire_event("s2sout-established", event_data);
- else
- prosody.events.fire_event("s2sin-established", event_data);
- hosts[session.to_host].events.fire_event("s2sin-established", event_data);
- end
if session.direction == "outgoing" then
if sendq then
close = function (session)
session.log("debug", "Attempt to close already-closed session");
end;
- filter = function (type, data) return data; end;
}; resting_session.__index = resting_session;
-function retire_session(session, reason)
+function retire_session(session)
local log = session.log or log;
for k in pairs(session) do
if k ~= "trace" and k ~= "log" and k ~= "id" then
end
end
- session.destruction_reason = reason;
-
function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); end
function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end
return setmetatable(session, resting_session);
function destroy_session(session, reason)
if session.destroyed then return; end
- (session.log or log)("debug", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host)..(reason and (": "..reason) or ""));
+ (session.log or log)("info", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host));
if session.direction == "outgoing" then
hosts[session.from_host].s2sout[session.to_host] = nil;
incoming_s2s[session] = nil;
end
- local event_data = { session = session, reason = reason };
- if session.type == "s2sout" then
- prosody.events.fire_event("s2sout-destroyed", event_data);
- if hosts[session.from_host] then
- hosts[session.from_host].events.fire_event("s2sout-destroyed", event_data);
- end
- elseif session.type == "s2sin" then
- prosody.events.fire_event("s2sin-destroyed", event_data);
- if hosts[session.to_host] then
- hosts[session.to_host].events.fire_event("s2sin-destroyed", event_data);
- end
- end
-
- retire_session(session, reason); -- Clean session until it is GC'd
- return true;
+ retire_session(session); -- Clean session until it is GC'd
end
return _M;
local resourceprep = require "util.encodings".stringprep.resourceprep;
local nodeprep = require "util.encodings".stringprep.nodeprep;
-local initialize_filters = require "util.filters".initialize;
-local fire_event = prosody.events.fire_event;
+local fire_event = require "core.eventmanager".fire_event;
local add_task = require "util.timer".add_task;
local gettime = require "socket".gettime;
end
open_sessions = open_sessions + 1;
log("debug", "open sessions now: ".. open_sessions);
-
- local filter = initialize_filters(session);
local w = conn.write;
- session.send = function (t)
- if t.name then
- t = filter("stanzas/out", t);
- end
- if t then
- t = filter("bytes/out", tostring(t));
- if t then
- return w(conn, t);
- end
- end
- end
+ session.send = function (t) w(conn, tostring(t)); end
session.ip = conn:ip();
local conn_name = "c2s"..tostring(conn):match("[a-f0-9]+$");
session.log = logger.init(conn_name);
close = function (session)
session.log("debug", "Attempt to close already-closed session");
end;
- filter = function (type, data) return data; end;
}; resting_session.__index = resting_session;
function retire_session(session)
end
function destroy_session(session, err)
- (session.log or log)("info", "Destroying session for %s (%s@%s)%s", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)", err and (": "..err) or "");
+ (session.log or log)("info", "Destroying session for %s (%s@%s)", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)");
if session.destroyed then return; end
-- Remove session/resource from user's session list
if session.full_jid then
- local host_session = hosts[session.host];
-
- -- Allow plugins to prevent session destruction
- if host_session.events.fire_event("pre-resource-unbind", {session=session, error=err}) then
- return;
- end
-
- host_session.sessions[session.username].sessions[session.resource] = nil;
+ hosts[session.host].sessions[session.username].sessions[session.resource] = nil;
full_sessions[session.full_jid] = nil;
- if not next(host_session.sessions[session.username].sessions) then
+ if not next(hosts[session.host].sessions[session.username].sessions) then
log("debug", "All resources of %s are now offline", session.username);
- host_session.sessions[session.username] = nil;
+ hosts[session.host].sessions[session.username] = nil;
bare_sessions[session.username..'@'..session.host] = nil;
end
- host_session.events.fire_event("resource-unbind", {session=session, error=err});
+ hosts[session.host].events.fire_event("resource-unbind", {session=session, error=err});
end
retire_session(session);
local tostring = tostring;
local st = require "util.stanza";
local send_s2s = require "core.s2smanager".send_to_host;
+local modules_handle_stanza = require "core.modulemanager".handle_stanza;
+local component_handle_stanza = require "core.componentmanager".handle_stanza;
local jid_split = require "util.jid".split;
local jid_prepped_split = require "util.jid".prepped_split;
local full_sessions = _G.prosody.full_sessions;
local bare_sessions = _G.prosody.bare_sessions;
-local function handle_unhandled_stanza(host, origin, stanza)
- local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns or "jabber:client", origin.type;
- if name == "iq" and xmlns == "jabber:client" then
- if stanza.attr.type == "get" or stanza.attr.type == "set" then
- xmlns = stanza.tags[1].attr.xmlns or "jabber:client";
- log("debug", "Stanza of type %s from %s has xmlns: %s", name, origin_type, xmlns);
- else
- log("debug", "Discarding %s from %s of type: %s", name, origin_type, stanza.attr.type);
- return true;
- end
- end
- if stanza.attr.xmlns == nil then
- log("debug", "Unhandled %s stanza: %s; xmlns=%s", origin.type, stanza.name, xmlns); -- we didn't handle it
- if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
- origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
- end
- elseif not((name == "features" or name == "error") and xmlns == "http://etherx.jabber.org/streams") then -- FIXME remove check once we handle S2S features
- log("warn", "Unhandled %s stream element: %s; xmlns=%s: %s", origin.type, stanza.name, xmlns, tostring(stanza)); -- we didn't handle it
- origin:close("unsupported-stanza-type");
- end
-end
-
-local iq_types = { set=true, get=true, result=true, error=true };
function core_process_stanza(origin, stanza)
(origin.log or log)("debug", "Received[%s]: %s", origin.type, stanza:top_tag())
if stanza.attr.type == "error" and #stanza.tags == 0 then return; end -- TODO invalid stanza, log
if stanza.name == "iq" then
if not stanza.attr.id then stanza.attr.id = ""; end -- COMPAT Jabiru doesn't send the id attribute on roster requests
- if not iq_types[stanza.attr.type] or ((stanza.attr.type == "set" or stanza.attr.type == "get") and (#stanza.tags ~= 1)) then
- origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid IQ type or incorrect number of children"));
+ if (stanza.attr.type == "set" or stanza.attr.type == "get") and (#stanza.tags ~= 1) then
+ origin.send(st.error_reply(stanza, "modify", "bad-request"));
return;
end
end
if h.events.fire_event(event, {origin = origin, stanza = stanza}) then return; end
end
if host and not hosts[host] then host = nil; end -- COMPAT: workaround for a Pidgin bug which sets 'to' to the SRV result
- handle_unhandled_stanza(host or origin.host or origin.to_host, origin, stanza);
+ modules_handle_stanza(host or origin.host or origin.to_host, origin, stanza);
end
end
if h then
if h.events.fire_event(stanza.name..to_type, event_data) then return; end -- do processing
if to_self and h.events.fire_event(stanza.name..'/self', event_data) then return; end -- do processing
- handle_unhandled_stanza(h.host, origin, stanza);
+
+ if h.type == "component" then
+ component_handle_stanza(origin, stanza);
+ return;
+ end
+ modules_handle_stanza(h.host, origin, stanza);
else
core_route_stanza(origin, stanza);
end
-- COPYING file in the source package for more information.
--
-local modulemanager = require "core.modulemanager";
+local datamanager = require "util.datamanager";
local log = require "util.logger".init("usermanager");
local type = type;
+local error = error;
local ipairs = ipairs;
+local hashes = require "util.hashes";
local jid_bare = require "util.jid".bare;
local config = require "core.configmanager";
local hosts = hosts;
-local sasl_new = require "util.sasl".new;
-local prosody = _G.prosody;
-
-local setmetatable = setmetatable;
-
-local default_provider = "internal_plain";
+local require_provisioning = config.get("*", "core", "cyrus_require_provisioning") or false;
module "usermanager"
-function new_null_provider()
- local function dummy() return nil, "method not implemented"; end;
- local function dummy_get_sasl_handler() return sasl_new(nil, {}); end
- return setmetatable({name = "null", get_sasl_handler = dummy_get_sasl_handler}, {
- __index = function(self, method) return dummy; end
- });
-end
+local function is_cyrus(host) return config.get(host, "core", "sasl_backend") == "cyrus"; end
-local provider_mt = { __index = new_null_provider() };
+function validate_credentials(host, username, password, method)
+ log("debug", "User '%s' is being validated", username);
+ if is_cyrus(host) then return nil, "Legacy auth not supported with Cyrus SASL."; end
+ local credentials = datamanager.load(username, host, "accounts") or {};
-function initialize_host(host)
- local host_session = hosts[host];
- if host_session.type ~= "local" then return; end
-
- host_session.events.add_handler("item-added/auth-provider", function (event)
- local provider = event.item;
- local auth_provider = config.get(host, "core", "authentication") or default_provider;
- if config.get(host, "core", "anonymous_login") then auth_provider = "anonymous"; end -- COMPAT 0.7
- if provider.name == auth_provider then
- host_session.users = setmetatable(provider, provider_mt);
- end
- if host_session.users ~= nil and host_session.users.name ~= nil then
- log("debug", "host '%s' now set to use user provider '%s'", host, host_session.users.name);
- end
- end);
- host_session.events.add_handler("item-removed/auth-provider", function (event)
- local provider = event.item;
- if host_session.users == provider then
- host_session.users = new_null_provider();
+ if method == nil then method = "PLAIN"; end
+ if method == "PLAIN" and credentials.password then -- PLAIN, do directly
+ if password == credentials.password then
+ return true;
+ else
+ return nil, "Auth failed. Invalid username or password.";
end
- end);
- host_session.users = new_null_provider(); -- Start with the default usermanager provider
- local auth_provider = config.get(host, "core", "authentication") or default_provider;
- if config.get(host, "core", "anonymous_login") then auth_provider = "anonymous"; end -- COMPAT 0.7
- if auth_provider ~= "null" then
- modulemanager.load(host, "auth_"..auth_provider);
+ end
+ -- must do md5
+ -- make credentials md5
+ local pwd = credentials.password;
+ if not pwd then pwd = credentials.md5; else pwd = hashes.md5(pwd, true); end
+ -- make password md5
+ if method == "PLAIN" then
+ password = hashes.md5(password or "", true);
+ elseif method ~= "DIGEST-MD5" then
+ return nil, "Unsupported auth method";
+ end
+ -- compare
+ if password == pwd then
+ return true;
+ else
+ return nil, "Auth failed. Invalid username or password.";
end
-end;
-prosody.events.add_handler("host-activated", initialize_host, 100);
-
-function test_password(username, host, password)
- return hosts[host].users.test_password(username, password);
end
function get_password(username, host)
- return hosts[host].users.get_password(username);
+ if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end
+ return (datamanager.load(username, host, "accounts") or {}).password
end
-
-function set_password(username, password, host)
- return hosts[host].users.set_password(username, password);
+function set_password(username, host, password)
+ if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end
+ local account = datamanager.load(username, host, "accounts");
+ if account then
+ account.password = password;
+ return datamanager.store(username, host, "accounts", account);
+ end
+ return nil, "Account not available.";
end
function user_exists(username, host)
- return hosts[host].users.user_exists(username);
+ if not(require_provisioning) and is_cyrus(host) then return true; end
+ local account, err = datamanager.load(username, host, "accounts");
+ return (account or err) ~= nil; -- FIXME also check for empty credentials
end
function create_user(username, password, host)
- return hosts[host].users.create_user(username, password);
+ if not(require_provisioning) and is_cyrus(host) then return nil, "Account creation/modification not available with Cyrus SASL."; end
+ return datamanager.store(username, host, "accounts", {password = password});
end
-function delete_user(username, host)
- return hosts[host].users.delete_user(username);
-end
-
-function get_sasl_handler(host)
- return hosts[host].users.get_sasl_handler();
-end
-
-function get_provider(host)
- return hosts[host].users;
+function get_supported_methods(host)
+ return {["PLAIN"] = true, ["DIGEST-MD5"] = true}; -- TODO this should be taken from the config
end
function is_admin(jid, host)
- if host and not hosts[host] then return false; end
-
- local is_admin;
- jid = jid_bare(jid);
host = host or "*";
-
- local host_admins = config.get(host, "core", "admins");
- local global_admins = config.get("*", "core", "admins");
-
- if host_admins and host_admins ~= global_admins then
- if type(host_admins) == "table" then
- for _,admin in ipairs(host_admins) do
- if admin == jid then
- is_admin = true;
- break;
- end
- end
- elseif host_admins then
- log("error", "Option 'admins' for host '%s' is not a list", host);
- end
+ local admins = config.get(host, "core", "admins");
+ if host ~= "*" and admins == config.get("*", "core", "admins") then
+ return nil;
end
-
- if not is_admin and global_admins then
- if type(global_admins) == "table" then
- for _,admin in ipairs(global_admins) do
- if admin == jid then
- is_admin = true;
- break;
- end
- end
- elseif global_admins then
- log("error", "Global option 'admins' is not a list");
+ if type(admins) == "table" then
+ jid = jid_bare(jid);
+ for _,admin in ipairs(admins) do
+ if admin == jid then return true; end
end
- end
-
- -- Still not an admin, check with auth provider
- if not is_admin and host ~= "*" and hosts[host].users and hosts[host].users.is_admin then
- is_admin = hosts[host].users.is_admin(jid);
- end
- return is_admin or false;
+ elseif admins then log("warn", "Option 'admins' for host '%s' is not a table", host); end
+ return nil;
end
return _M;
local st = stanza;
local tostring = tostring;
-local pairs = pairs;
-local ipairs = ipairs;
local t_insert = table.insert;
local t_concat = table.concat;
module "xmlhandlers"
local ns_prefixes = {
- ["http://www.w3.org/XML/1998/namespace"] = "xml";
- }
+ ["http://www.w3.org/XML/1998/namespace"] = "xml";
+};
+
+local xmlns_streams = "http://etherx.jabber.org/streams";
+
+local ns_separator = "\1";
+local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$";
function init_xmlhandlers(session, stream_callbacks)
- local ns_stack = { "" };
- local curr_tag;
- local chardata = {};
- local xml_handlers = {};
- local log = session.log or default_log;
-
- local cb_streamopened = stream_callbacks.streamopened;
- local cb_streamclosed = stream_callbacks.streamclosed;
- local cb_error = stream_callbacks.error or function (session, e) error("XML stream error: "..tostring(e)); end;
- local cb_handlestanza = stream_callbacks.handlestanza;
-
- local stream_tag = stream_callbacks.stream_tag;
- local stream_default_ns = stream_callbacks.default_ns;
-
- local stanza
- function xml_handlers:StartElement(tagname, attr)
- if stanza and #chardata > 0 then
- -- We have some character data in the buffer
- stanza:text(t_concat(chardata));
- chardata = {};
- end
- local curr_ns,name = tagname:match("^([^\1]*)\1?(.*)$");
- if name == "" then
- curr_ns, name = "", curr_ns;
- end
+ local chardata = {};
+ local xml_handlers = {};
+ local log = session.log or default_log;
+
+ local cb_streamopened = stream_callbacks.streamopened;
+ local cb_streamclosed = stream_callbacks.streamclosed;
+ local cb_error = stream_callbacks.error or function(session, e) error("XML stream error: "..tostring(e)); end;
+ local cb_handlestanza = stream_callbacks.handlestanza;
+
+ local stream_ns = stream_callbacks.stream_ns or xmlns_streams;
+ local stream_tag = stream_ns..ns_separator..(stream_callbacks.stream_tag or "stream");
+ local stream_error_tag = stream_ns..ns_separator..(stream_callbacks.error_tag or "error");
+
+ local stream_default_ns = stream_callbacks.default_ns;
+
+ local stanza;
+ function xml_handlers:StartElement(tagname, attr)
+ if stanza and #chardata > 0 then
+ -- We have some character data in the buffer
+ stanza:text(t_concat(chardata));
+ chardata = {};
+ end
+ local curr_ns,name = tagname:match(ns_pattern);
+ if name == "" then
+ curr_ns, name = "", curr_ns;
+ end
- if curr_ns ~= stream_default_ns then
- attr.xmlns = curr_ns;
- end
-
- -- FIXME !!!!!
- for i=1,#attr do
- local k = attr[i];
- attr[i] = nil;
- local ns, nm = k:match("^([^\1]*)\1?(.*)$");
- if nm ~= "" then
- ns = ns_prefixes[ns];
- if ns then
- attr[ns..":"..nm] = attr[k];
- attr[k] = nil;
- end
- end
- end
-
- if not stanza then --if we are not currently inside a stanza
- if session.notopen then
- if tagname == stream_tag then
- if cb_streamopened then
- cb_streamopened(session, attr);
- end
- else
- -- Garbage before stream?
- cb_error(session, "no-stream");
- end
- return;
- end
- if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then
- cb_error(session, "invalid-top-level-element");
- end
-
- stanza = st.stanza(name, attr);
- curr_tag = stanza;
- else -- we are inside a stanza, so add a tag
- attr.xmlns = nil;
- if curr_ns ~= stream_default_ns then
- attr.xmlns = curr_ns;
- end
- stanza:tag(name, attr);
- end
+ if curr_ns ~= stream_default_ns then
+ attr.xmlns = curr_ns;
end
- function xml_handlers:CharacterData(data)
- if stanza then
- t_insert(chardata, data);
+
+ -- FIXME !!!!!
+ for i=1,#attr do
+ local k = attr[i];
+ attr[i] = nil;
+ local ns, nm = k:match(ns_pattern);
+ if nm ~= "" then
+ ns = ns_prefixes[ns];
+ if ns then
+ attr[ns..":"..nm] = attr[k];
+ attr[k] = nil;
+ end
end
end
- function xml_handlers:EndElement(tagname)
- local curr_ns,name = tagname:match("^([^\1]*)\1?(.*)$");
- if name == "" then
- curr_ns, name = "", curr_ns;
- end
- if (not stanza) or (#stanza.last_add > 0 and name ~= stanza.last_add[#stanza.last_add].name) then
+
+ if not stanza then --if we are not currently inside a stanza
+ if session.notopen then
if tagname == stream_tag then
- if cb_streamclosed then
- cb_streamclosed(session);
+ if cb_streamopened then
+ cb_streamopened(session, attr);
end
- elseif name == "error" then
- cb_error(session, "stream-error", stanza);
else
- cb_error(session, "parse-error", "unexpected-element-close", name);
+ -- Garbage before stream?
+ cb_error(session, "no-stream");
end
- stanza, chardata = nil, {};
return;
end
+ if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then
+ cb_error(session, "invalid-top-level-element");
+ end
+
+ stanza = st.stanza(name, attr);
+ else -- we are inside a stanza, so add a tag
+ attr.xmlns = nil;
+ if curr_ns ~= stream_default_ns then
+ attr.xmlns = curr_ns;
+ end
+ stanza:tag(name, attr);
+ end
+ end
+ function xml_handlers:CharacterData(data)
+ if stanza then
+ t_insert(chardata, data);
+ end
+ end
+ function xml_handlers:EndElement(tagname)
+ if stanza then
if #chardata > 0 then
-- We have some character data in the buffer
stanza:text(t_concat(chardata));
end
-- Complete stanza
if #stanza.last_add == 0 then
- cb_handlestanza(session, stanza);
+ if tagname ~= stream_error_tag then
+ cb_handlestanza(session, stanza);
+ else
+ cb_error(session, "stream-error", stanza);
+ end
stanza = nil;
else
stanza:up();
end
- end
-
- local function restricted_handler(parser)
- cb_error(session, "parse-error", "restricted-xml", "Restricted XML, see RFC 6120 section 11.1.");
- if not parser:stop() then
- error("Failed to abort parsing");
+ else
+ if tagname == stream_tag then
+ if cb_streamclosed then
+ cb_streamclosed(session);
+ end
+ else
+ local curr_ns,name = tagname:match(ns_pattern);
+ if name == "" then
+ curr_ns, name = "", curr_ns;
+ end
+ cb_error(session, "parse-error", "unexpected-element-close", name);
end
+ stanza, chardata = nil, {};
end
-
- if lxp_supports_doctype then
- xml_handlers.StartDoctypeDecl = restricted_handler;
+ end
+
+ local function restricted_handler(parser)
+ cb_error(session, "parse-error", "restricted-xml", "Restricted XML, see RFC 6120 section 11.1.");
+ if not parser:stop() then
+ error("Failed to abort parsing");
end
- xml_handlers.Comment = restricted_handler;
- xml_handlers.ProcessingInstruction = restricted_handler;
-
+ end
+
+ if lxp_supports_doctype then
+ xml_handlers.StartDoctypeDecl = restricted_handler;
+ end
+ xml_handlers.Comment = restricted_handler;
+ xml_handlers.ProcessingInstruction = restricted_handler;
+
return xml_handlers;
end
return;
end
log("debug", "Records for %s not in cache, sending query (%s)...", qname, tostring(coroutine.running()));
- local ok, err = dns.query(qname, qtype, qclass);
- if ok then
- coroutine.yield({ qclass or "IN", qtype or "A", qname, coroutine.running()}); -- Wait for reply
- log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running()));
- end
- if ok then
- ok, err = pcall(handler, dns.peek(qname, qtype, qclass));
- else
- log("error", "Error sending DNS query: %s", err);
- ok, err = pcall(handler, nil, err);
- end
+ dns.query(qname, qtype, qclass);
+ coroutine.yield({ qclass or "IN", qtype or "A", qname, coroutine.running()}); -- Wait for reply
+ log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running()));
+ local ok, err = pcall(handler, dns.peek(qname, qtype, qclass));
if not ok then
log("error", "Error in DNS response handler: %s", tostring(err));
end
end)(dns.peek(qname, qtype, qclass));
end
-function cancel(handle, call_handler, reason)
+function cancel(handle, call_handler)
log("warn", "Cancelling DNS lookup for %s", tostring(handle[3]));
- dns.cancel(handle[1], handle[2], handle[3], handle[4], call_handler);
+ dns.cancel(handle);
+ if call_handler then
+ coroutine.resume(handle[4]);
+ end
end
function new_async_socket(sock, resolver)
handler.setpeername = function (_, ...) peername = (...); local ret = sock:setpeername(...); _:set_send(dummy_send); return ret; end
handler.connect = function (_, ...) return sock:connect(...) end
--handler.send = function (_, data) _:write(data); return _.sendbuffer and _.sendbuffer(); end
- handler.send = function (_, data)
- local getpeername = sock.getpeername;
- log("debug", "Sending DNS query to %s", (getpeername and getpeername(sock)) or "<unconnected>");
- return sock:send(data);
- end
+ handler.send = function (_, data) return sock:send(data); end
return handler;
end
local log = require "util.logger".init("connlisteners");
local tostring = tostring;
-local dofile, xpcall, error =
- dofile, xpcall, error
-
-local debug_traceback = debug.traceback;
+local dofile, pcall, error =
+ dofile, pcall, error
module "connlisteners"
function get(name)
local h = listeners[name];
if not h then
- local ok, ret = xpcall(function() dofile(listeners_dir..name:gsub("[^%w%-]", "_").."_listener.lua") end, debug_traceback);
+ local ok, ret = pcall(dofile, listeners_dir..name:gsub("[^%w%-]", "_").."_listener.lua");
if not ok then
log("error", "Error while loading listener '%s': %s", tostring(name), tostring(ret));
return nil, ret;
-- This file is included with Prosody IM. It has modifications,
-- which are hereby placed in the public domain.
+-- public domain 20080404 lua@ztact.com
+
-- todo: quick (default) header generation
-- todo: nxdomain, error handling
local socket = require "socket";
-local timer = require "util.timer";
-
+local ztact = require "util.ztact";
local _, windows = pcall(require, "util.windows");
local is_windows = (_ and windows) or os.getenv("WINDIR");
local coroutine, io, math, string, table =
coroutine, io, math, string, table;
-local ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack, select, type=
- ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack, select, type;
+local ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack =
+ ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack;
-local ztact = { -- public domain 20080404 lua@ztact.com
- get = function(parent, ...)
- local len = select('#', ...);
- for i=1,len do
- parent = parent[select(i, ...)];
- if parent == nil then break; end
- end
- return parent;
- end;
- set = function(parent, ...)
- local len = select('#', ...);
- local key, value = select(len-1, ...);
- local cutpoint, cutkey;
-
- for i=1,len-2 do
- local key = select (i, ...)
- local child = parent[key]
-
- if value == nil then
- if child == nil then
- return;
- elseif next(child, next(child)) then
- cutpoint = nil; cutkey = nil;
- elseif cutpoint == nil then
- cutpoint = parent; cutkey = key;
- end
- elseif child == nil then
- child = {};
- parent[key] = child;
- end
- parent = child
- end
-
- if value == nil and cutpoint then
- cutpoint[cutkey] = nil;
- else
- parent[key] = value;
- return value;
- end
- end;
-};
local get, set = ztact.get, ztact.set;
-local default_timeout = 15;
-------------------------------------------------- module dns
module('dns')
local resolver = {};
resolver.__index = resolver;
-resolver.timeout = default_timeout;
-local function default_rr_tostring(rr)
- local rr_val = rr.type and rr[rr.type:lower()];
- if type(rr_val) ~= "string" then
- return "<UNKNOWN RDATA TYPE>";
- end
- return rr_val;
-end
-
-local special_tostrings = {
- LOC = resolver.LOC_tostring;
- MX = function (rr)
- return string.format('%2i %s', rr.pref, rr.mx);
- end;
- SRV = function (rr)
- local s = rr.srv;
- return string.format('%5d %5d %5d %s', s.priority, s.weight, s.port, s.target);
- end;
-};
+local SRV_tostring;
+
local rr_metatable = {}; -- - - - - - - - - - - - - - - - - - - rr_metatable
function rr_metatable.__tostring(rr)
- local rr_string = (special_tostrings[rr.type] or default_rr_tostring)(rr);
- return string.format('%2s %-5s %6i %-28s %s', rr.class, rr.type, rr.ttl, rr.name, rr_string);
+ local s0 = string.format('%2s %-5s %6i %-28s', rr.class, rr.type, rr.ttl, rr.name);
+ local s1 = '';
+ if rr.type == 'A' then
+ s1 = ' '..rr.a;
+ elseif rr.type == 'MX' then
+ s1 = string.format(' %2i %s', rr.pref, rr.mx);
+ elseif rr.type == 'CNAME' then
+ s1 = ' '..rr.cname;
+ elseif rr.type == 'LOC' then
+ s1 = ' '..resolver.LOC_tostring(rr);
+ elseif rr.type == 'NS' then
+ s1 = ' '..rr.ns;
+ elseif rr.type == 'SRV' then
+ s1 = ' '..SRV_tostring(rr);
+ elseif rr.type == 'TXT' then
+ s1 = ' '..rr.txt;
+ else
+ s1 = ' <UNKNOWN RDATA TYPE>';
+ end
+ return s0..s1;
end
rr.a = string.format('%i.%i.%i.%i', b1, b2, b3, b4);
end
-function resolver:AAAA(rr)
- local addr = {};
- for i = 1, rr.rdlength, 2 do
- local b1, b2 = self:byte(2);
- table.insert(addr, ("%02x%02x"):format(b1, b2));
- end
- rr.aaaa = table.concat(addr, ":");
-end
function resolver:CNAME(rr) -- - - - - - - - - - - - - - - - - - - - CNAME
rr.cname = self:name();
rr.srv.target = self:name();
end
-function resolver:PTR(rr)
- rr.ptr = self:name();
+
+function SRV_tostring(rr) -- - - - - - - - - - - - - - - - - - SRV_tostring
+ local s = rr.srv;
+ return string.format( '%5d %5d %5d %s', s.priority, s.weight, s.port, s.target );
end
+
function resolver:TXT(rr) -- - - - - - - - - - - - - - - - - - - - - - TXT
- rr.txt = self:sub (self:byte());
+ rr.txt = self:sub (rr.rdlength);
end
function resolver:adddefaultnameservers() -- - - - - adddefaultnameservers
if is_windows then
- if windows and windows.get_nameservers then
+ if windows then
for _, server in ipairs(windows.get_nameservers()) do
self:addnameserver(server);
end
local sock = self.socket[servernum];
if sock then return sock; end
- local err;
- sock, err = socket.udp();
- if not sock then
- return nil, err;
- end
+ sock = socket.udp();
if self.socket_wrapper then sock = self.socket_wrapper(sock, self); end
sock:settimeout(0);
-- todo: attempt to use a random port, fallback to 0
retry = socket.gettime() + self.delays[1]
};
- -- remember the query
+ -- remember the query
self.active[id] = self.active[id] or {};
self.active[id][question] = o;
- -- remember which coroutine wants the answer
+ -- remember which coroutine wants the answer
local co = coroutine.running();
if co then
set(self.wanted, qclass, qtype, qname, co, true);
--set(self.yielded, co, qclass, qtype, qname, true);
end
- local conn, err = self:getsocket(o.server)
- if not conn then
- return nil, err;
- end
- conn:send (o.packet)
-
- if timer and self.timeout then
- local num_servers = #self.server;
- local i = 1;
- timer.add_task(self.timeout, function ()
- if get(self.wanted, qclass, qtype, qname, co) then
- if i < num_servers then
- i = i + 1;
- self:servfail(conn);
- o.server = self.best_server;
- conn, err = self:getsocket(o.server);
- if conn then
- conn:send(o.packet);
- return self.timeout;
- end
- end
- -- Tried everything, failed
- self:cancel(qclass, qtype, qname, co, true);
- end
- end)
- end
- return true;
+ self:getsocket (o.server):send (o.packet)
end
function resolver:servfail(sock)
end
end
end
-
+
if num == self.best_server then
self.best_server = self.best_server + 1;
if self.best_server > #self.server then
end
end
-function resolver:settimeout(seconds)
- self.timeout = seconds;
-end
-
function resolver:receive(rset) -- - - - - - - - - - - - - - - - - receive
--print('receive'); print(self.socket);
self.time = socket.gettime();
end
-function resolver:feed(sock, packet, force)
+function resolver:feed(sock, packet)
--print('receive'); print(self.socket);
self.time = socket.gettime();
- local response = self:decode(packet, force);
+ local response = self:decode(packet);
if response and self.active[response.header.id]
and self.active[response.header.id][response.question.raw] then
--print('received response');
return response;
end
-function resolver:cancel(qclass, qtype, qname, co, call_handler)
- local cos = get(self.wanted, qclass, qtype, qname);
+function resolver:cancel(data)
+ local cos = get(self.wanted, unpack(data, 1, 3));
if cos then
- if call_handler then
- coroutine.resume(co);
- end
- cos[co] = nil;
+ cos[data[4]] = nil;
end
end
function resolver:lookup(qname, qtype, qclass) -- - - - - - - - - - lookup
self:query (qname, qtype, qclass)
while self:pulse() do
- local recvt = {}
- for i, s in ipairs(self.socket) do
- recvt[i] = s
- end
- socket.select(recvt, nil, 4)
- end
+ local recvt = {}
+ for i, s in ipairs(self.socket) do
+ recvt[i] = s
+ end
+ socket.select(recvt, nil, 4)
+ end
--print(self.cache);
return self:peek(qname, qtype, qclass);
end
return self:peek(qname, qtype, qclass) or self:query(qname, qtype, qclass);
end
-function resolver:tohostname(ip)
- return dns.lookup(ip:gsub("(%d+)%.(%d+)%.(%d+)%.(%d+)", "%4.%3.%2.%1.in-addr.arpa."), "PTR");
-end
--print ---------------------------------------------------------------- print
return _resolver:lookup(...);
end
-function dns.tohostname(...)
- return _resolver:tohostname(...);
-end
-
function dns.purge(...) -- - - - - - - - - - - - - - - - - - - - - - purge
return _resolver:purge(...);
end
return _resolver:cancel(...);
end
-function dns.settimeout(...)
- return _resolver:settimeout(...);
-end
-
function dns.socket_wrapper_set(...) -- - - - - - - - - socket_wrapper_set
return _resolver:socket_wrapper_set(...);
end
local socket = require "socket"
local mime = require "mime"
local url = require "socket.url"
-local httpstream_new = require "util.httpstream".new;
local server = require "net.server"
local listener = connlisteners_get("httpclient") or error("No httpclient listener!");
local t_insert, t_concat = table.insert, table.concat;
-local pairs, ipairs = pairs, ipairs;
-local tonumber, tostring, xpcall, select, debug_traceback, char, format =
- tonumber, tostring, xpcall, select, debug.traceback, string.char, string.format;
+local tonumber, tostring, pairs, xpcall, select, debug_traceback, char, format =
+ tonumber, tostring, pairs, xpcall, select, debug.traceback, string.char, string.format;
local log = require "util.logger".init("http");
function urlencode(s) return s and (s:gsub("%W", function (c) return format("%%%02x", c:byte()); end)); end
function urldecode(s) return s and (s:gsub("%%(%x%x)", function (c) return char(tonumber(c,16)); end)); end
-local function _formencodepart(s)
- return s and (s:gsub("%W", function (c)
- if c ~= " " then
- return format("%%%02x", c:byte());
- else
- return "+";
- end
- end));
-end
-function formencode(form)
- local result = {};
- for _, field in ipairs(form) do
- t_insert(result, _formencodepart(field.name).."=".._formencodepart(field.value));
- end
- return t_concat(result, "&");
+local function expectbody(reqt, code)
+ if reqt.method == "HEAD" then return nil end
+ if code == 204 or code == 304 or code == 301 then return nil end
+ if code >= 100 and code < 200 then return nil end
+ return 1
end
local function request_reader(request, data, startpos)
- if not request.parser then
- local function success_cb(r)
- if request.callback then
- for k,v in pairs(r) do request[k] = v; end
- request.callback(r.body, r.code, request);
- request.callback = nil;
+ if not data then
+ if request.body then
+ log("debug", "Connection closed, but we have data, calling callback...");
+ request.callback(t_concat(request.body), request.code, request);
+ elseif request.state ~= "completed" then
+ -- Error.. connection was closed prematurely
+ request.callback("connection-closed", 0, request);
+ return;
+ end
+ destroy_request(request);
+ request.body = nil;
+ request.state = "completed";
+ return;
+ end
+ if request.state == "body" and request.state ~= "completed" then
+ log("debug", "Reading body...")
+ if not request.body then request.body = {}; request.havebodylength, request.bodylength = 0, tonumber(request.responseheaders["content-length"]); end
+ if startpos then
+ data = data:sub(startpos, -1)
+ end
+ t_insert(request.body, data);
+ if request.bodylength then
+ request.havebodylength = request.havebodylength + #data;
+ if request.havebodylength >= request.bodylength then
+ -- We have the body
+ log("debug", "Have full body, calling callback");
+ if request.callback then
+ request.callback(t_concat(request.body), request.code, request);
+ end
+ request.body = nil;
+ request.state = "completed";
+ else
+ log("debug", "Have "..request.havebodylength.." bytes out of "..request.bodylength);
end
+ end
+ elseif request.state == "headers" then
+ log("debug", "Reading headers...")
+ local pos = startpos;
+ local headers, headers_complete = request.responseheaders;
+ if not headers then
+ headers = {};
+ request.responseheaders = headers;
+ end
+ for line in data:sub(startpos, -1):gmatch("(.-)\r\n") do
+ startpos = startpos + #line + 2;
+ local k, v = line:match("(%S+): (.+)");
+ if k and v then
+ headers[k:lower()] = v;
+ --log("debug", "Header: "..k:lower().." = "..v);
+ elseif #line == 0 then
+ headers_complete = true;
+ break;
+ else
+ log("warn", "Unhandled header line: "..line);
+ end
+ end
+ if not headers_complete then return; end
+ -- Reached the end of the headers
+ if not expectbody(request, request.code) then
+ request.callback(nil, request.code, request);
+ return;
+ end
+ request.state = "body";
+ if #data > startpos then
+ return request_reader(request, data, startpos);
+ end
+ elseif request.state == "status" then
+ log("debug", "Reading status...")
+ local http, code, text, linelen = data:match("^HTTP/(%S+) (%d+) (.-)\r\n()", startpos);
+ code = tonumber(code);
+ if not code then
+ log("warn", "Invalid HTTP status line, telling callback then closing");
+ local ret = request.callback("invalid-status-line", 0, request);
destroy_request(request);
+ return ret;
end
- local function error_cb(r)
+
+ request.code, request.responseversion = code, http;
+
+ if request.onlystatus then
if request.callback then
- request.callback(r or "connection-closed", 0, request);
- request.callback = nil;
+ request.callback(nil, code, request);
end
destroy_request(request);
+ return;
end
- local function options_cb()
- return request;
+
+ request.state = "headers";
+
+ if #data > linelen then
+ return request_reader(request, data, linelen);
end
- request.parser = httpstream_new(success_cb, error_cb, "client", options_cb);
end
- request.parser:feed(data);
end
local function handleerr(err) log("error", "Traceback[http]: %s: %s", tostring(err), debug_traceback()); end
--
+local socket = require "socket"
local server = require "net.server"
local url_parse = require "socket.url".parse;
-local httpstream_new = require "util.httpstream".new;
local connlisteners_start = require "net.connlisteners".start;
local connlisteners_get = require "net.connlisteners".get;
local listener;
local t_insert, t_concat = table.insert, table.concat;
+local s_match, s_gmatch = string.match, string.gmatch;
local tonumber, tostring, pairs, ipairs, type = tonumber, tostring, pairs, ipairs, type;
-local xpcall = xpcall;
-local debug_traceback = debug.traceback;
-local urlencode = function (s) return s and (s:gsub("%W", function (c) return ("%%%02x"):format(c:byte()); end)); end
+local urlencode = function (s) return s and (s:gsub("%W", function (c) return string.format("%%%02x", c:byte()); end)); end
local log = require "util.logger".init("httpserver");
local default_handler;
+local function expectbody(reqt)
+ return reqt.method == "POST";
+end
+
local function send_response(request, response)
-- Write status line
local resp;
callback = (request.server and request.server.handlers[base]) or default_handler;
end
if callback then
- local _callback = callback;
- function callback(method, body, request)
- local ok, result = xpcall(function() return _callback(method, body, request) end, debug_traceback);
- if ok then return result; end
- log("error", "Error in HTTP server handler: %s", result);
- -- TODO: When we support pipelining, request.destroyed
- -- won't be the right flag - we just want to see if there
- -- has been a response to this request yet.
- if not request.destroyed then
- return {
- status = "500 Internal Server Error";
- headers = { ["Content-Type"] = "text/plain" };
- body = "There was an error processing your request. See the error log for more details.";
- };
- end
- end
if err then
log("debug", "Request error: "..err);
if not callback(nil, err, request) then
end
local function request_reader(request, data, startpos)
- if not request.parser then
- local function success_cb(r)
- for k,v in pairs(r) do request[k] = v; end
- request.url = url_parse(request.path);
- request.url.path = request.url.path and request.url.path:gsub("%%(%x%x)", function(x) return x.char(tonumber(x, 16)) end);
- request.body = { request.body };
+ if not data then
+ if request.body then
call_callback(request);
+ else
+ -- Error.. connection was closed prematurely
+ call_callback(request, "connection-closed");
end
- local function error_cb(r)
- call_callback(request, r or "connection-closed");
- destroy_request(request);
+ -- Here we force a destroy... the connection is gone, so we can't reply later
+ destroy_request(request);
+ return;
+ end
+ if request.state == "body" then
+ log("debug", "Reading body...")
+ if not request.body then request.body = {}; request.havebodylength, request.bodylength = 0, tonumber(request.headers["content-length"]); end
+ if startpos then
+ data = data:sub(startpos, -1)
+ end
+ t_insert(request.body, data);
+ if request.bodylength then
+ request.havebodylength = request.havebodylength + #data;
+ if request.havebodylength >= request.bodylength then
+ -- We have the body
+ call_callback(request);
+ end
+ end
+ elseif request.state == "headers" then
+ log("debug", "Reading headers...")
+ local pos = startpos;
+ local headers, headers_complete = request.headers;
+ if not headers then
+ headers = {};
+ request.headers = headers;
+ end
+
+ for line in data:gmatch("(.-)\r\n") do
+ startpos = (startpos or 1) + #line + 2;
+ local k, v = line:match("(%S+): (.+)");
+ if k and v then
+ headers[k:lower()] = v;
+ --log("debug", "Header: '"..k:lower().."' = '"..v.."'");
+ elseif #line == 0 then
+ headers_complete = true;
+ break;
+ else
+ log("debug", "Unhandled header line: "..line);
+ end
+ end
+
+ if not headers_complete then return; end
+
+ if not expectbody(request) then
+ call_callback(request);
+ return;
+ end
+
+ -- Reached the end of the headers
+ request.state = "body";
+ if #data > startpos then
+ return request_reader(request, data:sub(startpos, -1));
+ end
+ elseif request.state == "request" then
+ log("debug", "Reading request line...")
+ local method, path, http, linelen = data:match("^(%S+) (%S+) HTTP/(%S+)\r\n()", startpos);
+ if not method then
+ log("warn", "Invalid HTTP status line, telling callback then closing");
+ local ret = call_callback(request, "invalid-status-line");
+ request:destroy();
+ return ret;
+ end
+
+ request.method, request.path, request.httpversion = method, path, http;
+
+ request.url = url_parse(request.path);
+
+ log("debug", method.." request for "..tostring(request.path) .. " on port "..request.handler:serverport());
+
+ if request.onlystatus then
+ if not call_callback(request) then
+ return;
+ end
+ end
+
+ request.state = "headers";
+
+ if #data > linelen then
+ return request_reader(request, data:sub(linelen, -1));
end
- request.parser = httpstream_new(success_cb, error_cb);
end
- request.parser:feed(data);
end
-- The default handler for requests
log("warn", "Old syntax of httpserver.new_from_config being used to register %s", handle_request);
handle_request, default_options = default_options, { base = handle_request };
end
- ports = ports or {5280};
for _, options in ipairs(ports) do
local port = default_options.port or 5280;
local base = default_options.base;
ssl.options = "no_sslv2";
end
- new{ port = port, interface = interface,
- base = base, handler = handle_request,
+ new{ port = port, interface = interface,
+ base = base, handler = handle_request,
ssl = ssl, type = (ssl and "ssl") or "tcp" };
end
end
if buf:match("^[a-zA-Z]") then
local listener = httpserver_listener;
conn:setlistener(listener);
- local onconnect = listener.onconnect;
- if onconnect then onconnect(conn) end
listener.onincoming(conn, buf);
elseif buf:match(">") then
local listener;
listener = xmppclient_listener;
end
conn:setlistener(listener);
- local onconnect = listener.onconnect;
- if onconnect then onconnect(conn) end
listener.onincoming(conn, buf);
elseif #buf > 1024 then
conn:close();
-- COPYING file in the source package for more information.
--
-local use_luaevent = prosody and require "core.configmanager".get("*", "core", "use_libevent");
+local use_luaevent = require "core.configmanager".get("*", "core", "use_libevent");
if use_luaevent then
use_luaevent = pcall(require, "luaevent.core");
debug( "new connection failed. id:", self.id, "error:", self.fatalerror )
else
if plainssl and ssl then -- start ssl session
- self:starttls(nil, true)
+ self:starttls()
else -- normal connection
- self:_start_session(true)
+ self:_start_session( self.listener.onconnect )
end
debug( "new connection established. id:", self.id )
end
self.eventconnect = addevent( base, self.conn, EV_WRITE, callback, cfg.CONNECT_TIMEOUT )
return true
end
- function interface_mt:_start_session(call_onconnect) -- new session, for example after startssl
+ function interface_mt:_start_session(onconnect) -- new session, for example after startssl
if self.type == "client" then
local callback = function( )
self:_lock( false, false, false )
--vdebug( "start listening on client socket with id:", self.id )
self.eventread = addevent( base, self.conn, EV_READ, self.readcallback, cfg.READ_TIMEOUT ); -- register callback
- if call_onconnect then
- self:onconnect()
- end
+ self:onconnect()
self.eventsession = nil
return -1
end
end
return true
end
- function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed, therefore we have to close read/write events first
+ function interface_mt:_start_ssl(arg) -- old socket will be destroyed, therefore we have to close read/write events first
--vdebug( "starting ssl session with client id:", self.id )
local _
_ = self.eventread and self.eventread:close( ) -- close events; this must be called outside of the event callbacks!
if err then
self.fatalerror = err
self.conn = nil -- cannot be used anymore
- if call_onconnect then
+ if "onconnect" == arg then
self.ondisconnect = nil -- dont call this when client isnt really connected
end
self:_close()
self.send = self.conn.send -- caching table lookups with new client object
self.receive = self.conn.receive
local onsomething
- if not call_onconnect then -- trigger listener
- self:onstatus("ssl-handshake-complete");
+ if "onconnect" == arg then -- trigger listener
+ onsomething = self.onconnect
+ else
+ onsomething = self.onsslconnection
end
- self:_start_session( call_onconnect )
+ self:_start_session( onsomething )
debug( "ssl handshake done" )
+ self:onstatus("ssl-handshake-complete");
self.eventhandshake = nil
return -1
end
+ debug( "error during ssl handshake:", err )
if err == "wantwrite" then
event = EV_WRITE
elseif err == "wantread" then
event = EV_READ
else
- debug( "ssl handshake error:", err )
self.fatalerror = err
end
end
if self.fatalerror then
- if call_onconnect then
+ if "onconnect" == arg then
self.ondisconnect = nil -- dont call this when client isnt really connected
end
self:_close()
end
end
- function interface_mt:socket()
- return self.conn
- end
-
function interface_mt:server()
return self._server or self;
end
-- No-op, we always use the underlying connection's send
end
- function interface_mt:starttls(sslctx, call_onconnect)
+ function interface_mt:starttls(sslctx)
debug( "try to start ssl at client id:", self.id )
local err
self._sslctx = sslctx;
self._usingssl = true
self.startsslcallback = function( ) -- we have to start the handshake outside of a read/write event
self.startsslcallback = nil
- self:_start_ssl(call_onconnect);
+ self:_start_ssl();
self.eventstarthandshake = nil
return -1
end
function interface_mt:ondrain()
end
function interface_mt:onstatus()
+ debug("server.lua: Dummy onstatus()")
end
end
local clientinterface = handleclient( client, client_ip, client_port, interface, pattern, listener, nil, sslctx )
--vdebug( "client id:", clientinterface, "startssl:", startssl )
if ssl and sslctx then
- clientinterface:starttls(sslctx, true)
+ clientinterface:starttls(sslctx)
else
- clientinterface:_start_session( true )
+ clientinterface:_start_session( clientinterface.onconnect )
end
debug( "accepted incoming client connection from:", client_ip or "<unknown IP>", client_port or "<unknown port>", "to", port or "<unknown port>");
--vdebug( "creating new tcp server with following parameters:", addr or "nil", port or "nil", sslcfg or "nil", startssl or "nil")
local server, err = socket.bind( addr, port, cfg.ACCEPT_QUEUE ) -- create server socket
if not server then
- debug( "creating server socket on "..addr.." port "..port.." failed:", err )
+ debug( "creating server socket failed because:", err )
return nil, err
end
local sslctx
end
local function link(sender, receiver, buffersize)
+ sender:set_mode(buffersize);
local sender_locked;
function receiver:ondrain()
local type = use "type"
local pairs = use "pairs"
local ipairs = use "ipairs"
-local tonumber = use "tonumber"
local tostring = use "tostring"
local collectgarbage = use "collectgarbage"
--// lua lib methods //--
+local os_time = os.time
local os_difftime = os.difftime
-local math_min = math.min
-local math_huge = math.huge
local table_concat = table.concat
local table_remove = table.remove
local string_len = string.len
local luasec = use "ssl"
local luasocket = use "socket" or require "socket"
-local luasocket_gettime = luasocket.gettime
--// extern lib methods //--
local idfalse
local addtimer
local closeall
-local addsocket
local addserver
local getserver
local wrapserver
local _maxclientsperserver
-local _maxsslhandshake
-
----------------------------------// DEFINITION //--
_server = { } -- key = port, value = table; list of listening servers
local connections = 0
- local dispatch, disconnect = listeners.onconnect or listeners.onincoming, listeners.ondisconnect
+ local dispatch, disconnect = listeners.onincoming, listeners.ondisconnect
local accept = socket.accept
if drain then
drain(handler)
end
- _ = needtls and handler:starttls(nil)
+ _ = needtls and handler:starttls(nil, true)
_ = toclose and handler:close( )
return true
elseif byte and ( err == "timeout" or err == "wantwrite" ) then -- want write
_readlistlen = addsocket(_readlist, client, _readlistlen)
return true
else
+ out_put( "server.lua: error during ssl handshake: ", tostring(err) )
if err == "wantwrite" and not wrote then
_sendlistlen = addsocket(_sendlist, client, _sendlistlen)
wrote = true
_readlistlen = addsocket(_readlist, client, _readlistlen)
read = true
else
- out_put( "server.lua: ssl handshake error: ", tostring(err) )
break;
end
--coroutine_yield( handler, nil, err ) -- handshake not finished
end
else
local sslctx;
- handler.starttls = function( self, _sslctx)
+ handler.starttls = function( self, _sslctx, now )
if _sslctx then
sslctx = _sslctx;
handler:set_sslctx(sslctx);
end
- if bufferqueuelen > 0 then
- out_put "server.lua: we need to do tls, but delaying until send buffer empty"
+ if not now then
+ out_put "server.lua: we need to do tls, but delaying until later"
needtls = true
return
end
_socketlist[ socket ] = handler
_readlistlen = addsocket(_readlist, socket, _readlistlen)
+ if listeners.onconnect then
+ _sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
+ handler.sendbuffer = function ()
+ listeners.onconnect(handler);
+ handler.sendbuffer = _sendbuffer;
+ if bufferqueuelen > 0 then
+ return _sendbuffer();
+ end
+ end
+ end
return handler, socket
end
end
local function link(sender, receiver, buffersize)
+ sender:set_mode(buffersize);
local sender_locked;
local _sendbuffer = receiver.sendbuffer;
function receiver.sendbuffer()
return _readtraffic, _sendtraffic, _readlistlen, _sendlistlen, _timerlistlen
end
-local quitting;
+local dontstop = true; -- thinking about tomorrow, ...
setquitting = function (quit)
- quitting = not not quit;
+ dontstop = not quit;
+ return;
end
-loop = function(once) -- this is the main loop of the program
- if quitting then return "quitting"; end
- if once then quitting = "once"; end
- local next_timer_time = math_huge;
- repeat
- local read, write, err = socket_select( _readlist, _sendlist, math_min(_selecttimeout, next_timer_time) )
+loop = function( ) -- this is the main loop of the program
+ while dontstop do
+ local read, write, err = socket_select( _readlist, _sendlist, _selecttimeout )
for i, socket in ipairs( write ) do -- send data waiting in writequeues
local handler = _socketlist[ socket ]
if handler then
handler:close( true ) -- forced disconnect
end
clean( _closelist )
- _currenttime = luasocket_gettime( )
- if _currenttime - _timer >= math_min(next_timer_time, 1) then
- next_timer_time = math_huge;
+ _currenttime = os_time( )
+ if os_difftime( _currenttime - _timer ) >= 1 then
for i = 1, _timerlistlen do
- local t = _timerlist[ i ]( _currenttime ) -- fire timers
- if t then next_timer_time = math_min(next_timer_time, t); end
+ _timerlist[ i ]( _currenttime ) -- fire timers
end
_timer = _currenttime
- else
- next_timer_time = next_timer_time - (_currenttime - _timer);
end
socket_sleep( _sleeptime ) -- wait some time
--collectgarbage( )
- until quitting;
- if once and quitting == "once" then quitting = nil; return; end
+ end
return "quitting"
end
-step = function ()
- return loop(true);
-end
-
local function get_backend()
return "select";
end
local handler = wrapconnection( nil, listeners, socket, ip, serverport, "clientport", pattern, sslctx )
_socketlist[ socket ] = handler
_sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
- if listeners.onconnect then
- -- When socket is writeable, call onconnect
- local _sendbuffer = handler.sendbuffer;
- handler.sendbuffer = function ()
- handler.sendbuffer = _sendbuffer;
- listeners.onconnect(handler);
- -- If there was data with the incoming packet, handle it now.
- if #handler:bufferqueue() > 0 then
- return _sendbuffer();
- end
- end
- end
return handler, socket
end
use "setmetatable" ( _readtimes, { __mode = "k" } )
use "setmetatable" ( _writetimes, { __mode = "k" } )
-_timer = luasocket_gettime( )
-_starttime = luasocket_gettime( )
+_timer = os_time( )
+_starttime = os_time( )
addtimer( function( )
local difftime = os_difftime( _currenttime - _starttime )
loop = loop,
link = link,
- step = step,
stats = stats,
closeall = closeall,
addtimer = addtimer,
local logger = require "logger";
local log = logger.init("xmppclient_listener");
-local new_xmpp_stream = require "util.xmppstream".new;
+local lxp = require "lxp"
+local init_xmlhandlers = require "core.xmlhandlers"
+local sm_new_session = require "core.sessionmanager".new_session;
local connlisteners_register = require "net.connlisteners".register;
+local t_insert = table.insert;
+local t_concat = table.concat;
+local t_concatall = function (t, sep) local tt = {}; for _, s in ipairs(t) do t_insert(tt, tostring(s)); end return t_concat(tt, sep); end
+local m_random = math.random;
+local format = string.format;
local sessionmanager = require "core.sessionmanager";
local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session;
local sm_streamopened = sessionmanager.streamopened;
local sm_streamclosed = sessionmanager.streamclosed;
local st = require "util.stanza";
-local xpcall = xpcall;
-local tostring = tostring;
-local type = type;
-local traceback = debug.traceback;
local config = require "core.configmanager";
local opt_keepalives = config.get("*", "core", "tcp_keepalives");
session:close("invalid-namespace");
elseif error == "parse-error" then
(session.log or log)("debug", "Client XML parse error: %s", tostring(data));
- session:close("not-well-formed");
+ session:close("xml-not-well-formed");
elseif error == "stream-error" then
local condition, text = "undefined-condition";
for child in data:children() do
end
end
-local function handleerr(err) log("error", "Traceback[c2s]: %s: %s", tostring(err), traceback()); end
-function stream_callbacks.handlestanza(session, stanza)
- stanza = session.filter("stanzas/in", stanza);
- if stanza then
- return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
- end
+local function handleerr(err) log("error", "Traceback[c2s]: %s: %s", tostring(err), debug.traceback()); end
+function stream_callbacks.handlestanza(a, b)
+ xpcall(function () core_process_stanza(a, b) end, handleerr);
end
local sessions = {};
-- These are session methods --
+local function session_reset_stream(session)
+ -- Reset stream
+ local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "\1");
+ session.parser = parser;
+
+ session.notopen = true;
+
+ function session.data(conn, data)
+ local ok, err = parser:parse(data);
+ if ok then return; end
+ log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
+ session:close("xml-not-well-formed");
+ end
+
+ return true;
+end
+
local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
local function session_close(session, reason)
-- End of session methods --
-function xmppclient.onconnect(conn)
- local session = sm_new_session(conn);
- sessions[conn] = session;
-
- session.log("info", "Client connected");
-
- -- Client is using legacy SSL (otherwise mod_tls sets this flag)
- if conn:ssl() then
- session.secure = true;
- end
-
- if opt_keepalives ~= nil then
- conn:setoption("keepalive", opt_keepalives);
- end
-
- session.close = session_close;
-
- local stream = new_xmpp_stream(session, stream_callbacks);
- session.stream = stream;
-
- session.notopen = true;
-
- function session.reset_stream()
- session.notopen = true;
- session.stream:reset();
- end
-
- local filter = session.filter;
- function session.data(data)
- data = filter("bytes/in", data);
- if data then
- local ok, err = stream:feed(data);
- if ok then return; end
- log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
- session:close("not-well-formed");
- end
- end
-
- local handlestanza = stream_callbacks.handlestanza;
- function session.dispatch_stanza(session, stanza)
- return handlestanza(session, stanza);
- end
-end
-
function xmppclient.onincoming(conn, data)
local session = sessions[conn];
- if session then
- session.data(data);
+ if not session then
+ session = sm_new_session(conn);
+ sessions[conn] = session;
+
+ session.log("info", "Client connected");
+
+ -- Client is using legacy SSL (otherwise mod_tls sets this flag)
+ if conn:ssl() then
+ session.secure = true;
+ end
+
+ if opt_keepalives ~= nil then
+ conn:setoption("keepalive", opt_keepalives);
+ end
+
+ session.reset_stream = session_reset_stream;
+ session.close = session_close;
+
+ session_reset_stream(session); -- Initialise, ready for use
+
+ session.dispatch_stanza = stream_callbacks.handlestanza;
+ end
+ if data then
+ session.data(conn, data);
end
end
end
end
-function xmppclient.associate_session(conn, session)
- sessions[conn] = session;
-end
-
connlisteners_register("xmppclient", xmppclient);
local hosts = _G.hosts;
local t_concat = table.concat;
-local tostring = tostring;
-local type = type;
-local pairs = pairs;
local lxp = require "lxp";
local logger = require "util.logger";
local config = require "core.configmanager";
local connlisteners = require "net.connlisteners";
+local cm_register_component = require "core.componentmanager".register_component;
+local cm_deregister_component = require "core.componentmanager".deregister_component;
local uuid_gen = require "util.uuid".generate;
-local jid_split = require "util.jid".split;
local sha1 = require "util.hashes".sha1;
local st = require "util.stanza";
-local new_xmpp_stream = require "util.xmppstream".new;
+local init_xmlhandlers = require "core.xmlhandlers";
local sessions = {};
local xmlns_component = 'jabber:component:accept';
---- Callbacks/data for xmppstream to handle streams for us ---
+--- Callbacks/data for xmlhandlers to handle streams for us ---
local stream_callbacks = { default_ns = xmlns_component };
session:close("invalid-namespace");
elseif error == "parse-error" then
session.log("warn", "External component %s XML parse error: %s", tostring(session.host), tostring(data));
- session:close("not-well-formed");
+ session:close("xml-not-well-formed");
elseif error == "stream-error" then
local condition, text = "undefined-condition";
for child in data:children() do
function stream_callbacks.streamopened(session, attr)
if config.get(attr.to, "core", "component_module") ~= "component" then
- -- Trying to act as a component domain which
+ -- Trying to act as a component domain which
-- hasn't been configured
session:close{ condition = "host-unknown", text = tostring(attr.to).." does not match any configured external components" };
return;
end
- -- Note that we don't create the internal component
+ -- Store the original host (this is used for config, etc.)
+ session.user = attr.to;
+ -- Set the host for future reference
+ session.host = config.get(attr.to, "core", "component_address") or attr.to;
+ -- Note that we don't create the internal component
-- until after the external component auths successfully
- session.host = attr.to;
session.streamid = uuid_gen();
session.notopen = nil;
end
function stream_callbacks.streamclosed(session)
- session.log("debug", "Received </stream:stream>");
+ session.log("Received </stream:stream>");
session:close();
end
if not stanza.attr.xmlns and stanza.name == "handshake" then
stanza.attr.xmlns = xmlns_component;
end
- if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then
- local from = stanza.attr.from;
- if 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
- end
- else
- stanza.attr.from = session.host;
- end
- if not stanza.attr.to then
- session.log("warn", "Rejecting stanza with no 'to' address");
- session.send(st.error_reply(stanza, "modify", "bad-request", "Components MUST specify a 'to' address on stanzas"));
- return;
- end
- end
return core_process_stanza(session, stanza);
end
end
--- Component connlistener
-function component_listener.onconnect(conn)
- local _send = conn.write;
- local session = { type = "component", conn = conn, send = function (data) return _send(conn, tostring(data)); end };
-
- -- Logging functions --
- local conn_name = "jcp"..tostring(conn):match("[a-f0-9]+$");
- session.log = logger.init(conn_name);
- session.close = session_close;
-
- session.log("info", "Incoming Jabber component connection");
-
- local stream = new_xmpp_stream(session, stream_callbacks);
- session.stream = stream;
-
- session.notopen = true;
-
- function session.reset_stream()
+function component_listener.onincoming(conn, data)
+ local session = sessions[conn];
+ if not session then
+ local _send = conn.write;
+ session = { type = "component", conn = conn, send = function (data) return _send(conn, tostring(data)); end };
+ sessions[conn] = session;
+
+ -- Logging functions --
+
+ local conn_name = "jcp"..tostring(conn):match("[a-f0-9]+$");
+ session.log = logger.init(conn_name);
+ session.close = session_close;
+
+ session.log("info", "Incoming Jabber component connection");
+
+ local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "\1");
+ session.parser = parser;
+
session.notopen = true;
- session.stream:reset();
+
+ function session.data(conn, data)
+ local ok, err = parser:parse(data);
+ if ok then return; end
+ log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
+ session:close("xml-not-well-formed");
+ end
+
+ session.dispatch_stanza = stream_callbacks.handlestanza;
+
end
-
- function session.data(conn, data)
- local ok, err = stream:feed(data);
- if ok then return; end
- log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
- session:close("not-well-formed");
+ if data then
+ session.data(conn, data);
end
-
- session.dispatch_stanza = stream_callbacks.handlestanza;
-
- sessions[conn] = session;
-end
-function component_listener.onincoming(conn, data)
- local session = sessions[conn];
- session.data(conn, data);
end
+
function component_listener.ondisconnect(conn, err)
local session = sessions[conn];
if session then
(session.log or log)("info", "component disconnected: %s (%s)", tostring(session.host), tostring(err));
- if session.on_destroy then session:on_destroy(err); end
- sessions[conn] = nil;
+ if session.host then
+ log("debug", "Deregistering component");
+ cm_deregister_component(session.host);
+ hosts[session.host].connected = nil;
+ end
+ sessions[conn] = nil;
for k in pairs(session) do
if k ~= "log" and k ~= "close" then
session[k] = nil;
--
-local tostring = tostring;
-local type = type;
-local xpcall = xpcall;
-local s_format = string.format;
-local traceback = debug.traceback;
local logger = require "logger";
local log = logger.init("xmppserver_listener");
-local st = require "util.stanza";
-local connlisteners_register = require "net.connlisteners".register;
-local new_xmpp_stream = require "util.xmppstream".new;
+local lxp = require "lxp"
+local init_xmlhandlers = require "core.xmlhandlers"
local s2s_new_incoming = require "core.s2smanager".new_incoming;
local s2s_streamopened = require "core.s2smanager".streamopened;
local s2s_streamclosed = require "core.s2smanager".streamclosed;
session:close("invalid-namespace");
elseif error == "parse-error" then
session.log("debug", "Server-to-server XML parse error: %s", tostring(error));
- session:close("not-well-formed");
+ session:close("xml-not-well-formed");
elseif error == "stream-error" then
local condition, text = "undefined-condition";
for child in data:children() do
end
end
-local function handleerr(err) log("error", "Traceback[s2s]: %s: %s", tostring(err), traceback()); end
-function stream_callbacks.handlestanza(session, stanza)
- if stanza.attr.xmlns == "jabber:client" then --COMPAT: Prosody pre-0.6.2 may send jabber:client
- stanza.attr.xmlns = nil;
- end
- stanza = session.filter("stanzas/in", stanza);
- if stanza then
- return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
+local function handleerr(err) log("error", "Traceback[s2s]: %s: %s", tostring(err), debug.traceback()); end
+function stream_callbacks.handlestanza(a, b)
+ if b.attr.xmlns == "jabber:client" then --COMPAT: Prosody pre-0.6.2 may send jabber:client
+ b.attr.xmlns = nil;
end
+ xpcall(function () core_process_stanza(a, b) end, handleerr);
end
+local connlisteners_register = require "net.connlisteners".register;
+
+local t_insert = table.insert;
+local t_concat = table.concat;
+local t_concatall = function (t, sep) local tt = {}; for _, s in ipairs(t) do t_insert(tt, tostring(s)); end return t_concat(tt, sep); end
+local m_random = math.random;
+local format = string.format;
+local sessionmanager = require "core.sessionmanager";
+local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session;
+local st = require "util.stanza";
+
local sessions = {};
local xmppserver = { default_port = 5269, default_mode = "*a" };
-- These are session methods --
+local function session_reset_stream(session)
+ -- Reset stream
+ local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "\1");
+ session.parser = parser;
+
+ session.notopen = true;
+
+ function session.data(conn, data)
+ local ok, err = parser:parse(data);
+ if ok then return; end
+ (session.log or log)("warn", "Received invalid XML: %s", data);
+ (session.log or log)("warn", "Problem was: %s", err);
+ session:close("xml-not-well-formed");
+ end
+
+ return true;
+end
+
local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
local function session_close(session, reason, remote_reason)
-- End of session methods --
-local function initialize_session(session)
- local stream = new_xmpp_stream(session, stream_callbacks);
- session.stream = stream;
-
- session.notopen = true;
-
- function session.reset_stream()
- session.notopen = true;
- session.stream:reset();
- end
-
- local filter = session.filter;
- function session.data(data)
- data = filter("bytes/in", data);
- if data then
- local ok, err = stream:feed(data);
- if ok then return; end
- (session.log or log)("warn", "Received invalid XML: %s", data);
- (session.log or log)("warn", "Problem was: %s", err);
- session:close("not-well-formed");
- end
- end
-
- session.close = session_close;
- local handlestanza = stream_callbacks.handlestanza;
- function session.dispatch_stanza(session, stanza)
- return handlestanza(session, stanza);
- end
-end
-
-function xmppserver.onconnect(conn)
- if not sessions[conn] then -- May be an existing outgoing session
- local session = s2s_new_incoming(conn);
+function xmppserver.onincoming(conn, data)
+ local session = sessions[conn];
+ if not session then
+ session = s2s_new_incoming(conn);
sessions[conn] = session;
-
+
-- Logging functions --
+
+
local conn_name = "s2sin"..tostring(conn):match("[a-f0-9]+$");
session.log = logger.init(conn_name);
session.log("info", "Incoming s2s connection");
- initialize_session(session);
+ session.reset_stream = session_reset_stream;
+ session.close = session_close;
+
+ session_reset_stream(session); -- Initialise, ready for use
+
+ session.dispatch_stanza = stream_callbacks.handlestanza;
end
-end
-
-function xmppserver.onincoming(conn, data)
- local session = sessions[conn];
- if session then
- session.data(data);
+ if data then
+ session.data(conn, data);
end
end
if status == "ssl-handshake-complete" then
local session = sessions[conn];
if session and session.direction == "outgoing" then
- local to_host, from_host = session.to_host, session.from_host;
+ local format, to_host, from_host = string.format, session.to_host, session.from_host;
session.log("debug", "Sending stream header...");
- session.sends2s(s_format([[<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' from='%s' to='%s' version='1.0'>]], from_host, to_host));
+ session.sends2s(format([[<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' from='%s' to='%s' version='1.0'>]], from_host, to_host));
end
end
end
session.direction = "outgoing";
sessions[conn] = session;
- initialize_session(session);
+ session.reset_stream = session_reset_stream;
+ session.close = session_close;
+ session_reset_stream(session); -- Initialise, ready for use
+
+ --local function handleerr(err) print("Traceback:", err, debug.traceback()); end
+ --session.stanza_dispatch = function (stanza) return select(2, xpcall(function () return core_process_stanza(session, stanza); end, handleerr)); end
end
connlisteners_register("xmppserver", xmppserver);
-- COPYING file in the source package for more information.
--
-local st, jid = require "util.stanza", require "util.jid";
+local st, jid, set = require "util.stanza", require "util.jid", require "util.set";
local is_admin = require "core.usermanager".is_admin;
+local admins = set.new(config.get(module:get_host(), "core", "admins"));
-function send_to_online(message, host)
- local sessions;
- if host then
- sessions = { [host] = hosts[host] };
- else
- sessions = hosts;
- end
-
- local c = 0;
- for hostname, host_session in pairs(sessions) do
- if host_session.sessions then
- message.attr.from = hostname;
- for username in pairs(host_session.sessions) do
- c = c + 1;
- message.attr.to = username.."@"..hostname;
- core_post_stanza(host_session, message);
- end
- end
- end
-
- return c;
-end
-
-
--- Old <message>-based jabberd-style announcement sending
-function handle_announcement(event)
- local origin, stanza = event.origin, event.stanza;
- local node, host, resource = jid.split(stanza.attr.to);
+function handle_announcement(data)
+ local origin, stanza = data.origin, data.stanza;
+ local host, resource = select(2, jid.split(stanza.attr.to));
if resource ~= "announce/online" then
return; -- Not an announcement
if not is_admin(stanza.attr.from) then
-- Not an admin? Not allowed!
- module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from);
+ module:log("warn", "Non-admin %s tried to send server announcement", tostring(jid.bare(stanza.attr.from)));
return;
end
module:log("info", "Sending server announcement to all online users");
+ local host_session = hosts[host];
local message = st.clone(stanza);
message.attr.type = "headline";
message.attr.from = host;
- local c = send_to_online(message, host);
- module:log("info", "Announcement sent to %d online users", c);
- return true;
-end
-module:hook("message/host", handle_announcement);
-
--- Ad-hoc command (XEP-0133)
-local dataforms_new = require "util.dataforms".new;
-local announce_layout = dataforms_new{
- title = "Making an Announcement";
- instructions = "Fill out this form to make an announcement to all\nactive users of this service.";
-
- { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
- { name = "subject", type = "text-single", label = "Subject" };
- { name = "announcement", type = "text-multi", required = true, label = "Announcement" };
-};
-
-function announce_handler(self, data, state)
- if state then
- if data.action == "cancel" then
- return { status = "canceled" };
- end
-
- local fields = announce_layout:data(data.form);
-
- module:log("info", "Sending server announcement to all online users");
- local message = st.message({type = "headline"}, fields.announcement):up()
- :tag("subject"):text(fields.subject or "Announcement");
-
- local count = send_to_online(message, data.to);
-
- module:log("info", "Announcement sent to %d online users", count);
- return { status = "completed", info = ("Announcement sent to %d online users"):format(count) };
- else
- return { status = "executing", form = announce_layout }, "executing";
+ local c = 0;
+ for user in pairs(host_session.sessions) do
+ c = c + 1;
+ message.attr.to = user.."@"..host;
+ core_post_stanza(host_session, message);
end
-
+
+ module:log("info", "Announcement sent to %d online users", c);
return true;
end
-local adhoc_new = module:require "adhoc".new;
-local announce_desc = adhoc_new("Send Announcement to Online Users", "http://jabber.org/protocol/admin#announce", announce_handler, "admin");
-module:add_item("adhoc", announce_desc);
-
+module:hook("message/host", handle_announcement);
local hosts = _G.hosts;
local lxp = require "lxp";
-local new_xmpp_stream = require "util.xmppstream".new;
+local init_xmlhandlers = require "core.xmlhandlers"
+local server = require "net.server";
local httpserver = require "net.httpserver";
local sm = require "core.sessionmanager";
local sm_destroy_session = sm.destroy_session;
local new_uuid = require "util.uuid".generate;
-local fire_event = prosody.events.fire_event;
+local fire_event = require "core.eventmanager".fire_event;
local core_process_stanza = core_process_stanza;
local st = require "util.stanza";
local logger = require "util.logger";
local log = logger.init("mod_bosh");
-local timer = require "util.timer";
-local xmlns_streams = "http://etherx.jabber.org/streams";
-local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
local xmlns_bosh = "http://jabber.org/protocol/httpbind"; -- (hard-coded into a literal in session.send)
-
-local stream_callbacks = {
- stream_ns = xmlns_bosh, stream_tag = "body", default_ns = "jabber:client" };
+local stream_callbacks = { stream_ns = "http://jabber.org/protocol/httpbind", stream_tag = "body", default_ns = "jabber:client" };
local BOSH_DEFAULT_HOLD = tonumber(module:get_option("bosh_default_hold")) or 1;
local BOSH_DEFAULT_INACTIVITY = tonumber(module:get_option("bosh_max_inactivity")) or 60;
local BOSH_DEFAULT_POLLING = tonumber(module:get_option("bosh_max_polling")) or 5;
local BOSH_DEFAULT_REQUESTS = tonumber(module:get_option("bosh_max_requests")) or 2;
+local BOSH_DEFAULT_MAXPAUSE = tonumber(module:get_option("bosh_max_pause")) or 300;
local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure");
local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" };
+local session_close_reply = { headers = default_headers, body = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate" }), attr = {} };
local cross_domain = module:get_option("cross_domain_bosh");
if cross_domain then
end
end
-local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items;
-
-local function get_ip_from_request(request)
- local ip = request.handler:ip();
- local forwarded_for = request.headers["x-forwarded-for"];
- if forwarded_for then
- forwarded_for = forwarded_for..", "..ip;
- for forwarded_ip in forwarded_for:gmatch("[^%s,]+") do
- if not trusted_proxies[forwarded_ip] then
- ip = forwarded_ip;
- end
- end
- end
- return ip;
-end
-
local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
local os_time = os.time;
function handle_request(method, body, request)
if (not body) or request.method ~= "POST" then
if request.method == "OPTIONS" then
- local headers = {};
- for k,v in pairs(default_headers) do headers[k] = v; end
- headers["Content-Type"] = nil;
- return { headers = headers, body = "" };
+ return { headers = default_headers, body = "" };
else
return "<html><body>You really don't look like a BOSH client to me... what do you want?</body></html>";
end
request.log = log;
request.on_destroy = on_destroy_request;
- local stream = new_xmpp_stream(request, stream_callbacks);
- -- stream:feed() calls the stream_callbacks, so all stanzas in
- -- the body are processed in this next line before it returns.
- stream:feed(body);
+ local parser = lxp.new(init_xmlhandlers(request, stream_callbacks), "\1");
+
+ parser:parse(body);
local session = sessions[request.sid];
if session then
- -- Session was marked as inactive, since we have
- -- a request open now, unmark it
- if inactive_sessions[session] then
- inactive_sessions[session] = nil;
- end
-
local r = session.requests;
log("debug", "Session %s has %d out of %d requests open", request.sid, #r, session.bosh_hold);
log("debug", "and there are %d things in the send_buffer", #session.send_buffer);
request.reply_before = os_time() + session.bosh_wait;
waiting_requests[request] = true;
end
+ if inactive_sessions[session] then
+ -- Session was marked as inactive, since we have
+ -- a request open now, unmark it
+ inactive_sessions[session] = nil;
+ end
end
- if session.bosh_terminate then
- session.log("debug", "Closing session with %d requests open", #session.requests);
- session:close();
- return nil;
- else
- return true; -- Inform httpserver we shall reply later
- end
+ return true; -- Inform httpserver we shall reply later
end
end
local function bosh_reset_stream(session) session.notopen = true; end
-local stream_xmlns_attr = { xmlns = "urn:ietf:params:xml:ns:xmpp-streams" };
-
local function bosh_close_stream(session, reason)
(session.log or log)("info", "BOSH client disconnected");
-
- local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
- ["xmlns:streams"] = xmlns_streams });
-
-
- if reason then
- close_reply.attr.condition = "remote-stream-error";
- if type(reason) == "string" then -- assume stream error
- close_reply:tag("stream:error")
- :tag(reason, {xmlns = xmlns_xmpp_streams});
- elseif type(reason) == "table" then
- if reason.condition then
- close_reply:tag("stream:error")
- :tag(reason.condition, stream_xmlns_attr):up();
- if reason.text then
- close_reply:tag("text", stream_xmlns_attr):text(reason.text):up();
- end
- if reason.extra then
- close_reply:add_child(reason.extra);
- end
- elseif reason.name then -- a stanza
- close_reply = reason;
- end
- end
- log("info", "Disconnecting client, <stream:error> is: %s", tostring(close_reply));
- end
-
- local session_close_response = { headers = default_headers, body = tostring(close_reply) };
-
+ session_close_reply.attr.condition = reason;
for _, held_request in ipairs(session.requests) do
- held_request:send(session_close_response);
+ held_request:send(session_close_reply);
held_request:destroy();
end
sessions[session.sid] = nil;
if not hosts[attr.to] then
-- Unknown host
log("debug", "BOSH client tried to connect to unknown host: %s", tostring(attr.to));
- local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
- ["xmlns:streams"] = xmlns_streams, condition = "host-unknown" });
- request:send(tostring(close_reply));
+ session_close_reply.body.attr.condition = "host-unknown";
+ request:send(session_close_reply);
+ request.notopen = nil
return;
end
-- New session
sid = new_uuid();
local session = {
- type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid), host = attr.to,
+ type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid)-1, host = attr.to,
bosh_version = attr.ver, bosh_wait = attr.wait, streamid = sid,
bosh_hold = BOSH_DEFAULT_HOLD, bosh_max_inactive = BOSH_DEFAULT_INACTIVITY,
requests = { }, send_buffer = {}, reset_stream = bosh_reset_stream,
close = bosh_close_stream, dispatch_stanza = core_process_stanza,
- log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure,
- ip = get_ip_from_request(request);
+ log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure
};
sessions[sid] = session;
- session.log("debug", "BOSH session created for request from %s", session.ip);
log("info", "New BOSH session, assigned it sid '%s'", sid);
local r, send_buffer = session.requests, session.send_buffer;
local response = { headers = default_headers }
function session.send(s)
- -- We need to ensure that outgoing stanzas have the jabber:client xmlns
- if s.attr and not s.attr.xmlns then
- s = st.clone(s);
- s.attr.xmlns = "jabber:client";
- end
--log("debug", "Sending BOSH data: %s", tostring(s));
local oldest_request = r[1];
if oldest_request then
log("debug", "We have an open request, so sending on that");
- response.body = t_concat({
- "<body xmlns='http://jabber.org/protocol/httpbind' ",
- session.bosh_terminate and "type='terminate' " or "",
- "sid='", sid, "' xmlns:stream = 'http://etherx.jabber.org/streams'>",
- tostring(s),
- "</body>"
- });
+ response.body = t_concat{"<body xmlns='http://jabber.org/protocol/httpbind' sid='", sid, "' xmlns:stream = 'http://etherx.jabber.org/streams'>", tostring(s), "</body>" };
oldest_request:send(response);
--log("debug", "Sent");
if oldest_request.stayopen then
t_insert(session.send_buffer, tostring(s));
log("debug", "There are now %d things in the send_buffer", #session.send_buffer);
end
- return true;
end
-- Send creation response
fire_event("stream-features", session, features);
--xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh'
local response = st.stanza("body", { xmlns = xmlns_bosh,
- wait = attr.wait,
- inactivity = tostring(BOSH_DEFAULT_INACTIVITY),
- polling = tostring(BOSH_DEFAULT_POLLING),
- requests = tostring(BOSH_DEFAULT_REQUESTS),
- hold = tostring(session.bosh_hold),
- sid = sid, authid = sid,
- ver = '1.6', from = session.host,
- secure = 'true', ["xmpp:version"] = "1.0",
- ["xmlns:xmpp"] = "urn:xmpp:xbosh",
- ["xmlns:stream"] = "http://etherx.jabber.org/streams"
- }):add_child(features);
+ inactivity = tostring(BOSH_DEFAULT_INACTIVITY), polling = tostring(BOSH_DEFAULT_POLLING), requests = tostring(BOSH_DEFAULT_REQUESTS), hold = tostring(session.bosh_hold), maxpause = "120",
+ sid = sid, authid = sid, ver = '1.6', from = session.host, secure = 'true', ["xmpp:version"] = "1.0",
+ ["xmlns:xmpp"] = "urn:xmpp:xbosh", ["xmlns:stream"] = "http://etherx.jabber.org/streams" }):add_child(features);
request:send{ headers = default_headers, body = tostring(response) };
request.sid = sid;
-- Repeated, ignore
session.log("debug", "rid repeated (on request %s), ignoring: %s (diff %d)", request.id, session.rid, diff);
request.notopen = nil;
- request.ignore = true;
request.sid = sid;
t_insert(session.requests, request);
return;
session.rid = rid;
end
+ if attr.type == "terminate" then
+ -- Client wants to end this session
+ session:close();
+ request.notopen = nil;
+ return;
+ end
+
if session.notopen then
local features = st.stanza("stream:features");
hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
session.notopen = nil;
end
- if attr.type == "terminate" then
- -- Client wants to end this session, which we'll do
- -- after processing any stanzas in this request
- session.bosh_terminate = true;
- end
-
request.notopen = nil; -- Signals that we accept this opening tag
t_insert(session.requests, request);
request.sid = sid;
end
function stream_callbacks.handlestanza(request, stanza)
- if request.ignore then return; end
log("debug", "BOSH stanza received: %s\n", stanza:top_tag());
local session = sessions[request.sid];
if session then
if stanza.attr.xmlns == xmlns_bosh then
stanza.attr.xmlns = nil;
end
+ session.ip = request.handler:ip();
core_process_stanza(session, stanza);
end
end
-function stream_callbacks.error(request, error)
- log("debug", "Error parsing BOSH request payload; %s", error);
- if not request.sid then
- request:send({ headers = default_headers, status = "400 Bad Request" });
- return;
- end
-
- local session = sessions[request.sid];
- if error == "stream-error" then -- Remote stream error, we close normally
- session:close();
- else
- session:close({ condition = "bad-format", text = "Error processing stream" });
- end
-end
-
local dead_sessions = {};
function on_timer()
-- log("debug", "Checking for requests soon to timeout...");
dead_sessions[i] = nil;
sm_destroy_session(session, "BOSH client silent for over "..session.bosh_max_inactive.." seconds");
end
- return 1;
end
local function setup()
local ports = module:get_option("bosh_ports") or { 5280 };
httpserver.new_from_config(ports, handle_request, { base = "http-bind" });
- timer.add_task(1, on_timer);
+ server.addtimer(on_timer);
end
if prosody.start_time then -- already started
setup();
local t_concat = table.concat;
+local config = require "core.configmanager";
+local cm_register_component = require "core.componentmanager".register_component;
+local cm_deregister_component = require "core.componentmanager".deregister_component;
local sha1 = require "util.hashes".sha1;
local st = require "util.stanza";
local log = module._log;
-local main_session, send;
-
-local function on_destroy(session, err)
- if main_session == session then
- main_session = nil;
- send = nil;
- session.on_destroy = nil;
- end
-end
-
-local function handle_stanza(event)
- local stanza = event.stanza;
- if send then
- stanza.attr.xmlns = nil;
- send(stanza);
- else
- log("warn", "Stanza being handled by default component; bouncing error for: %s", stanza:top_tag());
- if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
- event.origin.send(st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable"));
- end
- end
- return true;
-end
-
-module:hook("iq/bare", handle_stanza, -1);
-module:hook("message/bare", handle_stanza, -1);
-module:hook("presence/bare", handle_stanza, -1);
-module:hook("iq/full", handle_stanza, -1);
-module:hook("message/full", handle_stanza, -1);
-module:hook("presence/full", handle_stanza, -1);
-module:hook("iq/host", handle_stanza, -1);
-module:hook("message/host", handle_stanza, -1);
-module:hook("presence/host", handle_stanza, -1);
-
--- Handle authentication attempts by components
-function handle_component_auth(event)
- local session, stanza = event.origin, event.stanza;
-
- if session.type ~= "component" then return; end
- if main_session == session then return; end
-
+function handle_component_auth(session, stanza)
+ log("info", "Handling component auth");
if (not session.host) or #stanza.tags > 0 then
- (session.log or log)("warn", "Invalid component handshake for host: %s", session.host);
+ (session.log or log)("warn", "Component handshake invalid");
session:close("not-authorized");
- return true;
+ return;
end
- local secret = module:get_option("component_secret");
+ local secret = config.get(session.user, "core", "component_secret");
if not secret then
- (session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.host);
+ (session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.user);
session:close("not-authorized");
- return true;
+ return;
end
local supplied_token = t_concat(stanza);
local calculated_token = sha1(session.streamid..secret, true);
if supplied_token:lower() ~= calculated_token:lower() then
- log("info", "Component authentication failed for %s", session.host);
+ log("info", "Component for %s authentication failed", session.host);
session:close{ condition = "not-authorized", text = "Given token does not match calculated token" };
- return true;
+ return;
end
+
+ -- Authenticated now
+ log("info", "Component authenticated: %s", session.host);
+
-- If component not already created for this host, create one now
- if not main_session then
- send = session.send;
- main_session = session;
- session.on_destroy = on_destroy;
- session.component_validate_from = module:get_option_boolean("validate_from_addresses") ~= false;
- log("info", "Component successfully authenticated: %s", session.host);
- session.send(st.stanza("handshake"));
- else -- TODO: Implement stanza distribution
- log("error", "Multiple components bound to the same address, first one wins: %s", session.host);
- session:close{ condition = "conflict", text = "Component already connected" };
+ if not hosts[session.host].connected then
+ local send = session.send;
+ session.component_session = cm_register_component(session.host, function (_, data)
+ if data.attr and data.attr.xmlns == "jabber:client" then
+ data.attr.xmlns = nil;
+ end
+ return send(data);
+ end);
+ hosts[session.host].connected = true;
+ log("info", "Component successfully registered");
+ else
+ log("error", "Multiple components bound to the same address, first one wins (TODO: Implement stanza distribution)");
end
- return true;
+ -- Signal successful authentication
+ session.send(st.stanza("handshake"));
end
-module:hook("stanza/jabber:component:accept:handshake", handle_component_auth);
+module:add_handler("component", "handshake", "jabber:component:accept", handle_component_auth);
local xmlns_compression_protocol = "http://jabber.org/protocol/compress"
local xmlns_stream = "http://etherx.jabber.org/streams";
local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up();
-local add_filter = require "util.filters".add_filter;
local compression_level = module:get_option("compression_level");
-- if not defined assume admin wants best compression
module:hook("s2s-stream-features", function(event)
local origin, features = event.origin, event.features;
-- FIXME only advertise compression support when TLS layer has no compression enabled
- if not origin.compressed then
+ if not origin.compressed then
features:add_child(compression_stream_feature);
end
end);
-- setup compression for a stream
local function setup_compression(session, deflate_stream)
- add_filter(session, "bytes/out", function(t)
- local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
- if status == false then
- module:log("warn", "%s", tostring(compressed));
- session:close({
- condition = "undefined-condition";
- text = compressed;
- extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
- });
- return;
- end
- return compressed;
- end);
+ local old_send = (session.sends2s or session.send);
+
+ local new_send = function(t)
+ --TODO: Better code injection in the sending process
+ local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
+ if status == false then
+ session:close({
+ condition = "undefined-condition";
+ text = compressed;
+ extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
+ });
+ module:log("warn", "%s", tostring(compressed));
+ return;
+ end
+ session.conn:write(compressed);
+ end;
+
+ if session.sends2s then session.sends2s = new_send
+ elseif session.send then session.send = new_send end
end
-- setup decompression for a stream
local function setup_decompression(session, inflate_stream)
- add_filter(session, "bytes/in", function(data)
- local status, decompressed, eof = pcall(inflate_stream, data);
- if status == false then
- module:log("warn", "%s", tostring(decompressed));
- session:close({
- condition = "undefined-condition";
- text = decompressed;
- extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
- });
- return;
- end
- return decompressed;
- end);
+ local old_data = session.data
+ session.data = function(conn, data)
+ local status, decompressed, eof = pcall(inflate_stream, data);
+ if status == false then
+ session:close({
+ condition = "undefined-condition";
+ text = decompressed;
+ extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
+ });
+ module:log("warn", "%s", tostring(decompressed));
+ return;
+ end
+ old_data(conn, decompressed);
+ end;
end
-module:hook("stanza/http://jabber.org/protocol/compress:compressed", function(event)
- local session = event.origin;
-
- if session.type == "s2sout_unauthed" or session.type == "s2sout" then
- session.log("debug", "Activating compression...")
- -- create deflate and inflate streams
- local deflate_stream = get_deflate_stream(session);
- if not deflate_stream then return true; end
-
- local inflate_stream = get_inflate_stream(session);
- if not inflate_stream then return true; end
-
- -- setup compression for session.w
- setup_compression(session, deflate_stream);
-
- -- setup decompression for session.data
- setup_decompression(session, inflate_stream);
- session:reset_stream();
- local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams",
- ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host};
- session.sends2s("<?xml version='1.0'?>");
- session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag());
- session.compressed = true;
- return true;
- end
-end);
-
-module:hook("stanza/http://jabber.org/protocol/compress:compress", function(event)
- local session, stanza = event.origin, event.stanza;
-
- if session.type == "c2s" or session.type == "s2sin" or session.type == "c2s_unauthed" or session.type == "s2sin_unauthed" then
- -- fail if we are already compressed
- if session.compressed then
- local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
- (session.sends2s or session.send)(error_st);
- session.log("debug", "Client tried to establish another compression layer.");
- return true;
- end
-
- -- checking if the compression method is supported
- local method = stanza:child_with_name("method");
- method = method and (method[1] or "");
- if method == "zlib" then
- session.log("debug", "zlib compression enabled.");
-
+module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compression_protocol,
+ function(session ,stanza)
+ session.log("debug", "Activating compression...")
-- create deflate and inflate streams
local deflate_stream = get_deflate_stream(session);
- if not deflate_stream then return true; end
+ if not deflate_stream then return end
local inflate_stream = get_inflate_stream(session);
- if not inflate_stream then return true; end
-
- (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
- session:reset_stream();
+ if not inflate_stream then return end
-- setup compression for session.w
setup_compression(session, deflate_stream);
-- setup decompression for session.data
setup_decompression(session, inflate_stream);
-
+ local session_reset_stream = session.reset_stream;
+ session.reset_stream = function(session)
+ session_reset_stream(session);
+ setup_decompression(session, inflate_stream);
+ return true;
+ end;
+ session:reset_stream();
+ local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams",
+ ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host};
+ session.sends2s("<?xml version='1.0'?>");
+ session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag());
session.compressed = true;
- elseif method then
- session.log("debug", "%s compression selected, but we don't support it.", tostring(method));
- local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method");
- (session.sends2s or session.send)(error_st);
- else
- (session.sends2s or session.send)(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"));
end
- return true;
- end
-end);
+);
+
+module:add_handler({"c2s_unauthed", "c2s", "s2sin_unauthed", "s2sin"}, "compress", xmlns_compression_protocol,
+ function(session, stanza)
+ -- fail if we are already compressed
+ if session.compressed then
+ local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
+ (session.sends2s or session.send)(error_st);
+ session.log("debug", "Client tried to establish another compression layer.");
+ return;
+ end
+
+ -- checking if the compression method is supported
+ local method = stanza:child_with_name("method");
+ method = method and (method[1] or "");
+ if method == "zlib" then
+ session.log("debug", "zlib compression enabled.");
+
+ -- create deflate and inflate streams
+ local deflate_stream = get_deflate_stream(session);
+ if not deflate_stream then return end
+
+ local inflate_stream = get_inflate_stream(session);
+ if not inflate_stream then return end
+
+ (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
+ session:reset_stream();
+
+ -- setup compression for session.w
+ setup_compression(session, deflate_stream);
+
+ -- setup decompression for session.data
+ setup_decompression(session, inflate_stream);
+
+ local session_reset_stream = session.reset_stream;
+ session.reset_stream = function(session)
+ session_reset_stream(session);
+ setup_decompression(session, inflate_stream);
+ return true;
+ end;
+ session.compressed = true;
+ elseif method then
+ session.log("debug", "%s compression selected, but we don't support it.", tostring(method));
+ local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method");
+ (session.sends2s or session.send)(error_st);
+ else
+ (session.sends2s or session.send)(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"));
+ end
+ end
+);
--- /dev/null
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+module.host = "*";
+
+local _G = _G;
+
+local prosody = _G.prosody;
+local hosts = prosody.hosts;
+local connlisteners_register = require "net.connlisteners".register;
+
+local console_listener = { default_port = 5582; default_mode = "*l"; default_interface = "127.0.0.1" };
+
+require "util.iterators";
+local jid_bare = require "util.jid".bare;
+local set, array = require "util.set", require "util.array";
+
+local commands = {};
+local def_env = {};
+local default_env_mt = { __index = def_env };
+
+prosody.console = { commands = commands, env = def_env };
+
+local function redirect_output(_G, session)
+ return setmetatable({ print = session.print }, { __index = function (t, k) return rawget(_G, k); end, __newindex = function (t, k, v) rawset(_G, k, v); end });
+end
+
+console = {};
+
+function console:new_session(conn)
+ local w = function(s) conn:write(s:gsub("\n", "\r\n")); end;
+ local session = { conn = conn;
+ send = function (t) w(tostring(t)); end;
+ print = function (t) w("| "..tostring(t).."\n"); end;
+ disconnect = function () conn:close(); end;
+ };
+ session.env = setmetatable({}, default_env_mt);
+
+ -- Load up environment with helper objects
+ for name, t in pairs(def_env) do
+ if type(t) == "table" then
+ session.env[name] = setmetatable({ session = session }, { __index = t });
+ end
+ end
+
+ return session;
+end
+
+local sessions = {};
+
+function console_listener.onconnect(conn)
+ -- Handle new connection
+ local session = console:new_session(conn);
+ sessions[conn] = session;
+ printbanner(session);
+ session.send(string.char(0));
+end
+
+function console_listener.onincoming(conn, data)
+ local session = sessions[conn];
+
+ -- Handle data
+ (function(session, data)
+ local useglobalenv;
+
+ if data:match("^>") then
+ data = data:gsub("^>", "");
+ useglobalenv = true;
+ elseif data == "\004" then
+ commands["bye"](session, data);
+ return;
+ else
+ local command = data:lower();
+ command = data:match("^%w+") or data:match("%p");
+ if commands[command] then
+ commands[command](session, data);
+ return;
+ end
+ end
+
+ session.env._ = data;
+
+ local chunkname = "=console";
+ local chunk, err = loadstring("return "..data, chunkname);
+ if not chunk then
+ chunk, err = loadstring(data, chunkname);
+ if not chunk then
+ err = err:gsub("^%[string .-%]:%d+: ", "");
+ err = err:gsub("^:%d+: ", "");
+ err = err:gsub("'<eof>'", "the end of the line");
+ session.print("Sorry, I couldn't understand that... "..err);
+ return;
+ end
+ end
+
+ setfenv(chunk, (useglobalenv and redirect_output(_G, session)) or session.env or nil);
+
+ local ranok, taskok, message = pcall(chunk);
+
+ if not (ranok or message or useglobalenv) and commands[data:lower()] then
+ commands[data:lower()](session, data);
+ return;
+ end
+
+ if not ranok then
+ session.print("Fatal error while running command, it did not complete");
+ session.print("Error: "..taskok);
+ return;
+ end
+
+ if not message then
+ session.print("Result: "..tostring(taskok));
+ return;
+ elseif (not taskok) and message then
+ session.print("Command completed with a problem");
+ session.print("Message: "..tostring(message));
+ return;
+ end
+
+ session.print("OK: "..tostring(message));
+ end)(session, data);
+
+ session.send(string.char(0));
+end
+
+function console_listener.ondisconnect(conn, err)
+ local session = sessions[conn];
+ if session then
+ session.disconnect();
+ sessions[conn] = nil;
+ end
+end
+
+connlisteners_register('console', console_listener);
+
+-- Console commands --
+-- These are simple commands, not valid standalone in Lua
+
+function commands.bye(session)
+ session.print("See you! :)");
+ session.disconnect();
+end
+commands.quit, commands.exit = commands.bye, commands.bye;
+
+commands["!"] = function (session, data)
+ if data:match("^!!") then
+ session.print("!> "..session.env._);
+ return console_listener.onincoming(session.conn, session.env._);
+ end
+ local old, new = data:match("^!(.-[^\\])!(.-)!$");
+ if old and new then
+ local ok, res = pcall(string.gsub, session.env._, old, new);
+ if not ok then
+ session.print(res)
+ return;
+ end
+ session.print("!> "..res);
+ return console_listener.onincoming(session.conn, res);
+ end
+ session.print("Sorry, not sure what you want");
+end
+
+function commands.help(session, data)
+ local print = session.print;
+ local section = data:match("^help (%w+)");
+ if not section then
+ print [[Commands are divided into multiple sections. For help on a particular section, ]]
+ print [[type: help SECTION (for example, 'help c2s'). Sections are: ]]
+ print [[]]
+ print [[c2s - Commands to manage local client-to-server sessions]]
+ print [[s2s - Commands to manage sessions between this server and others]]
+ print [[module - Commands to load/reload/unload modules/plugins]]
+ print [[server - Uptime, version, shutting down, etc.]]
+ print [[config - Reloading the configuration, etc.]]
+ print [[console - Help regarding the console itself]]
+ elseif section == "c2s" then
+ print [[c2s:show(jid) - Show all client sessions with the specified JID (or all if no JID given)]]
+ print [[c2s:show_insecure() - Show all unencrypted client connections]]
+ print [[c2s:show_secure() - Show all encrypted client connections]]
+ print [[c2s:close(jid) - Close all sessions for the specified JID]]
+ elseif section == "s2s" then
+ print [[s2s:show(domain) - Show all s2s connections for the given domain (or all if no domain given)]]
+ print [[s2s:close(from, to) - Close a connection from one domain to another]]
+ elseif section == "module" then
+ print [[module:load(module, host) - Load the specified module on the specified host (or all hosts if none given)]]
+ print [[module:reload(module, host) - The same, but unloads and loads the module (saving state if the module supports it)]]
+ print [[module:unload(module, host) - The same, but just unloads the module from memory]]
+ print [[module:list(host) - List the modules loaded on the specified host]]
+ elseif section == "server" then
+ print [[server:version() - Show the server's version number]]
+ print [[server:uptime() - Show how long the server has been running]]
+ print [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]]
+ elseif section == "config" then
+ print [[config:reload() - Reload the server configuration. Modules may need to be reloaded for changes to take effect.]]
+ elseif section == "console" then
+ print [[Hey! Welcome to Prosody's admin console.]]
+ print [[First thing, if you're ever wondering how to get out, simply type 'quit'.]]
+ print [[Secondly, note that we don't support the full telnet protocol yet (it's coming)]]
+ print [[so you may have trouble using the arrow keys, etc. depending on your system.]]
+ print [[]]
+ print [[For now we offer a couple of handy shortcuts:]]
+ print [[!! - Repeat the last command]]
+ print [[!old!new! - repeat the last command, but with 'old' replaced by 'new']]
+ print [[]]
+ print [[For those well-versed in Prosody's internals, or taking instruction from those who are,]]
+ print [[you can prefix a command with > to escape the console sandbox, and access everything in]]
+ print [[the running server. Great fun, but be careful not to break anything :)]]
+ end
+ print [[]]
+end
+
+-- Session environment --
+-- Anything in def_env will be accessible within the session as a global variable
+
+def_env.server = {};
+
+function def_env.server:insane_reload()
+ prosody.unlock_globals();
+ dofile "prosody"
+ prosody = _G.prosody;
+ return true, "Server reloaded";
+end
+
+function def_env.server:version()
+ return true, tostring(prosody.version or "unknown");
+end
+
+function def_env.server:uptime()
+ local t = os.time()-prosody.start_time;
+ local seconds = t%60;
+ t = (t - seconds)/60;
+ local minutes = t%60;
+ t = (t - minutes)/60;
+ local hours = t%24;
+ t = (t - hours)/24;
+ local days = t;
+ return true, string.format("This server has been running for %d day%s, %d hour%s and %d minute%s (since %s)",
+ days, (days ~= 1 and "s") or "", hours, (hours ~= 1 and "s") or "",
+ minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time));
+end
+
+function def_env.server:shutdown(reason)
+ prosody.shutdown(reason);
+ return true, "Shutdown initiated";
+end
+
+def_env.module = {};
+
+local function get_hosts_set(hosts, module)
+ if type(hosts) == "table" then
+ if hosts[1] then
+ return set.new(hosts);
+ elseif hosts._items then
+ return hosts;
+ end
+ elseif type(hosts) == "string" then
+ return set.new { hosts };
+ elseif hosts == nil then
+ local mm = require "modulemanager";
+ return set.new(array.collect(keys(prosody.hosts)))
+ / function (host) return prosody.hosts[host].type == "local" or module and mm.is_loaded(host, module); end;
+ end
+end
+
+function def_env.module:load(name, hosts, config)
+ local mm = require "modulemanager";
+
+ hosts = get_hosts_set(hosts);
+
+ -- Load the module for each host
+ local ok, err, count = true, nil, 0;
+ for host in hosts do
+ if (not mm.is_loaded(host, name)) then
+ ok, err = mm.load(host, name, config);
+ if not ok then
+ ok = false;
+ self.session.print(err or "Unknown error loading module");
+ else
+ count = count + 1;
+ self.session.print("Loaded for "..host);
+ end
+ end
+ end
+
+ return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
+end
+
+function def_env.module:unload(name, hosts)
+ local mm = require "modulemanager";
+
+ hosts = get_hosts_set(hosts, name);
+
+ -- Unload the module for each host
+ local ok, err, count = true, nil, 0;
+ for host in hosts do
+ if mm.is_loaded(host, name) then
+ ok, err = mm.unload(host, name);
+ if not ok then
+ ok = false;
+ self.session.print(err or "Unknown error unloading module");
+ else
+ count = count + 1;
+ self.session.print("Unloaded from "..host);
+ end
+ end
+ end
+ return ok, (ok and "Module unloaded from "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
+end
+
+function def_env.module:reload(name, hosts)
+ local mm = require "modulemanager";
+
+ hosts = get_hosts_set(hosts, name);
+
+ -- Reload the module for each host
+ local ok, err, count = true, nil, 0;
+ for host in hosts do
+ if mm.is_loaded(host, name) then
+ ok, err = mm.reload(host, name);
+ if not ok then
+ ok = false;
+ self.session.print(err or "Unknown error reloading module");
+ else
+ count = count + 1;
+ if ok == nil then
+ ok = true;
+ end
+ self.session.print("Reloaded on "..host);
+ end
+ end
+ end
+ return ok, (ok and "Module reloaded on "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
+end
+
+function def_env.module:list(hosts)
+ if hosts == nil then
+ hosts = array.collect(keys(prosody.hosts));
+ end
+ if type(hosts) == "string" then
+ hosts = { hosts };
+ end
+ if type(hosts) ~= "table" then
+ return false, "Please supply a host or a list of hosts you would like to see";
+ end
+
+ local print = self.session.print;
+ for _, host in ipairs(hosts) do
+ print(host..":");
+ local modules = array.collect(keys(prosody.hosts[host] and prosody.hosts[host].modules or {})):sort();
+ if #modules == 0 then
+ if prosody.hosts[host] then
+ print(" No modules loaded");
+ else
+ print(" Host not found");
+ end
+ else
+ for _, name in ipairs(modules) do
+ print(" "..name);
+ end
+ end
+ end
+end
+
+def_env.config = {};
+function def_env.config:load(filename, format)
+ local config_load = require "core.configmanager".load;
+ local ok, err = config_load(filename, format);
+ if not ok then
+ return false, err or "Unknown error loading config";
+ end
+ return true, "Config loaded";
+end
+
+function def_env.config:get(host, section, key)
+ local config_get = require "core.configmanager".get
+ return true, tostring(config_get(host, section, key));
+end
+
+function def_env.config:reload()
+ local ok, err = prosody.reload_config();
+ return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err);
+end
+
+def_env.hosts = {};
+function def_env.hosts:list()
+ for host, host_session in pairs(hosts) do
+ self.session.print(host);
+ end
+ return true, "Done";
+end
+
+function def_env.hosts:add(name)
+end
+
+def_env.c2s = {};
+
+local function show_c2s(callback)
+ for hostname, host in pairs(hosts) do
+ for username, user in pairs(host.sessions or {}) do
+ for resource, session in pairs(user.sessions or {}) do
+ local jid = username.."@"..hostname.."/"..resource;
+ callback(jid, session);
+ end
+ end
+ end
+end
+
+function def_env.c2s:show(match_jid)
+ local print, count = self.session.print, 0;
+ local curr_host;
+ show_c2s(function (jid, session)
+ if curr_host ~= session.host then
+ curr_host = session.host;
+ print(curr_host);
+ end
+ if (not match_jid) or jid:match(match_jid) then
+ count = count + 1;
+ local status, priority = "unavailable", tostring(session.priority or "-");
+ if session.presence then
+ status = session.presence:child_with_name("show");
+ if status then
+ status = status:get_text() or "[invalid!]";
+ else
+ status = "available";
+ end
+ end
+ print(" "..jid.." - "..status.."("..priority..")");
+ end
+ end);
+ return true, "Total: "..count.." clients";
+end
+
+function def_env.c2s:show_insecure(match_jid)
+ local print, count = self.session.print, 0;
+ show_c2s(function (jid, session)
+ if ((not match_jid) or jid:match(match_jid)) and not session.secure then
+ count = count + 1;
+ print(jid);
+ end
+ end);
+ return true, "Total: "..count.." insecure client connections";
+end
+
+function def_env.c2s:show_secure(match_jid)
+ local print, count = self.session.print, 0;
+ show_c2s(function (jid, session)
+ if ((not match_jid) or jid:match(match_jid)) and session.secure then
+ count = count + 1;
+ print(jid);
+ end
+ end);
+ return true, "Total: "..count.." secure client connections";
+end
+
+function def_env.c2s:close(match_jid)
+ local print, count = self.session.print, 0;
+ show_c2s(function (jid, session)
+ if jid == match_jid or jid_bare(jid) == match_jid then
+ count = count + 1;
+ session:close();
+ end
+ end);
+ return true, "Total: "..count.." sessions closed";
+end
+
+def_env.s2s = {};
+function def_env.s2s:show(match_jid)
+ local _print = self.session.print;
+ local print = self.session.print;
+
+ local count_in, count_out = 0,0;
+
+ for host, host_session in pairs(hosts) do
+ print = function (...) _print(host); _print(...); print = _print; end
+ for remotehost, session in pairs(host_session.s2sout) do
+ if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then
+ count_out = count_out + 1;
+ print(" "..host.." -> "..remotehost..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
+ if session.sendq then
+ print(" There are "..#session.sendq.." queued outgoing stanzas for this connection");
+ end
+ if session.type == "s2sout_unauthed" then
+ if session.connecting then
+ print(" Connection not yet established");
+ if not session.srv_hosts then
+ if not session.conn then
+ print(" We do not yet have a DNS answer for this host's SRV records");
+ else
+ print(" This host has no SRV records, using A record instead");
+ end
+ elseif session.srv_choice then
+ print(" We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts);
+ local srv_choice = session.srv_hosts[session.srv_choice];
+ print(" Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269));
+ end
+ elseif session.notopen then
+ print(" The <stream> has not yet been opened");
+ elseif not session.dialback_key then
+ print(" Dialback has not been initiated yet");
+ elseif session.dialback_key then
+ print(" Dialback has been requested, but no result received");
+ end
+ end
+ end
+ end
+ local subhost_filter = function (h)
+ return (match_jid and h:match(match_jid));
+ end
+ for session in pairs(incoming_s2s) do
+ if session.to_host == host and ((not match_jid) or host:match(match_jid)
+ or (session.from_host and session.from_host:match(match_jid))
+ -- Pft! is what I say to list comprehensions
+ or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then
+ count_in = count_in + 1;
+ print(" "..host.." <- "..(session.from_host or "(unknown)")..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
+ if session.type == "s2sin_unauthed" then
+ print(" Connection not yet authenticated");
+ end
+ for name in pairs(session.hosts) do
+ if name ~= session.from_host then
+ print(" also hosts "..tostring(name));
+ end
+ end
+ end
+ end
+
+ print = _print;
+ end
+
+ for session in pairs(incoming_s2s) do
+ if not session.to_host and ((not match_jid) or session.from_host and session.from_host:match(match_jid)) then
+ count_in = count_in + 1;
+ print("Other incoming s2s connections");
+ print(" (unknown) <- "..(session.from_host or "(unknown)"));
+ end
+ end
+
+ return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections";
+end
+
+function def_env.s2s:close(from, to)
+ local print, count = self.session.print, 0;
+
+ if not (from and to) then
+ return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'";
+ elseif from == to then
+ return false, "Both from and to are the same... you can't do that :)";
+ end
+
+ if hosts[from] and not hosts[to] then
+ -- Is an outgoing connection
+ local session = hosts[from].s2sout[to];
+ if not session then
+ print("No outgoing connection from "..from.." to "..to)
+ else
+ (session.close or s2smanager.destroy_session)(session);
+ count = count + 1;
+ print("Closed outgoing session from "..from.." to "..to);
+ end
+ elseif hosts[to] and not hosts[from] then
+ -- Is an incoming connection
+ for session in pairs(incoming_s2s) do
+ if session.to_host == to and session.from_host == from then
+ (session.close or s2smanager.destroy_session)(session);
+ count = count + 1;
+ end
+ end
+
+ if count == 0 then
+ print("No incoming connections from "..from.." to "..to);
+ else
+ print("Closed "..count.." incoming session"..((count == 1 and "") or "s").." from "..from.." to "..to);
+ end
+ elseif hosts[to] and hosts[from] then
+ return false, "Both of the hostnames you specified are local, there are no s2s sessions to close";
+ else
+ return false, "Neither of the hostnames you specified are being used on this server";
+ end
+
+ return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s");
+end
+
+def_env.host = {}; def_env.hosts = def_env.host;
+function def_env.host:activate(hostname, config)
+ local hostmanager_activate = require "core.hostmanager".activate;
+ if hosts[hostname] then
+ return false, "The host "..tostring(hostname).." is already activated";
+ end
+
+ local defined_hosts = config or configmanager.getconfig();
+ if not config and not defined_hosts[hostname] then
+ return false, "Couldn't find "..tostring(hostname).." defined in the config, perhaps you need to config:reload()?";
+ end
+ hostmanager_activate(hostname, config or defined_hosts[hostname]);
+ return true, "Host "..tostring(hostname).." activated";
+end
+
+function def_env.host:deactivate(hostname, reason)
+ local hostmanager_deactivate = require "core.hostmanager".deactivate;
+ local host = hosts[hostname];
+ if not host then
+ return false, "The host "..tostring(hostname).." is not activated";
+ end
+ if reason then
+ reason = { condition = "host-gone", text = reason };
+ end
+ hostmanager_deactivate(hostname, reason);
+ return true, "Host "..tostring(hostname).." deactivated";
+end
+
+function def_env.host:list()
+ local print = self.session.print;
+ local i = 0;
+ for host in values(array.collect(keys(prosody.hosts)):sort()) do
+ i = i + 1;
+ print(host);
+ end
+ return true, i.." hosts";
+end
+
+-------------
+
+function printbanner(session)
+ local option = config.get("*", "core", "console_banner");
+if option == nil or option == "full" or option == "graphic" then
+session.print [[
+ ____ \ / _
+ | _ \ _ __ ___ ___ _-_ __| |_ _
+ | |_) | '__/ _ \/ __|/ _ \ / _` | | | |
+ | __/| | | (_) \__ \ |_| | (_| | |_| |
+ |_| |_| \___/|___/\___/ \__,_|\__, |
+ A study in simplicity |___/
+
+]]
+end
+if option == nil or option == "short" or option == "full" then
+session.print("Welcome to the Prosody administration console. For a list of commands, type: help");
+session.print("You may find more help on using this console in our online documentation at ");
+session.print("http://prosody.im/doc/console\n");
+end
+if option and option ~= "short" and option ~= "full" and option ~= "graphic" then
+ if type(option) == "string" then
+ session.print(option)
+ elseif type(option) == "function" then
+ setfenv(option, redirect_output(_G, session));
+ pcall(option, session);
+ end
+end
+end
+
+prosody.net_activate_ports("console", "console", {5582}, "tcp");
-- COPYING file in the source package for more information.
--
-local get_children = require "core.hostmanager".get_children;
+local componentmanager_get_children = require "core.componentmanager".get_children;
local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
local jid_split = require "util.jid".split;
local jid_bare = require "util.jid".bare;
local st = require "util.stanza"
-local calculate_hash = require "util.caps".calculate_hash;
local disco_items = module:get_option("disco_items") or {};
do -- validate disco_items
module:add_feature("http://jabber.org/protocol/disco#info");
module:add_feature("http://jabber.org/protocol/disco#items");
--- Generate and cache disco result and caps hash
-local _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash;
-local function build_server_disco_info()
- local query = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" });
+module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type ~= "get" then return; end
+ local node = stanza.tags[1].attr.node;
+ if node and node ~= "" then return; end -- TODO fire event?
+
+ local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info");
local done = {};
for _,identity in ipairs(module:get_host_items("identity")) do
local identity_s = identity.category.."\0"..identity.type;
if not done[identity_s] then
- query:tag("identity", identity):up();
+ reply:tag("identity", identity):up();
done[identity_s] = true;
end
end
for _,feature in ipairs(module:get_host_items("feature")) do
if not done[feature] then
- query:tag("feature", {var=feature}):up();
+ reply:tag("feature", {var=feature}):up();
done[feature] = true;
end
end
- _cached_server_disco_info = query;
- _cached_server_caps_hash = calculate_hash(query);
- _cached_server_caps_feature = st.stanza("c", {
- xmlns = "http://jabber.org/protocol/caps";
- hash = "sha-1";
- node = "http://prosody.im";
- ver = _cached_server_caps_hash;
- });
-end
-local function clear_disco_cache()
- _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash = nil, nil, nil;
-end
-local function get_server_disco_info()
- if not _cached_server_disco_info then build_server_disco_info(); end
- return _cached_server_disco_info;
-end
-local function get_server_caps_feature()
- if not _cached_server_caps_feature then build_server_disco_info(); end
- return _cached_server_caps_feature;
-end
-local function get_server_caps_hash()
- if not _cached_server_caps_hash then build_server_disco_info(); end
- return _cached_server_caps_hash;
-end
-
-module:hook("item-added/identity", clear_disco_cache);
-module:hook("item-added/feature", clear_disco_cache);
-module:hook("item-removed/identity", clear_disco_cache);
-module:hook("item-removed/feature", clear_disco_cache);
-
--- Handle disco requests to the server
-module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(event)
- local origin, stanza = event.origin, event.stanza;
- if stanza.attr.type ~= "get" then return; end
- local node = stanza.tags[1].attr.node;
- if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then return; end -- TODO fire event?
- local reply_query = get_server_disco_info();
- reply_query.node = node;
- local reply = st.reply(stanza):add_child(reply_query);
origin.send(reply);
return true;
end);
if node and node ~= "" then return; end -- TODO fire event?
local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
- for jid in pairs(get_children(module.host)) do
+ for jid in pairs(componentmanager_get_children(module.host)) do
reply:tag("item", {jid = jid}):up();
end
for _, item in ipairs(disco_items) do
origin.send(reply);
return true;
end);
-
--- Handle caps stream feature
-module:hook("stream-features", function (event)
- if event.origin.type == "c2s" then
- event.features:add_child(get_server_caps_feature());
- end
-end);
-
--- Handle disco requests to user accounts
module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(event)
local origin, stanza = event.origin, event.stanza;
if stanza.attr.type ~= "get" then return; end
if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
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
- module:fire_event("account-disco-info", { origin = origin, stanza = reply });
+ module:fire_event("account-disco-info", { session = origin, stanza = reply });
origin.send(reply);
return true;
end
if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items'});
if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
- module:fire_event("account-disco-items", { origin = origin, stanza = reply });
+ module:fire_event("account-disco-items", { session = origin, stanza = reply });
origin.send(reply);
return true;
end
if jid ~= bare_jid then
if not roster[jid] then roster[jid] = {}; end
roster[jid].subscription = "both";
- if groups[group_name][jid] then
- roster[jid].name = groups[group_name][jid];
- end
if not roster[jid].groups then
roster[jid].groups = { [group_name] = true };
end
groups[curr_group] = groups[curr_group] or {};
else
-- Add JID
- local entryjid, name = line:match("([^=]*)=?(.*)");
- module:log("debug", "entryjid = '%s', name = '%s'", entryjid, name);
- local jid;
- jid = jid_prep(entryjid:match("%S+"));
+ local jid = jid_prep(line:match("%S+"));
if jid then
module:log("debug", "New member of %s: %s", tostring(curr_group), tostring(jid));
- groups[curr_group][jid] = name or false;
+ groups[curr_group][jid] = true;
members[jid] = members[jid] or {};
members[jid][#members[jid]+1] = curr_group;
end
local httpserver = require "net.httpserver";
-local lfs = require "lfs";
local open = io.open;
local t_concat = table.concat;
-local stat = lfs.attributes;
local http_base = config.get("*", "core", "http_path") or "www_files";
end
function serve_file(path)
- local full_path = http_base..path;
- if stat(full_path, "mode") == "directory" then
- if stat(full_path.."/index.html", "mode") == "file" then
- return serve_file(path.."/index.html");
- end
- return response_403;
- end
- local f, err = open(full_path, "rb");
+ local f, err = open(http_base..path, "rb");
if not f then return response_404; end
local data = f:read("*a");
f:close();
local st = require "util.stanza";
local jid_split = require "util.jid".split;
+local user_exists = require "core.usermanager".user_exists;
local full_sessions = full_sessions;
local bare_sessions = bare_sessions;
-if module:get_host_type() == "local" then
- module:hook("iq/full", function(data)
- -- IQ to full JID recieved
- local origin, stanza = data.origin, data.stanza;
+module:hook("iq/full", function(data)
+ -- IQ to full JID recieved
+ local origin, stanza = data.origin, data.stanza;
- local session = full_sessions[stanza.attr.to];
- if session then
- -- TODO fire post processing event
- session.send(stanza);
- else -- resource not online
- if stanza.attr.type == "get" or stanza.attr.type == "set" then
- origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
- end
+ local session = full_sessions[stanza.attr.to];
+ if session then
+ -- TODO fire post processing event
+ session.send(stanza);
+ else -- resource not online
+ if stanza.attr.type == "get" or stanza.attr.type == "set" then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
end
- return true;
- end);
-end
+ end
+ return true;
+end);
module:hook("iq/bare", function(data)
-- IQ to bare JID recieved
local origin, stanza = data.origin, data.stanza;
- local type = stanza.attr.type;
+ local to = stanza.attr.to;
+ if to and not bare_sessions[to] then -- quick check for account existance
+ local node, host = jid_split(to);
+ if not user_exists(node, host) then -- full check for account existance
+ if stanza.attr.type == "get" or stanza.attr.type == "set" then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end
+ return true;
+ end
+ end
-- TODO fire post processing events
- if type == "get" or type == "set" then
- local child = stanza.tags[1];
- local ret = module:fire_event("iq/bare/"..child.attr.xmlns..":"..child.name, data);
- if ret ~= nil then return ret; end
- return module:fire_event("iq-"..type.."/bare/"..child.attr.xmlns..":"..child.name, data);
+ if stanza.attr.type == "get" or stanza.attr.type == "set" then
+ return module:fire_event("iq/bare/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data);
else
- return module:fire_event("iq-"..type.."/bare/"..stanza.attr.id, data);
+ module:fire_event("iq/bare/"..stanza.attr.id, data);
+ return true;
end
end);
module:hook("iq/self", function(data)
- -- IQ to self JID recieved
+ -- IQ to bare JID recieved
local origin, stanza = data.origin, data.stanza;
- local type = stanza.attr.type;
- if type == "get" or type == "set" then
- local child = stanza.tags[1];
- local ret = module:fire_event("iq/self/"..child.attr.xmlns..":"..child.name, data);
- if ret ~= nil then return ret; end
- return module:fire_event("iq-"..type.."/self/"..child.attr.xmlns..":"..child.name, data);
+ if stanza.attr.type == "get" or stanza.attr.type == "set" then
+ return module:fire_event("iq/self/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data);
else
- return module:fire_event("iq-"..type.."/self/"..stanza.attr.id, data);
+ module:fire_event("iq/self/"..stanza.attr.id, data);
+ return true;
end
end);
module:hook("iq/host", function(data)
-- IQ to a local host recieved
local origin, stanza = data.origin, data.stanza;
- local type = stanza.attr.type;
- if type == "get" or type == "set" then
- local child = stanza.tags[1];
- local ret = module:fire_event("iq/host/"..child.attr.xmlns..":"..child.name, data);
- if ret ~= nil then return ret; end
- return module:fire_event("iq-"..type.."/host/"..child.attr.xmlns..":"..child.name, data);
+ if stanza.attr.type == "get" or stanza.attr.type == "set" then
+ return module:fire_event("iq/host/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data);
else
- return module:fire_event("iq-"..type.."/host/"..stanza.attr.id, data);
+ module:fire_event("iq/host/"..stanza.attr.id, data);
+ return true;
end
end);
local st = require "util.stanza";
local t_concat = table.concat;
-local secure_auth_only = module:get_option("c2s_require_encryption")
- or module:get_option("require_encryption")
- or not(module:get_option("allow_unencrypted_plain_auth"));
+local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
local sessionmanager = require "core.sessionmanager";
local usermanager = require "core.usermanager";
end
end);
-module:hook("stanza/iq/jabber:iq:auth:query", function(event)
- local session, stanza = event.origin, event.stanza;
-
- if session.type ~= "c2s_unauthed" then
- session.send(st.error_reply(stanza, "cancel", "service-unavailable", "Legacy authentication is only allowed for unauthenticated client connections."));
- return true;
- end
-
- if secure_auth_only and not session.secure then
- session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server"));
- return true;
- end
-
- local username = stanza.tags[1]:child_with_name("username");
- local password = stanza.tags[1]:child_with_name("password");
- local resource = stanza.tags[1]:child_with_name("resource");
- if not (username and password and resource) then
- local reply = st.reply(stanza);
- session.send(reply:query("jabber:iq:auth")
- :tag("username"):up()
- :tag("password"):up()
- :tag("resource"):up());
- else
- username, password, resource = t_concat(username), t_concat(password), t_concat(resource);
- username = nodeprep(username);
- resource = resourceprep(resource)
- local reply = st.reply(stanza);
- if usermanager.test_password(username, session.host, password) then
- -- Authentication successful!
- local success, err = sessionmanager.make_authenticated(session, username);
- if success then
- local err_type, err_msg;
- success, err_type, err, err_msg = sessionmanager.bind_resource(session, resource);
- if not success then
- session.send(st.error_reply(stanza, err_type, err, err_msg));
- session.username, session.type = nil, "c2s_unauthed"; -- FIXME should this be placed in sessionmanager?
- return true;
- elseif resource ~= session.resource then -- server changed resource, not supported by legacy auth
- session.send(st.error_reply(stanza, "cancel", "conflict", "The requested resource could not be assigned to this session."));
- session:close(); -- FIXME undo resource bind and auth instead of closing the session?
- return true;
+module:add_iq_handler("c2s_unauthed", "jabber:iq:auth",
+ function (session, stanza)
+ if secure_auth_only and not session.secure then
+ session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server"));
+ return true;
+ end
+
+ local username = stanza.tags[1]:child_with_name("username");
+ local password = stanza.tags[1]:child_with_name("password");
+ local resource = stanza.tags[1]:child_with_name("resource");
+ if not (username and password and resource) then
+ local reply = st.reply(stanza);
+ session.send(reply:query("jabber:iq:auth")
+ :tag("username"):up()
+ :tag("password"):up()
+ :tag("resource"):up());
+ else
+ username, password, resource = t_concat(username), t_concat(password), t_concat(resource);
+ username = nodeprep(username);
+ resource = resourceprep(resource)
+ local reply = st.reply(stanza);
+ if usermanager.validate_credentials(session.host, username, password) then
+ -- Authentication successful!
+ local success, err = sessionmanager.make_authenticated(session, username);
+ if success then
+ local err_type, err_msg;
+ success, err_type, err, err_msg = sessionmanager.bind_resource(session, resource);
+ if not success then
+ session.send(st.error_reply(stanza, err_type, err, err_msg));
+ session.username, session.type = nil, "c2s_unauthed"; -- FIXME should this be placed in sessionmanager?
+ return true;
+ elseif resource ~= session.resource then -- server changed resource, not supported by legacy auth
+ session.send(st.error_reply(stanza, "cancel", "conflict", "The requested resource could not be assigned to this session."));
+ session:close(); -- FIXME undo resource bind and auth instead of closing the session?
+ return true;
+ end
+ end
+ session.send(st.reply(stanza));
+ else
+ session.send(st.error_reply(stanza, "auth", "not-authorized"));
end
end
- session.send(st.reply(stanza));
- else
- session.send(st.error_reply(stanza, "auth", "not-authorized"));
- end
- end
- return true;
-end);
+ return true;
+ end);
local pairs, ipairs = pairs, ipairs;
local next = next;
local type = type;
-local calculate_hash = require "util.caps".calculate_hash;
+local load_roster = require "core.rostermanager".load_roster;
+local sha1 = require "util.hashes".sha1;
+local base64 = require "util.encodings".base64.encode;
local NULL = {};
local data = {};
local function subscription_presence(user_bare, recipient)
local recipient_bare = jid_bare(recipient);
if (recipient_bare == user_bare) then return true end
- local username, host = jid_split(user_bare);
- return is_contact_subscribed(username, host, recipient_bare);
+ local item = load_roster(jid_split(user_bare))[recipient_bare];
+ return item and (item.subscription == 'from' or item.subscription == 'both');
end
local function publish(session, node, id, item)
-- inbound presence to bare JID recieved
local origin, stanza = event.origin, event.stanza;
local user = stanza.attr.to or (origin.username..'@'..origin.host);
- local t = stanza.attr.type;
- local self = not stanza.attr.to;
- if not t then -- available presence
- if self or subscription_presence(user, stanza.attr.from) then
- local recipient = stanza.attr.from;
- local current = recipients[user] and recipients[user][recipient];
- local hash = get_caps_hash_from_presence(stanza, current);
- if current == hash or (current and current == hash_map[hash]) then return; end
- if not hash then
- if recipients[user] then recipients[user][recipient] = nil; end
+ if not stanza.attr.to or subscription_presence(user, stanza.attr.from) then
+ local recipient = stanza.attr.from;
+ local current = recipients[user] and recipients[user][recipient];
+ local hash = get_caps_hash_from_presence(stanza, current);
+ if current == hash then return; end
+ if not hash then
+ if recipients[user] then recipients[user][recipient] = nil; end
+ else
+ recipients[user] = recipients[user] or {};
+ if hash_map[hash] then
+ recipients[user][recipient] = hash_map[hash];
+ publish_all(user, recipient, origin);
else
- recipients[user] = recipients[user] or {};
- if hash_map[hash] then
- recipients[user][recipient] = hash_map[hash];
- publish_all(user, recipient, origin);
- else
- recipients[user][recipient] = hash;
- local from_bare = origin.type == "c2s" and origin.username.."@"..origin.host;
- if self or origin.type ~= "c2s" or (recipients[from_bare] and recipients[from_bare][origin.full_jid]) ~= hash then
- origin.send(
- st.stanza("iq", {from=stanza.attr.to, to=stanza.attr.from, id="disco", type="get"})
- :query("http://jabber.org/protocol/disco#info")
- );
- end
- end
- end
- end
- elseif t == "unavailable" then
- if recipients[user] then recipients[user][stanza.attr.from] = nil; end
- elseif not self and t == "unsubscribe" then
- local from = jid_bare(stanza.attr.from);
- local subscriptions = recipients[user];
- if subscriptions then
- for subscriber in pairs(subscriptions) do
- if jid_bare(subscriber) == from then
- recipients[user][subscriber] = nil;
- end
+ recipients[user][recipient] = hash;
+ origin.send(
+ st.stanza("iq", {from=stanza.attr.to, to=stanza.attr.from, id="disco", type="get"})
+ :query("http://jabber.org/protocol/disco#info")
+ );
end
end
end
end
end);
-module:hook("iq-result/bare/disco", function(event)
+local function calculate_hash(disco_info)
+ local identities, features, extensions = {}, {}, {};
+ for _, tag in pairs(disco_info) do
+ if tag.name == "identity" then
+ table.insert(identities, (tag.attr.category or "").."\0"..(tag.attr.type or "").."\0"..(tag.attr["xml:lang"] or "").."\0"..(tag.attr.name or ""));
+ elseif tag.name == "feature" then
+ table.insert(features, tag.attr.var or "");
+ elseif tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then
+ local form = {};
+ local FORM_TYPE;
+ for _, field in pairs(tag.tags) do
+ if field.name == "field" and field.attr.var then
+ local values = {};
+ for _, val in pairs(field.tags) do
+ val = #val.tags == 0 and table.concat(val); -- FIXME use get_text?
+ if val then table.insert(values, val); end
+ end
+ table.sort(values);
+ if field.attr.var == "FORM_TYPE" then
+ FORM_TYPE = values[1];
+ elseif #values > 0 then
+ table.insert(form, field.attr.var.."\0"..table.concat(values, "<"));
+ else
+ table.insert(form, field.attr.var);
+ end
+ end
+ end
+ table.sort(form);
+ form = table.concat(form, "<");
+ if FORM_TYPE then form = FORM_TYPE.."\0"..form; end
+ table.insert(extensions, form);
+ end
+ end
+ table.sort(identities);
+ table.sort(features);
+ table.sort(extensions);
+ if #identities > 0 then identities = table.concat(identities, "<"):gsub("%z", "/").."<"; else identities = ""; end
+ if #features > 0 then features = table.concat(features, "<").."<"; else features = ""; end
+ if #extensions > 0 then extensions = table.concat(extensions, "<"):gsub("%z", "<").."<"; else extensions = ""; end
+ local S = identities..features..extensions;
+ local ver = base64(sha1(S));
+ return ver, S;
+end
+
+module:hook("iq/bare/disco", function(event)
local session, stanza = event.origin, event.stanza;
if stanza.attr.type == "result" then
local disco = stanza.tags[1];
if disco and disco.name == "query" and disco.attr.xmlns == "http://jabber.org/protocol/disco#info" then
-- Process disco response
- local self = not stanza.attr.to;
local user = stanza.attr.to or (session.username..'@'..session.host);
local contact = stanza.attr.from;
local current = recipients[user] and recipients[user][contact];
end
end
hash_map[ver] = notify; -- update hash map
- if self then
- for jid, item in pairs(session.roster) do -- for all interested contacts
- if item.subscription == "both" or item.subscription == "from" then
- if not recipients[jid] then recipients[jid] = {}; end
- recipients[jid][contact] = notify;
- publish_all(jid, contact, session);
- end
- end
- end
recipients[user][contact] = notify; -- set recipient's data to calculated data
-- send messages to recipient
publish_all(user, contact, session);
end);
module:hook("account-disco-items", function(event)
- local stanza = event.stanza;
- local bare = stanza.attr.to;
+ local session, stanza = event.session, event.stanza;
+ local bare = session.username..'@'..session.host;
local user_data = data[bare];
if user_data then
--
-local want_pposix_version = "0.3.5";
+local want_pposix_version = "0.3.3";
local pposix = assert(require "util.pposix");
if pposix._VERSION ~= want_pposix_version then module:log("warn", "Unknown version (%s) of binary pposix module, expected %s", tostring(pposix._VERSION), want_pposix_version); end
module:log("warn", "Couldn't load signal library, won't respond to SIGTERM");
end
+local logger_set = require "util.logger".setwriter;
+
local lfs = require "lfs";
local stat = lfs.attributes;
pposix.umask(umask);
-- Allow switching away from root, some people like strange ports.
-module:hook("server-started", function ()
+module:add_event_hook("server-started", function ()
local uid = module:get_option("setuid");
local gid = module:get_option("setgid");
if gid then
end);
-- Don't even think about it!
-if not prosody.start_time then -- server-starting
- local suid = module:get_option("setuid");
- if not suid or suid == 0 or suid == "root" then
- if pposix.getuid() == 0 and not module:get_option("run_as_root") then
- module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!");
- module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root");
- prosody.shutdown("Refusing to run as root");
+module:add_event_hook("server-starting", function ()
+ local suid = module:get_option("setuid");
+ if not suid or suid == 0 or suid == "root" then
+ if pposix.getuid() == 0 and not module:get_option("run_as_root") then
+ module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!");
+ module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root");
+ prosody.shutdown("Refusing to run as root");
+ end
end
- end
-end
+ end);
local pidfile;
local pidfile_handle;
pidfile_handle = nil;
prosody.shutdown("Prosody already running");
else
- pidfile_handle:close();
- pidfile_handle, err = io.open(pidfile, "w+");
- if not pidfile_handle then
- module:log("error", "Couldn't write pidfile at %s; %s", pidfile, err);
- prosody.shutdown("Couldn't write pidfile");
- else
- if lfs.lock(pidfile_handle, "w") then
- pidfile_handle:write(tostring(pposix.getpid()));
- pidfile_handle:flush();
- end
- end
+ pidfile_handle:write(tostring(pposix.getpid()));
+ pidfile_handle:flush();
end
end
end
end
-local syslog_opened;
+local syslog_opened
function syslog_sink_maker(config)
if not syslog_opened then
pposix.syslog_open("prosody");
end
local syslog, format = pposix.syslog_log, string.format;
return function (name, level, message, ...)
- if ... then
- syslog(level, format(message, ...));
- else
- syslog(level, message);
- end
- end;
+ if ... then
+ syslog(level, format(message, ...));
+ else
+ syslog(level, message);
+ end
+ end;
end
require "core.loggingmanager".register_sink_type("syslog", syslog_sink_maker);
write_pidfile();
end
end
- if not prosody.start_time then -- server-starting
- daemonize_server();
- end
+ module:add_event_hook("server-starting", daemonize_server);
else
-- Not going to daemonize, so write the pid of this process
write_pidfile();
end
-module:hook("server-stopped", remove_pidfile);
+module:add_event_hook("server-stopped", remove_pidfile);
-- Set signal handlers
if signal.signal then
local rostermanager = require "core.rostermanager";
local sessionmanager = require "core.sessionmanager";
+local offlinemanager = require "core.offlinemanager";
+
+local _core_route_stanza = core_route_stanza;
+local core_route_stanza;
+function core_route_stanza(origin, stanza)
+ if stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" and stanza.attr.type ~= "error" then
+ local node, host = jid_split(stanza.attr.to);
+ host = hosts[host];
+ if node and host and host.type == "local" then
+ handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
+ return;
+ end
+ end
+ _core_route_stanza(origin, stanza);
+end
local function select_top_resources(user)
local priority = 0;
local ignore_presence_priority = module:get_option("ignore_presence_priority");
-function handle_normal_presence(origin, stanza)
+function handle_normal_presence(origin, stanza, core_route_stanza)
if ignore_presence_priority then
local priority = stanza:child_with_name("priority");
if priority and priority[1] ~= "0" then
priority[1] = "0";
end
end
- local priority = stanza:child_with_name("priority");
- if priority and #priority > 0 then
- priority = t_concat(priority);
- if s_find(priority, "^[+-]?[0-9]+$") then
- priority = tonumber(priority);
- if priority < -128 then priority = -128 end
- if priority > 127 then priority = 127 end
- else priority = 0; end
- else priority = 0; end
if full_sessions[origin.full_jid] then -- if user is still connected
origin.send(stanza); -- reflect their presence back to them
end
for _, res in pairs(user and user.sessions or NULL) do -- broadcast to all resources
if res ~= origin and res.presence then -- to resource
stanza.attr.to = res.full_jid;
- core_post_stanza(origin, stanza, true);
+ core_route_stanza(origin, stanza);
end
end
for jid, item in pairs(roster) do -- broadcast to all interested contacts
if item.subscription == "both" or item.subscription == "from" then
stanza.attr.to = jid;
- core_post_stanza(origin, stanza, true);
+ core_route_stanza(origin, stanza);
end
end
if stanza.attr.type == nil and not origin.presence then -- initial presence
for jid, item in pairs(roster) do -- probe all contacts we are subscribed to
if item.subscription == "both" or item.subscription == "to" then
probe.attr.to = jid;
- core_post_stanza(origin, probe, true);
+ core_route_stanza(origin, probe);
end
end
for _, res in pairs(user and user.sessions or NULL) do -- broadcast from all available resources
if res ~= origin and res.presence then
res.presence.attr.to = origin.full_jid;
- core_post_stanza(res, res.presence, true);
+ core_route_stanza(res, res.presence);
res.presence.attr.to = nil;
end
end
for jid, item in pairs(roster) do -- resend outgoing subscription requests
if item.ask then
request.attr.to = jid;
- core_post_stanza(origin, request, true);
+ core_route_stanza(origin, request);
end
end
-
- if priority >= 0 then
- local event = { origin = origin }
- module:fire_event('message/offline/broadcast', event);
+ local offline = offlinemanager.load(node, host);
+ if offline then
+ for _, msg in ipairs(offline) do
+ origin.send(msg); -- FIXME do we need to modify to/from in any way?
+ end
+ offlinemanager.deleteAll(node, host);
end
end
if stanza.attr.type == "unavailable" then
if origin.directed then
for jid in pairs(origin.directed) do
stanza.attr.to = jid;
- core_post_stanza(origin, stanza, true);
+ core_route_stanza(origin, stanza);
end
origin.directed = nil;
end
else
origin.presence = stanza;
+ local priority = stanza:child_with_name("priority");
+ if priority and #priority > 0 then
+ priority = t_concat(priority);
+ if s_find(priority, "^[+-]?[0-9]+$") then
+ priority = tonumber(priority);
+ if priority < -128 then priority = -128 end
+ if priority > 127 then priority = 127 end
+ else priority = 0; end
+ else priority = 0; end
if origin.priority ~= priority then
origin.priority = priority;
recalc_resource_map(user);
stanza.attr.to = nil; -- reset it
end
-function send_presence_of_available_resources(user, host, jid, recipient_session, stanza)
+function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza, stanza)
local h = hosts[host];
local count = 0;
if h and h.type == "local" then
if pres then
if stanza then pres = stanza; pres.attr.from = session.full_jid; end
pres.attr.to = jid;
- core_post_stanza(session, pres, true);
+ core_route_stanza(session, pres);
pres.attr.to = nil;
count = count + 1;
end
return count;
end
-function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
+function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza)
local node, host = jid_split(from_bare);
- if to_bare == from_bare then return; end -- No self contacts
+ if to_bare == origin.username.."@"..origin.host then return; end -- No self contacts
local st_from, st_to = stanza.attr.from, stanza.attr.to;
stanza.attr.from, stanza.attr.to = from_bare, to_bare;
log("debug", "outbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);
- if stanza.attr.type == "probe" then
- stanza.attr.from, stanza.attr.to = st_from, st_to;
- return;
- elseif stanza.attr.type == "subscribe" then
+ if stanza.attr.type == "subscribe" then
-- 1. route stanza
-- 2. roster push (subscription = none, ask = subscribe)
if rostermanager.set_contact_pending_out(node, host, to_bare) then
rostermanager.roster_push(node, host, to_bare);
end -- else file error
- core_post_stanza(origin, stanza);
+ core_route_stanza(origin, stanza);
elseif stanza.attr.type == "unsubscribe" then
-- 1. route stanza
-- 2. roster push (subscription = none or from)
if rostermanager.unsubscribe(node, host, to_bare) then
rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed?
end -- else file error
- core_post_stanza(origin, stanza);
+ core_route_stanza(origin, stanza);
elseif stanza.attr.type == "subscribed" then
-- 1. route stanza
-- 2. roster_push ()
if rostermanager.subscribed(node, host, to_bare) then
rostermanager.roster_push(node, host, to_bare);
end
- core_post_stanza(origin, stanza);
- send_presence_of_available_resources(node, host, to_bare, origin);
+ core_route_stanza(origin, stanza);
+ send_presence_of_available_resources(node, host, to_bare, origin, core_route_stanza);
elseif stanza.attr.type == "unsubscribed" then
-- 1. route stanza
-- 2. roster push (subscription = none or to)
if rostermanager.unsubscribed(node, host, to_bare) then
rostermanager.roster_push(node, host, to_bare);
end
- core_post_stanza(origin, stanza);
- else
- origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
+ core_route_stanza(origin, stanza);
end
stanza.attr.from, stanza.attr.to = st_from, st_to;
- return true;
end
-function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
+function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza)
local node, host = jid_split(to_bare);
local st_from, st_to = stanza.attr.from, stanza.attr.to;
stanza.attr.from, stanza.attr.to = from_bare, to_bare;
if stanza.attr.type == "probe" then
local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
if result then
- if 0 == send_presence_of_available_resources(node, host, st_from, origin) then
- core_post_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"}), true); -- TODO send last activity
+ if 0 == send_presence_of_available_resources(node, host, st_from, origin, core_route_stanza) then
+ core_route_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"})); -- TODO send last activity
end
elseif not err then
- core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}), true);
+ core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}));
end
elseif stanza.attr.type == "subscribe" then
if rostermanager.is_contact_subscribed(node, host, from_bare) then
- core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"}), true); -- already subscribed
+ core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"})); -- already subscribed
-- Sending presence is not clearly stated in the RFC, but it seems appropriate
- if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then
- core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- TODO send last activity
+ if 0 == send_presence_of_available_resources(node, host, from_bare, origin, core_route_stanza) then
+ core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- TODO send last activity
end
else
- core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt
+ core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- acknowledging receipt
if not rostermanager.is_contact_pending_in(node, host, from_bare) then
if rostermanager.set_contact_pending_in(node, host, from_bare) then
sessionmanager.send_to_available_resources(node, host, stanza);
sessionmanager.send_to_interested_resources(node, host, stanza);
rostermanager.roster_push(node, host, from_bare);
end
- else
- origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
- end
+ end -- discard any other type
stanza.attr.from, stanza.attr.to = st_from, st_to;
- return true;
end
local outbound_presence_handler = function(data)
if to then
local t = stanza.attr.type;
if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes
- return handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
+ handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
+ return true;
end
local to_bare = jid_bare(to);
- local roster = origin.roster;
- if roster and not(roster[to_bare] and (roster[to_bare].subscription == "both" or roster[to_bare].subscription == "from")) then -- directed presence
+ if not(origin.roster[to_bare] and (origin.roster[to_bare].subscription == "both" or origin.roster[to_bare].subscription == "from")) then -- directed presence
origin.directed = origin.directed or {};
if t then -- removing from directed presence list on sending an error or unavailable
origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to?
local t = stanza.attr.type;
if to then
if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID
- return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
+ handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
+ return true;
end
local user = bare_sessions[to];
end
end -- no resources not online, discard
elseif not t or t == "unavailable" then
- handle_normal_presence(origin, stanza);
- else
- origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
+ handle_normal_presence(origin, stanza, core_route_stanza);
end
return true;
end);
local t = stanza.attr.type;
if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID
- return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
+ handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
+ return true;
end
local session = full_sessions[stanza.attr.to];
local from_bare = jid_bare(stanza.attr.from);
local t = stanza.attr.type;
if t == "probe" then
- core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
+ core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
elseif t == "subscribe" then
- core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" }));
- core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
+ core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" }));
+ core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
end
return true;
end);
pres:tag("status"):text("Disconnected: "..err):up();
for jid in pairs(session.directed) do
pres.attr.to = jid;
- core_post_stanza(session, pres, true);
+ core_route_stanza(session, pres);
end
session.directed = nil;
end
end
end
+function sendUnavailable(origin, to, from)
+--[[ example unavailable presence stanza
+<presence from="node@host/resource" type="unavailable" to="node@host" >
+ <status>Logged out</status>
+</presence>
+]]--
+ local presence = st.presence({from=from, type="unavailable"});
+ presence:tag("status"):text("Logged out");
+
+ local node, host = jid_bare(to);
+ local bare = node .. "@" .. host;
+
+ local user = bare_sessions[bare];
+ if user then
+ for resource, session in pairs(user.sessions) do
+ presence.attr.to = session.full_jid;
+ module:log("debug", "send unavailable to: %s; from: %s", tostring(presence.attr.to), tostring(presence.attr.from));
+ origin.send(presence);
+ end
+ end
+end
+
function declineList(privacy_lists, origin, stanza, which)
if which == "default" then
if isAnotherSessionUsingDefaultList(origin) then
return {"modify", "bad-request", "Not existing list specifed to be deleted."};
end
-function createOrReplaceList (privacy_lists, origin, stanza, name, entries)
+function createOrReplaceList (privacy_lists, origin, stanza, name, entries, roster)
local bare_jid = origin.username.."@"..origin.host;
if privacy_lists.lists == nil then
if name == nil then
if privacy_lists.lists then
- if origin.activePrivacyList then
+ if origin.ActivePrivacyList then
reply:tag("active", {name=origin.activePrivacyList}):up();
end
if privacy_lists.default then
return; -- from one of a user's resource to another => HANDS OFF!
end
+ local item;
local listname = session.activePrivacyList;
if listname == nil then
listname = privacy_lists.default; -- no active list selected, use default list
end
if resource == nil then
local prio = 0;
+ local session_;
if bare_sessions[node.."@"..host] ~= nil then
for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do
if session_.priority ~= nil and session_.priority > prio then
e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource;
end
end
- if session.username then -- FIXME do properly
- return checkIfNeedToBeBlocked(e, session);
- end
+ return checkIfNeedToBeBlocked(e, session);
end
module:hook("pre-message/full", preCheckOutgoing, 500);
--
+
local st = require "util.stanza"
local jid_split = require "util.jid".split;
module:add_feature("jabber:iq:private");
-module:hook("iq/self/jabber:iq:private:query", function(event)
- local origin, stanza = event.origin, event.stanza;
- local type = stanza.attr.type;
- local query = stanza.tags[1];
- if #query.tags == 1 then
- local tag = query.tags[1];
- local key = tag.name..":"..tag.attr.xmlns;
- local data, err = datamanager.load(origin.username, origin.host, "private");
- if err then
- origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
- return true;
- end
- if stanza.attr.type == "get" then
- if data and data[key] then
- origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"}):add_child(st.deserialize(data[key])));
- else
- origin.send(st.reply(stanza):add_child(stanza.tags[1]));
- end
- else -- set
- if not data then data = {}; end;
- if #tag == 0 then
- data[key] = nil;
- else
- data[key] = st.preserialize(tag);
- end
- -- TODO delete datastore if empty
- if datamanager.store(origin.username, origin.host, "private", data) then
- origin.send(st.reply(stanza));
+module:add_iq_handler("c2s", "jabber:iq:private",
+ function (session, stanza)
+ local type = stanza.attr.type;
+ local query = stanza.tags[1];
+ if (type == "get" or type == "set") and query.name == "query" then
+ local node, host = jid_split(stanza.attr.to);
+ if not(node or host) or (node == session.username and host == session.host) then
+ node, host = session.username, session.host;
+ if #query.tags == 1 then
+ local tag = query.tags[1];
+ local key = tag.name..":"..tag.attr.xmlns;
+ local data, err = datamanager.load(node, host, "private");
+ if err then
+ session.send(st.error_reply(stanza, "wait", "internal-server-error"));
+ return true;
+ end
+ if stanza.attr.type == "get" then
+ if data and data[key] then
+ session.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"}):add_child(st.deserialize(data[key])));
+ else
+ session.send(st.reply(stanza):add_child(stanza.tags[1]));
+ end
+ else -- set
+ if not data then data = {}; end;
+ if #tag == 0 then
+ data[key] = nil;
+ else
+ data[key] = st.preserialize(tag);
+ end
+ -- TODO delete datastore if empty
+ if datamanager.store(node, host, "private", data) then
+ session.send(st.reply(stanza));
+ else
+ session.send(st.error_reply(stanza, "wait", "internal-server-error"));
+ end
+ end
+ else
+ session.send(st.error_reply(stanza, "modify", "bad-format"));
+ end
else
- origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
+ session.send(st.error_reply(stanza, "cancel", "forbidden"));
end
end
- else
- origin.send(st.error_reply(stanza, "modify", "bad-format"));
- end
- return true;
-end);
+ end);
module:load("proxy65", <proxy65_jid>);
]]--
+if module:get_host_type() ~= "component" then
+ error("proxy65 should be loaded as a component, please see http://prosody.im/doc/components", 0);
+end
-local module = module;
-local tostring = tostring;
-local jid_split, jid_join, jid_compare = require "util.jid".split, require "util.jid".join, require "util.jid".compare;
+local jid_split, jid_join = require "util.jid".split, require "util.jid".join;
local st = require "util.stanza";
+local componentmanager = require "core.componentmanager";
+local config_get = require "core.configmanager".get;
local connlisteners = require "net.connlisteners";
local sha1 = require "util.hashes".sha1;
local server = require "net.server";
local host, name = module:get_host(), "SOCKS5 Bytestreams Service";
-local sessions, transfers, replies_cache = {}, {}, {};
+local sessions, transfers, component, replies_cache = {}, {}, nil, {};
-local proxy_port = module:get_option("proxy65_port") or 5000;
-local proxy_interface = module:get_option("proxy65_interface") or "*";
-local proxy_address = module:get_option("proxy65_address") or (proxy_interface ~= "*" and proxy_interface) or host;
-local proxy_acl = module:get_option("proxy65_acl");
+local proxy_port = config_get(host, "core", "proxy65_port") or 5000;
+local proxy_interface = config_get(host, "core", "proxy65_interface") or "*";
+local proxy_address = config_get(host, "core", "proxy65_address") or (proxy_interface ~= "*" and proxy_interface) or host;
+local proxy_acl = config_get(host, "core", "proxy65_acl");
local max_buffer_size = 4096;
local connlistener = { default_port = proxy_port, default_interface = proxy_interface, default_mode = "*a" };
function connlistener.onincoming(conn, data)
local session = sessions[conn] or {};
- if session.setup == nil and data ~= nil and data:byte(1) == 0x05 and #data > 2 then
- local nmethods = data:byte(2);
+ if session.setup == nil and data ~= nil and data:sub(1):byte() == 0x05 and data:len() > 2 then
+ local nmethods = data:sub(2):byte();
local methods = data:sub(3);
local supported = false;
for i=1, nmethods, 1 do
- if(methods:byte(i) == 0x00) then -- 0x00 == method: NO AUTH
+ if(methods:sub(i):byte() == 0x00) then -- 0x00 == method: NO AUTH
supported = true;
break;
end
return;
end
end
- if data ~= nil and #data == 0x2F and -- 40 == length of SHA1 HASH, and 7 other bytes => 47 => 0x2F
- data:byte(1) == 0x05 and -- SOCKS5 has 5 in first byte
- data:byte(2) == 0x01 and -- CMD must be 1
- data:byte(3) == 0x00 and -- RSV must be 0
- data:byte(4) == 0x03 and -- ATYP must be 3
- data:byte(5) == 40 and -- SHA1 HASH length must be 40 (0x28)
- data:byte(-2) == 0x00 and -- PORT must be 0, size 2 byte
- data:byte(-1) == 0x00
+ if data ~= nil and data:len() == 0x2F and -- 40 == length of SHA1 HASH, and 7 other bytes => 47 => 0x2F
+ data:sub(1):byte() == 0x05 and -- SOCKS5 has 5 in first byte
+ data:sub(2):byte() == 0x01 and -- CMD must be 1
+ data:sub(3):byte() == 0x00 and -- RSV must be 0
+ data:sub(4):byte() == 0x03 and -- ATYP must be 3
+ data:sub(5):byte() == 40 and -- SHA1 HASH length must be 40 (0x28)
+ data:sub(-2):byte() == 0x00 and -- PORT must be 0, size 2 byte
+ data:sub(-1):byte() == 0x00
then
local sha = data:sub(6, 45); -- second param is not count! it's the ending index (included!)
if transfers[sha] == nil then
server.link(conn, transfers[sha].target, max_buffer_size);
server.link(transfers[sha].target, conn, max_buffer_size);
end
- conn:write(string.char(5, 0, 0, 3, #sha) .. sha .. string.char(0, 0)); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte)
+ conn:write(string.char(5, 0, 0, 3, sha:len()) .. sha .. string.char(0, 0)); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte)
conn:lock_read(true)
else
module:log("warn", "Neither data transfer nor initial connect of a participator of a transfer.")
end
end
-module:add_identity("proxy", "bytestreams", name);
-module:add_feature("http://jabber.org/protocol/bytestreams");
-
-module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function(event)
- local origin, stanza = event.origin, event.stanza;
+local function get_disco_info(stanza)
local reply = replies_cache.disco_info;
if reply == nil then
reply = st.iq({type='result', from=host}):query("http://jabber.org/protocol/disco#info")
reply.attr.id = stanza.attr.id;
reply.attr.to = stanza.attr.from;
- origin.send(reply);
- return true;
-end, -1);
+ return reply;
+end
-module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function(event)
- local origin, stanza = event.origin, event.stanza;
+local function get_disco_items(stanza)
local reply = replies_cache.disco_items;
if reply == nil then
reply = st.iq({type='result', from=host}):query("http://jabber.org/protocol/disco#items");
reply.attr.id = stanza.attr.id;
reply.attr.to = stanza.attr.from;
- origin.send(reply);
- return true;
-end, -1);
+ return reply;
+end
-module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event)
- local origin, stanza = event.origin, event.stanza;
+local function get_stream_host(origin, stanza)
local reply = replies_cache.stream_host;
local err_reply = replies_cache.stream_host_err;
local sid = stanza.tags[1].attr.sid;
local allow = false;
- local jid = stanza.attr.from;
+ local jid_node, jid_host, jid_resource = jid_split(stanza.attr.from);
+
+ if stanza.attr.from == nil then
+ jid_node = origin.username;
+ jid_host = origin.host;
+ jid_resource = origin.resource;
+ end
if proxy_acl and #proxy_acl > 0 then
- for _, acl in ipairs(proxy_acl) do
- if jid_compare(jid, acl) then allow = true; end
+ if host ~= nil then -- at least a domain is needed.
+ for _, acl in ipairs(proxy_acl) do
+ local acl_node, acl_host, acl_resource = jid_split(acl);
+ if ((acl_node ~= nil and acl_node == jid_node) or acl_node == nil) and
+ ((acl_host ~= nil and acl_host == jid_host) or acl_host == nil) and
+ ((acl_resource ~= nil and acl_resource == jid_resource) or acl_resource == nil) then
+ allow = true;
+ end
+ end
end
else
allow = true;
replies_cache.stream_host = reply;
end
else
- module:log("warn", "Denying use of proxy for %s", tostring(jid));
+ module:log("warn", "Denying use of proxy for %s", tostring(jid_join(jid_node, jid_host, jid_resource)));
if err_reply == nil then
err_reply = st.iq({type="error", from=host})
:query("http://jabber.org/protocol/bytestreams")
reply.attr.id = stanza.attr.id;
reply.attr.to = stanza.attr.from;
reply.tags[1].attr.sid = sid;
- origin.send(reply);
- return true;
-end);
+ return reply;
+end
module.unload = function()
+ componentmanager.deregister_component(host);
connlisteners.deregister(module.host .. ':proxy65');
end
local function set_activation(stanza)
- local to, reply;
- local from = stanza.attr.from;
- local query = stanza.tags[1];
- local sid = query.attr.sid;
- if query.tags[1] and query.tags[1].name == "activate" then
- to = query.tags[1][1];
+ local from, to, sid, reply = nil;
+ from = stanza.attr.from;
+ if stanza.tags[1] ~= nil and tostring(stanza.tags[1].name) == "query" then
+ if stanza.tags[1].attr ~= nil then
+ sid = stanza.tags[1].attr.sid;
+ end
+ if stanza.tags[1].tags[1] ~= nil and tostring(stanza.tags[1].tags[1].name) == "activate" then
+ to = stanza.tags[1].tags[1][1];
+ end
end
if from ~= nil and to ~= nil and sid ~= nil then
reply = st.iq({type="result", from=host, to=from});
return reply, from, to, sid;
end
-module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event)
- local origin, stanza = event.origin, event.stanza;
-
- module:log("debug", "Received activation request from %s", stanza.attr.from);
- local reply, from, to, sid = set_activation(stanza);
- if reply ~= nil and from ~= nil and to ~= nil and sid ~= nil then
- local sha = sha1(sid .. from .. to, true);
- if transfers[sha] == nil then
- module:log("error", "transfers[sha]: nil");
- elseif(transfers[sha] ~= nil and transfers[sha].initiator ~= nil and transfers[sha].target ~= nil) then
- origin.send(reply);
- transfers[sha].activated = true;
- transfers[sha].target:lock_read(false);
- transfers[sha].initiator:lock_read(false);
- else
- module:log("debug", "Both parties were not yet connected");
- local message = "Neither party is connected to the proxy";
- if transfers[sha].initiator then
- message = "The recipient is not connected to the proxy";
- elseif transfers[sha].target then
- message = "The sender (you) is not connected to the proxy";
+function handle_to_domain(origin, stanza)
+ local to_node, to_host, to_resource = jid_split(stanza.attr.to);
+ if to_node == nil then
+ local type = stanza.attr.type;
+ if type == "error" or type == "result" then return; end
+ if stanza.name == "iq" and type == "get" then
+ local xmlns = stanza.tags[1].attr.xmlns
+ if xmlns == "http://jabber.org/protocol/disco#info" then
+ origin.send(get_disco_info(stanza));
+ return true;
+ elseif xmlns == "http://jabber.org/protocol/disco#items" then
+ origin.send(get_disco_items(stanza));
+ return true;
+ elseif xmlns == "http://jabber.org/protocol/bytestreams" then
+ origin.send(get_stream_host(origin, stanza));
+ return true;
+ else
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ return true;
+ end
+ elseif stanza.name == "iq" and type == "set" then
+ module:log("debug", "Received activation request from %s", stanza.attr.from);
+ local reply, from, to, sid = set_activation(stanza);
+ if reply ~= nil and from ~= nil and to ~= nil and sid ~= nil then
+ local sha = sha1(sid .. from .. to, true);
+ if transfers[sha] == nil then
+ module:log("error", "transfers[sha]: nil");
+ elseif(transfers[sha] ~= nil and transfers[sha].initiator ~= nil and transfers[sha].target ~= nil) then
+ origin.send(reply);
+ transfers[sha].activated = true;
+ transfers[sha].target:lock_read(false);
+ transfers[sha].initiator:lock_read(false);
+ else
+ module:log("debug", "Both parties were not yet connected");
+ local message = "Neither party is connected to the proxy";
+ if transfers[sha].initiator then
+ message = "The recipient is not connected to the proxy";
+ elseif transfers[sha].target then
+ message = "The sender (you) is not connected to the proxy";
+ end
+ origin.send(st.error_reply(stanza, "cancel", "not-allowed", message));
+ end
+ else
+ module:log("error", "activation failed: sid: %s, initiator: %s, target: %s", tostring(sid), tostring(from), tostring(to));
end
- origin.send(st.error_reply(stanza, "cancel", "not-allowed", message));
end
- return true;
- else
- module:log("error", "activation failed: sid: %s, initiator: %s, target: %s", tostring(sid), tostring(from), tostring(to));
end
-end);
+ return;
+end
if not connlisteners.register(module.host .. ':proxy65', connlistener) then
module:log("error", "mod_proxy65: Could not establish a connection listener. Check your configuration please.");
end
connlisteners.start(module.host .. ':proxy65');
+component = componentmanager.register_component(host, handle_to_domain);
local usermanager_user_exists = require "core.usermanager".user_exists;
local usermanager_create_user = require "core.usermanager".create_user;
local usermanager_set_password = require "core.usermanager".set_password;
-local usermanager_delete_user = require "core.usermanager".delete_user;
+local datamanager_store = require "util.datamanager".store;
local os_time = os.time;
local nodeprep = require "util.encodings".stringprep.nodeprep;
-local jid_bare = require "util.jid".bare;
-
-local compat = module:get_option_boolean("registration_compat", true);
-local allow_registration = module:get_option_boolean("allow_registration", false);
module:add_feature("jabber:iq:register");
-local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up();
-module:hook("stream-features", function(event)
- local session, features = event.origin, event.features;
-
- -- Advertise registration to unauthorized clients only.
- if not(allow_registration) or session.type ~= "c2s_unauthed" then
- return
- end
-
- features:add_child(register_stream_feature);
-end);
-
-local function handle_registration_stanza(event)
- local session, stanza = event.origin, event.stanza;
-
- local query = stanza.tags[1];
- if stanza.attr.type == "get" then
- local reply = st.reply(stanza);
- reply:tag("query", {xmlns = "jabber:iq:register"})
- :tag("registered"):up()
- :tag("username"):text(session.username):up()
- :tag("password"):up();
- session.send(reply);
- else -- stanza.attr.type == "set"
- if query.tags[1] and query.tags[1].name == "remove" then
- -- TODO delete user auth data, send iq response, kick all user resources with a <not-authorized/>, delete all user data
- local username, host = session.username, session.host;
-
- local ok, err = usermanager_delete_user(username, host);
-
- if not ok then
- module:log("debug", "Removing user account %s@%s failed: %s", username, host, err);
- session.send(st.error_reply(stanza, "cancel", "service-unavailable", err));
- return true;
- end
-
- session.send(st.reply(stanza));
- local roster = session.roster;
- for _, session in pairs(hosts[host].sessions[username].sessions) do -- disconnect all resources
- session:close({condition = "not-authorized", text = "Account deleted"});
- end
- -- TODO datamanager should be able to delete all user data itself
- datamanager.store(username, host, "vcard", nil);
- datamanager.store(username, host, "private", nil);
- datamanager.list_store(username, host, "offline", nil);
- local bare = username.."@"..host;
- for jid, item in pairs(roster) do
- if jid and jid ~= "pending" then
- if item.subscription == "both" or item.subscription == "from" or (roster.pending and roster.pending[jid]) then
- core_post_stanza(hosts[host], st.presence({type="unsubscribed", from=bare, to=jid}));
- end
- if item.subscription == "both" or item.subscription == "to" or item.ask then
- core_post_stanza(hosts[host], st.presence({type="unsubscribe", from=bare, to=jid}));
+module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza)
+ if stanza.tags[1].name == "query" then
+ local query = stanza.tags[1];
+ if stanza.attr.type == "get" then
+ local reply = st.reply(stanza);
+ reply:tag("query", {xmlns = "jabber:iq:register"})
+ :tag("registered"):up()
+ :tag("username"):text(session.username):up()
+ :tag("password"):up();
+ session.send(reply);
+ elseif stanza.attr.type == "set" then
+ if query.tags[1] and query.tags[1].name == "remove" then
+ -- TODO delete user auth data, send iq response, kick all user resources with a <not-authorized/>, delete all user data
+ local username, host = session.username, session.host;
+ --session.send(st.error_reply(stanza, "cancel", "not-allowed"));
+ --return;
+ --usermanager_set_password(username, host, nil); -- Disable account
+ -- FIXME the disabling currently allows a different user to recreate the account
+ -- we should add an in-memory account block mode when we have threading
+ session.send(st.reply(stanza));
+ local roster = session.roster;
+ for _, session in pairs(hosts[host].sessions[username].sessions) do -- disconnect all resources
+ session:close({condition = "not-authorized", text = "Account deleted"});
+ end
+ -- TODO datamanager should be able to delete all user data itself
+ datamanager.store(username, host, "vcard", nil);
+ datamanager.store(username, host, "private", nil);
+ datamanager.list_store(username, host, "offline", nil);
+ local bare = username.."@"..host;
+ for jid, item in pairs(roster) do
+ if jid and jid ~= "pending" then
+ if item.subscription == "both" or item.subscription == "from" or (roster.pending and roster.pending[jid]) then
+ core_post_stanza(hosts[host], st.presence({type="unsubscribed", from=bare, to=jid}));
+ end
+ if item.subscription == "both" or item.subscription == "to" or item.ask then
+ core_post_stanza(hosts[host], st.presence({type="unsubscribe", from=bare, to=jid}));
+ end
end
end
- end
- datamanager.store(username, host, "roster", nil);
- datamanager.store(username, host, "privacy", nil);
- module:log("info", "User removed their account: %s@%s", username, host);
- module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session });
- else
- local username = nodeprep(query:get_child("username"):get_text());
- local password = query:get_child("password"):get_text();
- if username and password then
- if username == session.username then
- if usermanager_set_password(username, password, session.host) then
- session.send(st.reply(stanza));
+ datamanager.store(username, host, "roster", nil);
+ datamanager.store(username, host, "privacy", nil);
+ datamanager.store(username, host, "accounts", nil); -- delete accounts datastore at the end
+ module:log("info", "User removed their account: %s@%s", username, host);
+ module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session });
+ else
+ local username = query:child_with_name("username");
+ local password = query:child_with_name("password");
+ if username and password then
+ -- FIXME shouldn't use table.concat
+ username = nodeprep(table.concat(username));
+ password = table.concat(password);
+ if username == session.username then
+ if usermanager_set_password(username, session.host, password) then
+ session.send(st.reply(stanza));
+ else
+ -- TODO unable to write file, file may be locked, etc, what's the correct error?
+ session.send(st.error_reply(stanza, "wait", "internal-server-error"));
+ end
else
- -- TODO unable to write file, file may be locked, etc, what's the correct error?
- session.send(st.error_reply(stanza, "wait", "internal-server-error"));
+ session.send(st.error_reply(stanza, "modify", "bad-request"));
end
else
session.send(st.error_reply(stanza, "modify", "bad-request"));
end
- else
- session.send(st.error_reply(stanza, "modify", "bad-request"));
end
end
- end
- return true;
-end
-
-module:hook("iq/self/jabber:iq:register:query", handle_registration_stanza);
-if compat then
- module:hook("iq/host/jabber:iq:register:query", function (event)
- local session, stanza = event.origin, event.stanza;
- if session.type == "c2s" and jid_bare(stanza.attr.to) == session.host then
- return handle_registration_stanza(event);
- end
- end);
-end
+ else
+ session.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end;
+end);
local recent_ips = {};
local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations");
for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end
for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end
-module:hook("stanza/iq/jabber:iq:register:query", function(event)
- local session, stanza = event.origin, event.stanza;
-
- if not(allow_registration) or session.type ~= "c2s_unauthed" then
+module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, stanza)
+ if module:get_option("allow_registration") == false then
session.send(st.error_reply(stanza, "cancel", "service-unavailable"));
- else
+ elseif stanza.tags[1].name == "query" then
local query = stanza.tags[1];
if stanza.attr.type == "get" then
local reply = st.reply(stanza);
module:log("debug", "User's IP not known; can't apply blacklist/whitelist");
elseif blacklisted_ips[session.ip] or (whitelist_only and not whitelisted_ips[session.ip]) then
session.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not allowed to register an account."));
- return true;
+ return;
elseif min_seconds_between_registrations and not whitelisted_ips[session.ip] then
if not recent_ips[session.ip] then
recent_ips[session.ip] = { time = os_time(), count = 1 };
if os_time() - ip.time < min_seconds_between_registrations then
ip.time = os_time();
session.send(st.error_reply(stanza, "wait", "not-acceptable"));
- return true;
+ return;
end
ip.time = os_time();
end
if usermanager_create_user(username, password, host) then
session.send(st.reply(stanza)); -- user created!
module:log("info", "User account created: %s@%s", username, host);
- module:fire_event("user-registered", {
+ module:fire_event("user-registered", {
username = username, host = host, source = "mod_register",
session = session });
else
end
end
end
- end
- return true;
+ else
+ session.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end;
end);
+
--
+
local st = require "util.stanza"
local jid_split = require "util.jid".split;
local jid_prep = require "util.jid".prep;
local t_concat = table.concat;
-local tonumber = tonumber;
-local pairs, ipairs = pairs, ipairs;
+local tostring = tostring;
local rm_remove_from_roster = require "core.rostermanager".remove_from_roster;
local rm_add_to_roster = require "core.rostermanager".add_to_roster;
module:add_feature("jabber:iq:roster");
-local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"});
+local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"}):tag("optional"):up();
module:hook("stream-features", function(event)
local origin, features = event.origin, event.features;
if origin.username then
end
end);
-module:hook("iq/self/jabber:iq:roster:query", function(event)
- local session, stanza = event.origin, event.stanza;
-
- if stanza.attr.type == "get" then
- local roster = st.reply(stanza);
-
- local client_ver = tonumber(stanza.tags[1].attr.ver);
- local server_ver = tonumber(session.roster[false].version or 1);
-
- if not (client_ver and server_ver) or client_ver ~= server_ver then
- roster:query("jabber:iq:roster");
- -- Client does not support versioning, or has stale roster
- for jid, item in pairs(session.roster) do
- if jid ~= "pending" and jid then
- roster:tag("item", {
- jid = jid,
- subscription = item.subscription,
- ask = item.ask,
- name = item.name,
- });
- for group in pairs(item.groups) do
- roster:tag("group"):text(group):up();
- end
- roster:up(); -- move out from item
- end
- end
- roster.tags[1].attr.ver = server_ver;
- end
- session.send(roster);
- session.interested = true; -- resource is interested in roster updates
- else -- stanza.attr.type == "set"
- local query = stanza.tags[1];
- if #query.tags == 1 and query.tags[1].name == "item"
- and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid
- -- Protection against overwriting roster.pending, until we move it
- and query.tags[1].attr.jid ~= "pending" then
- local item = query.tags[1];
- local from_node, from_host = jid_split(stanza.attr.from);
- local from_bare = from_node and (from_node.."@"..from_host) or from_host; -- bare JID
- local jid = jid_prep(item.attr.jid);
- local node, host, resource = jid_split(jid);
- if not resource and host then
- if jid ~= from_node.."@"..from_host then
- if item.attr.subscription == "remove" then
- local roster = session.roster;
- local r_item = roster[jid];
- if r_item then
- local to_bare = node and (node.."@"..host) or host; -- bare JID
- if r_item.subscription == "both" or r_item.subscription == "from" or (roster.pending and roster.pending[jid]) then
- core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=to_bare}));
- end
- if r_item.subscription == "both" or r_item.subscription == "to" or r_item.ask then
- core_post_stanza(session, st.presence({type="unsubscribe", from=session.full_jid, to=to_bare}));
- end
- local success, err_type, err_cond, err_msg = rm_remove_from_roster(session, jid);
- if success then
- session.send(st.reply(stanza));
- rm_roster_push(from_node, from_host, jid);
- else
- session.send(st.error_reply(stanza, err_type, err_cond, err_msg));
+module:add_iq_handler("c2s", "jabber:iq:roster",
+ function (session, stanza)
+ if stanza.tags[1].name == "query" then
+ if stanza.attr.type == "get" then
+ local roster = st.reply(stanza);
+
+ local client_ver = tonumber(stanza.tags[1].attr.ver);
+ local server_ver = tonumber(session.roster[false].version or 1);
+
+ if not (client_ver and server_ver) or client_ver ~= server_ver then
+ roster:query("jabber:iq:roster");
+ -- Client does not support versioning, or has stale roster
+ for jid in pairs(session.roster) do
+ if jid ~= "pending" and jid then
+ roster:tag("item", {
+ jid = jid,
+ subscription = session.roster[jid].subscription,
+ ask = session.roster[jid].ask,
+ name = session.roster[jid].name,
+ });
+ for group in pairs(session.roster[jid].groups) do
+ roster:tag("group"):text(group):up();
+ end
+ roster:up(); -- move out from item
end
- else
- session.send(st.error_reply(stanza, "modify", "item-not-found"));
end
- else
- local r_item = {name = item.attr.name, groups = {}};
- if r_item.name == "" then r_item.name = nil; end
- if session.roster[jid] then
- r_item.subscription = session.roster[jid].subscription;
- r_item.ask = session.roster[jid].ask;
- else
- r_item.subscription = "none";
- end
- for _, child in ipairs(item) do
- if child.name == "group" then
- local text = t_concat(child);
- if text and text ~= "" then
- r_item.groups[text] = true;
+ roster.tags[1].attr.ver = server_ver;
+ end
+ session.send(roster);
+ session.interested = true; -- resource is interested in roster updates
+ return true;
+ elseif stanza.attr.type == "set" then
+ local query = stanza.tags[1];
+ if #query.tags == 1 and query.tags[1].name == "item"
+ and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid
+ -- Protection against overwriting roster.pending, until we move it
+ and query.tags[1].attr.jid ~= "pending" then
+ local item = query.tags[1];
+ local from_node, from_host = jid_split(stanza.attr.from);
+ local from_bare = from_node and (from_node.."@"..from_host) or from_host; -- bare JID
+ local jid = jid_prep(item.attr.jid);
+ local node, host, resource = jid_split(jid);
+ if not resource and host then
+ if jid ~= from_node.."@"..from_host then
+ if item.attr.subscription == "remove" then
+ local roster = session.roster;
+ local r_item = roster[jid];
+ if r_item then
+ local to_bare = node and (node.."@"..host) or host; -- bare JID
+ if r_item.subscription == "both" or r_item.subscription == "from" or (roster.pending and roster.pending[jid]) then
+ core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=to_bare}));
+ end
+ if r_item.subscription == "both" or r_item.subscription == "to" or r_item.ask then
+ core_post_stanza(session, st.presence({type="unsubscribe", from=session.full_jid, to=to_bare}));
+ end
+ local success, err_type, err_cond, err_msg = rm_remove_from_roster(session, jid);
+ if success then
+ session.send(st.reply(stanza));
+ rm_roster_push(from_node, from_host, jid);
+ else
+ session.send(st.error_reply(stanza, err_type, err_cond, err_msg));
+ end
+ else
+ session.send(st.error_reply(stanza, "modify", "item-not-found"));
+ end
+ else
+ local r_item = {name = item.attr.name, groups = {}};
+ if r_item.name == "" then r_item.name = nil; end
+ if session.roster[jid] then
+ r_item.subscription = session.roster[jid].subscription;
+ r_item.ask = session.roster[jid].ask;
+ else
+ r_item.subscription = "none";
+ end
+ for _, child in ipairs(item) do
+ if child.name == "group" then
+ local text = t_concat(child);
+ if text and text ~= "" then
+ r_item.groups[text] = true;
+ end
+ end
+ end
+ local success, err_type, err_cond, err_msg = rm_add_to_roster(session, jid, r_item);
+ if success then
+ -- Ok, send success
+ session.send(st.reply(stanza));
+ -- and push change to all resources
+ rm_roster_push(from_node, from_host, jid);
+ else
+ -- Adding to roster failed
+ session.send(st.error_reply(stanza, err_type, err_cond, err_msg));
+ end
end
+ else
+ -- Trying to add self to roster
+ session.send(st.error_reply(stanza, "cancel", "not-allowed"));
end
- end
- local success, err_type, err_cond, err_msg = rm_add_to_roster(session, jid, r_item);
- if success then
- -- Ok, send success
- session.send(st.reply(stanza));
- -- and push change to all resources
- rm_roster_push(from_node, from_host, jid);
else
- -- Adding to roster failed
- session.send(st.error_reply(stanza, err_type, err_cond, err_msg));
+ -- Invalid JID added to roster
+ session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error?
end
+ else
+ -- Roster set didn't include a single item, or its name wasn't 'item'
+ session.send(st.error_reply(stanza, "modify", "bad-request"));
end
- else
- -- Trying to add self to roster
- session.send(st.error_reply(stanza, "cancel", "not-allowed"));
+ return true;
end
- else
- -- Invalid JID added to roster
- session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error?
end
- else
- -- Roster set didn't include a single item, or its name wasn't 'item'
- session.send(st.error_reply(stanza, "modify", "bad-request"));
- end
- end
- return true;
-end);
+ end);
local st = require "util.stanza";
local sm_bind_resource = require "core.sessionmanager".bind_resource;
local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
-local s2s_make_authenticated = require "core.s2smanager".make_authenticated;
local base64 = require "util.encodings".base64;
-local cert_verify_identity = require "util.x509".verify_identity;
-
local nodeprep = require "util.encodings".stringprep.nodeprep;
-local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler;
+local datamanager_load = require "util.datamanager".load;
+local usermanager_validate_credentials = require "core.usermanager".validate_credentials;
+local usermanager_get_supported_methods = require "core.usermanager".get_supported_methods;
+local usermanager_user_exists = require "core.usermanager".user_exists;
+local usermanager_get_password = require "core.usermanager".get_password;
+local t_concat, t_insert = table.concat, table.insert;
local tostring = tostring;
+local jid_split = require "util.jid".split;
+local md5 = require "util.hashes".md5;
+local config = require "core.configmanager";
local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
-local allow_unencrypted_plain_auth = module:get_option("allow_unencrypted_plain_auth")
+local sasl_backend = module:get_option("sasl_backend") or "builtin";
+
+-- Cyrus config options
+local require_provisioning = module:get_option("cyrus_require_provisioning") or false;
+local cyrus_service_realm = module:get_option("cyrus_service_realm");
+local cyrus_service_name = module:get_option("cyrus_service_name");
+local cyrus_application_name = module:get_option("cyrus_application_name");
local log = module._log;
local xmlns_bind ='urn:ietf:params:xml:ns:xmpp-bind';
local xmlns_stanzas ='urn:ietf:params:xml:ns:xmpp-stanzas';
+local new_sasl;
+if sasl_backend == "builtin" then
+ new_sasl = require "util.sasl".new;
+elseif sasl_backend == "cyrus" then
+ prosody.unlock_globals(); --FIXME: Figure out why this is needed and
+ -- why cyrussasl isn't caught by the sandbox
+ local ok, cyrus = pcall(require, "util.sasl_cyrus");
+ prosody.lock_globals();
+ if ok then
+ local cyrus_new = cyrus.new;
+ new_sasl = function(realm)
+ return cyrus_new(
+ cyrus_service_realm or realm,
+ cyrus_service_name or "xmpp",
+ cyrus_application_name or "prosody"
+ );
+ end
+ else
+ module:log("error", "Failed to load Cyrus SASL because: %s", cyrus);
+ error("Failed to load Cyrus SASL");
+ end
+else
+ module:log("error", "Unknown SASL backend: %s", sasl_backend);
+ error("Unknown SASL backend");
+end
+
+local default_authentication_profile = {
+ plain = function(username, realm)
+ local prepped_username = nodeprep(username);
+ if not prepped_username then
+ log("debug", "NODEprep failed on username: %s", username);
+ return "", nil;
+ end
+ local password = usermanager_get_password(prepped_username, realm);
+ if not password then
+ return "", nil;
+ end
+ return password, true;
+ end
+};
+
+local anonymous_authentication_profile = {
+ anonymous = function(username, realm)
+ return true; -- for normal usage you should always return true here
+ end
+};
+
local function build_reply(status, ret, err_msg)
local reply = st.stanza(status, {xmlns = xmlns_sasl});
if status == "challenge" then
elseif status == "success" then
local username = nodeprep(session.sasl_handler.username);
- local ok, err = sm_make_authenticated(session, session.sasl_handler.username);
- if ok then
- session.sasl_handler = nil;
- session:reset_stream();
+ if not(require_provisioning) or usermanager_user_exists(username, session.host) then
+ local aret, err = sm_make_authenticated(session, session.sasl_handler.username);
+ if aret then
+ session.sasl_handler = nil;
+ session:reset_stream();
+ else
+ module:log("warn", "SASL succeeded but username was invalid");
+ session.sasl_handler = session.sasl_handler:clean_clone();
+ return "failure", "not-authorized", "User authenticated successfully, but username was invalid";
+ end
else
- module:log("warn", "SASL succeeded but username was invalid");
+ module:log("warn", "SASL succeeded but we don't have an account provisioned for %s", username);
session.sasl_handler = session.sasl_handler:clean_clone();
- return "failure", "not-authorized", "User authenticated successfully, but username was invalid";
+ return "failure", "not-authorized", "User authenticated successfully, but not provisioned for XMPP";
end
end
return status, ret, err_msg;
end
-local function sasl_process_cdata(session, stanza)
+local function sasl_handler(session, stanza)
+ if stanza.name == "auth" then
+ -- FIXME ignoring duplicates because ejabberd does
+ if config.get(session.host or "*", "core", "anonymous_login") then
+ if stanza.attr.mechanism ~= "ANONYMOUS" then
+ return session.send(build_reply("failure", "invalid-mechanism"));
+ end
+ elseif stanza.attr.mechanism == "ANONYMOUS" then
+ return session.send(build_reply("failure", "mechanism-too-weak"));
+ end
+ local valid_mechanism = session.sasl_handler:select(stanza.attr.mechanism);
+ if not valid_mechanism then
+ return session.send(build_reply("failure", "invalid-mechanism"));
+ end
+ if secure_auth_only and not session.secure then
+ return session.send(build_reply("failure", "encryption-required"));
+ end
+ elseif not session.sasl_handler then
+ return; -- FIXME ignoring out of order stanzas because ejabberd does
+ end
local text = stanza[1];
if text then
text = base64.decode(text);
if not text then
session.sasl_handler = nil;
session.send(build_reply("failure", "incorrect-encoding"));
- return true;
+ return;
end
end
local status, ret, err_msg = session.sasl_handler:process(text);
local s = build_reply(status, ret, err_msg);
log("debug", "sasl reply: %s", tostring(s));
session.send(s);
- return true;
end
-module:hook_stanza(xmlns_sasl, "success", function (session, stanza)
- if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
- module:log("debug", "SASL EXTERNAL with %s succeeded", session.to_host);
- session.external_auth = "succeeded"
- session:reset_stream();
-
- local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams",
- ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host};
- session.sends2s("<?xml version='1.0'?>");
- session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag());
-
- s2s_make_authenticated(session, session.to_host);
- return true;
-end)
-
-module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
- if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
-
- module:log("info", "SASL EXTERNAL with %s failed", session.to_host)
- -- TODO: Log the failure reason
- session.external_auth = "failed"
-end, 500)
-
-module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
- -- TODO: Dialback wasn't loaded. Do something useful.
-end, 90)
-
-module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
- if session.type ~= "s2sout_unauthed" or not session.secure then return; end
-
- local mechanisms = stanza:get_child("mechanisms", xmlns_sasl)
- if mechanisms then
- for mech in mechanisms:childtags() do
- if mech[1] == "EXTERNAL" then
- module:log("debug", "Initiating SASL EXTERNAL with %s", session.to_host);
- local reply = st.stanza("auth", {xmlns = xmlns_sasl, mechanism = "EXTERNAL"});
- reply:text(base64.encode(session.from_host))
- session.sends2s(reply)
- session.external_auth = "attempting"
- return true
- end
- end
- end
-end, 150);
-
-local function s2s_external_auth(session, stanza)
- local mechanism = stanza.attr.mechanism;
-
- if not session.secure then
- if mechanism == "EXTERNAL" then
- session.sends2s(build_reply("failure", "encryption-required"))
- else
- session.sends2s(build_reply("failure", "invalid-mechanism"))
- end
- return true;
- end
-
- if mechanism ~= "EXTERNAL" or session.cert_chain_status ~= "valid" then
- session.sends2s(build_reply("failure", "invalid-mechanism"))
- return true;
- end
-
- local text = stanza[1]
- if not text then
- session.sends2s(build_reply("failure", "malformed-request"))
- return true
- end
-
- -- Either the value is "=" and we've already verified the external
- -- cert identity, or the value is a string and either matches the
- -- from_host (
-
- text = base64.decode(text)
- if not text then
- session.sends2s(build_reply("failure", "incorrect-encoding"))
- return true;
- end
-
- if session.cert_identity_status == "valid" then
- if text ~= "" and text ~= session.from_host then
- session.sends2s(build_reply("failure", "invalid-authzid"))
- return true
- end
- else
- if text == "" then
- session.sends2s(build_reply("failure", "invalid-authzid"))
- return true
- end
-
- local cert = session.conn:socket():getpeercertificate()
- if (cert_verify_identity(text, "xmpp-server", cert)) then
- session.cert_identity_status = "valid"
- else
- session.cert_identity_status = "invalid"
- session.sends2s(build_reply("failure", "invalid-authzid"))
- return true
- end
- end
-
- session.external_auth = "succeeded"
-
- if not session.from_host then
- session.from_host = text;
- end
- session.sends2s(build_reply("success"))
- module:log("info", "Accepting SASL EXTERNAL identity from %s", text or session.from_host);
- s2s_make_authenticated(session, text or session.from_host)
- session:reset_stream();
- return true
-end
-
-module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
- local session, stanza = event.origin, event.stanza;
- if session.type == "s2sin_unauthed" then
- return s2s_external_auth(session, stanza)
- end
-
- if session.type ~= "c2s_unauthed" then return; end
-
- if session.sasl_handler and session.sasl_handler.selected then
- session.sasl_handler = nil; -- allow starting a new SASL negotiation before completing an old one
- end
- if not session.sasl_handler then
- session.sasl_handler = usermanager_get_sasl_handler(module.host);
- end
- local mechanism = stanza.attr.mechanism;
- if not session.secure and (secure_auth_only or (mechanism == "PLAIN" and not allow_unencrypted_plain_auth)) then
- session.send(build_reply("failure", "encryption-required"));
- return true;
- end
- local valid_mechanism = session.sasl_handler:select(mechanism);
- if not valid_mechanism then
- session.send(build_reply("failure", "invalid-mechanism"));
- return true;
- end
- return sasl_process_cdata(session, stanza);
-end);
-module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:response", function(event)
- local session = event.origin;
- if not(session.sasl_handler and session.sasl_handler.selected) then
- session.send(build_reply("failure", "not-authorized", "Out of order SASL element"));
- return true;
- end
- return sasl_process_cdata(session, event.stanza);
-end);
-module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event)
- local session = event.origin;
- session.sasl_handler = nil;
- session.send(build_reply("failure", "aborted"));
- return true;
-end);
+module:add_handler("c2s_unauthed", "auth", xmlns_sasl, sasl_handler);
+module:add_handler("c2s_unauthed", "abort", xmlns_sasl, sasl_handler);
+module:add_handler("c2s_unauthed", "response", xmlns_sasl, sasl_handler);
local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' };
local bind_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-bind' };
if secure_auth_only and not origin.secure then
return;
end
- origin.sasl_handler = usermanager_get_sasl_handler(module.host);
- features:tag("mechanisms", mechanisms_attr);
- for mechanism in pairs(origin.sasl_handler:mechanisms()) do
- if mechanism ~= "PLAIN" or origin.secure or allow_unencrypted_plain_auth then
- features:tag("mechanism"):text(mechanism):up();
+ local realm = module:get_option("sasl_realm") or origin.host;
+ if module:get_option("anonymous_login") then
+ origin.sasl_handler = new_sasl(realm, anonymous_authentication_profile);
+ else
+ origin.sasl_handler = new_sasl(realm, default_authentication_profile);
+ if not (module:get_option("allow_unencrypted_plain_auth")) and not origin.secure then
+ origin.sasl_handler:forbidden({"PLAIN"});
end
end
+ features:tag("mechanisms", mechanisms_attr);
+ for k, v in pairs(origin.sasl_handler:mechanisms()) do
+ features:tag("mechanism"):text(v):up();
+ end
features:up();
else
features:tag("bind", bind_attr):tag("required"):up():up();
end
end);
-module:hook("s2s-stream-features", function(event)
- local origin, features = event.origin, event.features;
- if origin.secure and origin.type == "s2sin_unauthed" then
- -- Offer EXTERNAL if chain is valid and either we didn't validate
- -- the identity or it passed.
- if origin.cert_chain_status == "valid" and origin.cert_identity_status ~= "invalid" then --TODO: Configurable
- module:log("debug", "Offering SASL EXTERNAL")
- features:tag("mechanisms", { xmlns = xmlns_sasl })
- :tag("mechanism"):text("EXTERNAL")
- :up():up();
- end
- end
-end);
-
-module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event)
- local origin, stanza = event.origin, event.stanza;
+module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind", function(session, stanza)
+ log("debug", "Client requesting a resource bind");
local resource;
if stanza.attr.type == "set" then
local bind = stanza.tags[1];
- resource = bind:child_with_name("resource");
- resource = resource and #resource.tags == 0 and resource[1] or nil;
+ if bind and bind.attr.xmlns == xmlns_bind then
+ resource = bind:child_with_name("resource");
+ if resource then
+ resource = resource[1];
+ end
+ end
end
- local success, err_type, err, err_msg = sm_bind_resource(origin, resource);
- if success then
- origin.send(st.reply(stanza)
- :tag("bind", { xmlns = xmlns_bind })
- :tag("jid"):text(origin.full_jid));
- origin.log("debug", "Resource bound: %s", origin.full_jid);
+ local success, err_type, err, err_msg = sm_bind_resource(session, resource);
+ if not success then
+ session.send(st.error_reply(stanza, err_type, err, err_msg));
else
- origin.send(st.error_reply(stanza, err_type, err, err_msg));
- origin.log("debug", "Resource bind failed: %s", err_msg or err);
+ session.send(st.reply(stanza)
+ :tag("bind", { xmlns = xmlns_bind})
+ :tag("jid"):text(session.full_jid));
end
- return true;
end);
-local function handle_legacy_session(event)
- event.origin.send(st.reply(event.stanza));
- return true;
-end
-
-module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session);
-module:hook("iq/host/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session);
+module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-session", function(session, stanza)
+ log("debug", "Client requesting a session");
+ session.send(st.reply(stanza));
+end);
-- COPYING file in the source package for more information.
--
-local config = require "core.configmanager";
-local create_context = require "core.certmanager".create_context;
local st = require "util.stanza";
local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
local host = origin.to_host or origin.host;
local ssl_ctx = host and hosts[host].ssl_ctx_in or global_ssl_ctx;
origin.conn:starttls(ssl_ctx);
- origin.log("debug", "TLS negotiation started for %s...", origin.type);
+ origin.log("info", "TLS negotiation started for %s...", origin.type);
origin.secure = false;
else
origin.log("warn", "Attempt to start TLS, but TLS is not available on this %s connection", origin.type);
module:log("debug", "Proceeding with TLS on s2sout...");
session:reset_stream();
local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx;
- session.conn:starttls(ssl_ctx);
+ session.conn:starttls(ssl_ctx, true);
session.secure = false;
return true;
end);
-
-function module.load()
- local ssl_config = config.rawget(module.host, "core", "ssl");
- if not ssl_config then
- local base_host = module.host:match("%.(.*)");
- ssl_config = config.get(base_host, "core", "ssl");
- end
- host.ssl_ctx = create_context(host.host, "client", ssl_config); -- for outgoing connections
- host.ssl_ctx_in = create_context(host.host, "server", ssl_config); -- for incoming connections
-end
-
-function module.unload()
- host.ssl_ctx = nil;
- host.ssl_ctx_in = nil;
-end
local muc_name = module:get_option("name");
if type(muc_name) ~= "string" then muc_name = "Prosody Chatrooms"; end
local restrict_room_creation = module:get_option("restrict_room_creation");
-if restrict_room_creation then
- if restrict_room_creation == true then
- restrict_room_creation = "admin";
- elseif restrict_room_creation ~= "admin" and restrict_room_creation ~= "local" then
- restrict_room_creation = nil;
- end
-end
+if restrict_room_creation and restrict_room_creation ~= true then restrict_room_creation = nil; end
+
local muc_new_room = module:require "muc".new_room;
+local register_component = require "core.componentmanager".register_component;
+local deregister_component = require "core.componentmanager".deregister_component;
local jid_split = require "util.jid".split;
local jid_bare = require "util.jid".bare;
local st = require "util.stanza";
local datamanager = require "util.datamanager";
local um_is_admin = require "core.usermanager".is_admin;
-rooms = {};
-local rooms = rooms;
+local rooms = {};
local persistent_rooms = datamanager.load(nil, muc_host, "persistent") or {};
-local component = hosts[module.host];
-
--- Configurable options
-local max_history_messages = module:get_option_number("max_history_messages");
+local component;
local function is_admin(jid)
- return um_is_admin(jid, module.host);
+ return um_is_admin(jid) or um_is_admin(jid, module.host);
end
local function room_route_stanza(room, stanza) core_post_stanza(component, stanza); end
room._data.history = history;
elseif forced then
datamanager.store(node, muc_host, "config", nil);
- if not next(room._occupants) then -- Room empty
- rooms[room.jid] = nil;
- end
end
if forced then datamanager.store(nil, muc_host, "persistent", persistent_rooms); end
end
for jid in pairs(persistent_rooms) do
local node = jid_split(jid);
local data = datamanager.load(node, muc_host, "config") or {};
- local room = muc_new_room(jid, {
- history_length = max_history_messages;
- });
+ local room = muc_new_room(jid);
room._data = data._data;
- room._data.history_length = max_history_messages; --TODO: Need to allow per-room with a global limit
room._affiliations = data._affiliations;
room.route_stanza = room_route_stanza;
room.save = room_save;
rooms[jid] = room;
end
-local host_room = muc_new_room(muc_host, {
- history_length = max_history_messages;
-});
+local host_room = muc_new_room(muc_host);
host_room.route_stanza = room_route_stanza;
host_room.save = room_save;
local function get_disco_items(stanza)
local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
for jid, room in pairs(rooms) do
- if not room:is_hidden() then
- reply:tag("item", {jid=jid, name=room:get_name()}):up();
+ if not room._data.hidden then
+ reply:tag("item", {jid=jid, name=jid}):up();
end
end
return reply; -- TODO cache disco reply
end
end
-function stanza_handler(event)
- local origin, stanza = event.origin, event.stanza;
+component = register_component(muc_host, function(origin, stanza)
local to_node, to_host, to_resource = jid_split(stanza.attr.to);
if to_node then
local bare = to_node.."@"..to_host;
if to_host == muc_host or bare == muc_host then
local room = rooms[bare];
if not room then
- if not(restrict_room_creation) or
- (restrict_room_creation == "admin" and is_admin(stanza.attr.from)) or
- (restrict_room_creation == "local" and select(2, jid_split(stanza.attr.from)) == module.host:gsub("^[^%.]+%.", "")) then
- room = muc_new_room(bare, {
- history_length = max_history_messages;
- });
+ if not(restrict_room_creation) or is_admin(stanza.attr.from) then
+ room = muc_new_room(bare);
room.route_stanza = room_route_stanza;
room.save = room_save;
rooms[bare] = room;
origin.send(st.error_reply(stanza, "cancel", "not-allowed"));
end
else --[[not for us?]] end
- return true;
+ return;
end
-- to the main muc domain
handle_to_domain(origin, stanza);
- return true;
-end
-module:hook("iq/bare", stanza_handler, -1);
-module:hook("message/bare", stanza_handler, -1);
-module:hook("presence/bare", stanza_handler, -1);
-module:hook("iq/full", stanza_handler, -1);
-module:hook("message/full", stanza_handler, -1);
-module:hook("presence/full", stanza_handler, -1);
-module:hook("iq/host", stanza_handler, -1);
-module:hook("message/host", stanza_handler, -1);
-module:hook("presence/host", stanza_handler, -1);
-
-hosts[module.host].send = function(stanza) -- FIXME do a generic fix
+end);
+function component.send(stanza) -- FIXME do a generic fix
if stanza.attr.type == "result" or stanza.attr.type == "error" then
core_post_stanza(component, stanza);
else error("component.send only supports result and error stanzas at the moment"); end
prosody.hosts[module:get_host()].muc = { rooms = rooms };
+module.unload = function()
+ deregister_component(muc_host);
+end
module.save = function()
return {rooms = rooms};
end
module.restore = function(data)
+ rooms = {};
for jid, oldroom in pairs(data.rooms or {}) do
local room = muc_new_room(jid);
room._jid_nick = oldroom._jid_nick;
-- COPYING file in the source package for more information.
--
-local select = select;
-local pairs, ipairs = pairs, ipairs;
-
local datamanager = require "util.datamanager";
local datetime = require "util.datetime";
-local dataform = require "util.dataforms";
-
local jid_split = require "util.jid".split;
local jid_bare = require "util.jid".bare;
local jid_prep = require "util.jid".prep;
local md5 = require "util.hashes".md5;
local muc_domain = nil; --module:get_host();
-local default_history_length = 20;
+local history_length = 20;
------------
local function filter_xmlns_from_array(array, filters)
function room_mt:get_default_role(affiliation)
if affiliation == "owner" or affiliation == "admin" then
return "moderator";
- elseif affiliation == "member" then
+ elseif affiliation == "member" or not affiliation then
return "participant";
- elseif not affiliation then
- if not self:is_members_only() then
- return self:is_moderated() and "visitor" or "participant";
- end
end
end
self:broadcast_except_nick(stanza, stanza.attr.from);
local me = self._occupants[stanza.attr.from];
if me then
- stanza:tag("status", {code='110'}):up();
+ stanza:tag("status", {code='110'});
stanza.attr.to = sid;
self:_route_stanza(stanza);
end
local history = self._data['history'];
if not history then history = {}; self._data['history'] = history; end
stanza = st.clone(stanza);
- stanza.attr.to = "";
- local stamp = datetime.datetime();
- local chars = #tostring(stanza);
- stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203
+ stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203
stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
- local entry = { stanza = stanza, stamp = stamp };
- t_insert(history, entry);
- while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end
+ t_insert(history, st.preserialize(stanza));
+ while #history > history_length do t_remove(history, 1) end
end
end
function room_mt:broadcast_except_nick(stanza, nick)
end
end
end
-function room_mt:send_history(to, stanza)
+function room_mt:send_history(to)
local history = self._data['history']; -- send discussion history
if history then
- local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc");
- local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc");
-
- local maxchars = history_tag and tonumber(history_tag.attr.maxchars);
- if maxchars then maxchars = math.floor(maxchars); end
-
- local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history);
- if not history_tag then maxstanzas = 20; end
-
- local seconds = history_tag and tonumber(history_tag.attr.seconds);
- if seconds then seconds = datetime.datetime(os.time() - math.floor(seconds)); end
-
- local since = history_tag and history_tag.attr.since;
- if since then since = datetime.parse(since); since = since and datetime.datetime(since); end
- if seconds and (not since or since < seconds) then since = seconds; end
-
- local n = 0;
- local charcount = 0;
- local stanzacount = 0;
-
- for i=#history,1,-1 do
- local entry = history[i];
- if maxchars then
- if not entry.chars then
- entry.stanza.attr.to = "";
- entry.chars = #tostring(entry.stanza);
- end
- charcount = charcount + entry.chars + #to;
- if charcount > maxchars then break; end
- end
- if since and since > entry.stamp then break; end
- if n + 1 > maxstanzas then break; end
- n = n + 1;
- end
- for i=#history-n+1,#history do
- local msg = history[i].stanza;
- msg.attr.to = to;
+ for _, msg in ipairs(history) do
+ msg = st.deserialize(msg);
+ msg.attr.to=to;
self:_route_stanza(msg);
end
end
if self._data['subject'] then
- self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject']));
+ self:_route_stanza(st.message({type='groupchat', from=self.jid, to=to}):tag("subject"):text(self._data['subject']));
end
end
function room_mt:get_disco_info(stanza)
- local count = 0; for _ in pairs(self._occupants) do count = count + 1; end
return st.reply(stanza):query("http://jabber.org/protocol/disco#info")
- :tag("identity", {category="conference", type="text", name=self:get_name()}):up()
- :tag("feature", {var="http://jabber.org/protocol/muc"}):up()
- :tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up()
- :tag("feature", {var=self:is_moderated() and "muc_moderated" or "muc_unmoderated"}):up()
- :tag("feature", {var=self:is_members_only() and "muc_membersonly" or "muc_open"}):up()
- :tag("feature", {var=self:is_persistent() and "muc_persistent" or "muc_temporary"}):up()
- :tag("feature", {var=self:is_hidden() and "muc_hidden" or "muc_public"}):up()
- :tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up()
- :add_child(dataform.new({
- { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" },
- { name = "muc#roominfo_description", label = "Description"},
- { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) }
- }):form({["muc#roominfo_description"] = self:get_description()}, 'result'))
- ;
+ :tag("identity", {category="conference", type="text"}):up()
+ :tag("feature", {var="http://jabber.org/protocol/muc"});
end
function room_mt:get_disco_items(stanza)
local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
-- TODO check nick's authority
if subject == "" then subject = nil; end
self._data['subject'] = subject;
- self._data['subject_from'] = current_nick;
if self.save then self:save(); end
local msg = st.message({type='groupchat', from=current_nick})
:tag('subject'):text(subject):up();
local function build_unavailable_presence_from_error(stanza)
local type, condition, text = stanza:get_error();
- local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error");
+ local error_message = "Kicked: "..condition:gsub("%-", " ");
if text then
error_message = error_message..": "..text;
end
:tag('status'):text(error_message);
end
-function room_mt:set_name(name)
- if name == "" or type(name) ~= "string" or name == (jid_split(self.jid)) then name = nil; end
- if self._data.name ~= name then
- self._data.name = name;
- if self.save then self:save(true); end
- end
-end
-function room_mt:get_name()
- return self._data.name or jid_split(self.jid);
-end
-function room_mt:set_description(description)
- if description == "" or type(description) ~= "string" then description = nil; end
- if self._data.description ~= description then
- self._data.description = description;
- if self.save then self:save(true); end
- end
-end
-function room_mt:get_description()
- return self._data.description;
-end
-function room_mt:set_password(password)
- if password == "" or type(password) ~= "string" then password = nil; end
- if self._data.password ~= password then
- self._data.password = password;
- if self.save then self:save(true); end
- end
-end
-function room_mt:get_password()
- return self._data.password;
-end
-function room_mt:set_moderated(moderated)
- moderated = moderated and true or nil;
- if self._data.moderated ~= moderated then
- self._data.moderated = moderated;
- if self.save then self:save(true); end
- end
-end
-function room_mt:is_moderated()
- return self._data.moderated;
-end
-function room_mt:set_members_only(members_only)
- members_only = members_only and true or nil;
- if self._data.members_only ~= members_only then
- self._data.members_only = members_only;
- if self.save then self:save(true); end
- end
-end
-function room_mt:is_members_only()
- return self._data.members_only;
-end
-function room_mt:set_persistent(persistent)
- persistent = persistent and true or nil;
- if self._data.persistent ~= persistent then
- self._data.persistent = persistent;
- if self.save then self:save(true); end
- end
-end
-function room_mt:is_persistent()
- return self._data.persistent;
-end
-function room_mt:set_hidden(hidden)
- hidden = hidden and true or nil;
- if self._data.hidden ~= hidden then
- self._data.hidden = hidden;
- if self.save then self:save(true); end
- end
-end
-function room_mt:is_hidden()
- return self._data.hidden;
-end
-function room_mt:set_changesubject(changesubject)
- changesubject = changesubject and true or nil;
- if self._data.changesubject ~= changesubject then
- self._data.changesubject = changesubject;
- if self.save then self:save(true); end
- end
-end
-function room_mt:get_changesubject()
- return self._data.changesubject;
-end
-
function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
local from, to = stanza.attr.from, stanza.attr.to;
local room = jid_bare(to);
pr.attr.to = from;
pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
:tag("item", {affiliation=occupant.affiliation or "none", role='none'}):up()
- :tag("status", {code='110'}):up();
+ :tag("status", {code='110'});
self:_route_stanza(pr);
if jid ~= new_jid then
pr = st.clone(occupant.sessions[new_jid])
end
is_merge = true;
end
- local password = stanza:get_child("x", "http://jabber.org/protocol/muc");
- password = password and password:get_child("password", "http://jabber.org/protocol/muc");
- password = password and password[1] ~= "" and password[1];
- if self:get_password() and self:get_password() ~= password then
- log("debug", "%s couldn't join due to invalid password: %s", from, to);
- local reply = st.error_reply(stanza, "auth", "not-authorized"):up();
- reply.tags[1].attr.code = "401";
- origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
- elseif not new_nick then
+ if not new_nick then
log("debug", "%s couldn't join due to nick conflict: %s", from, to);
local reply = st.error_reply(stanza, "cancel", "conflict"):up();
reply.tags[1].attr.code = "409";
self._jid_nick[from] = to;
self:send_occupant_list(from);
pr.attr.from = to;
- pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
- :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up();
if not is_merge then
- self:broadcast_except_nick(pr, to);
+ self:broadcast_presence(pr, from);
+ else
+ pr.attr.to = from;
+ self:_route_stanza(pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up()
+ :tag("status", {code='110'}));
end
- pr:tag("status", {code='110'}):up();
- if self._data.whois == 'anyone' then
- pr:tag("status", {code='100'}):up();
+ if self._data.whois == 'anyone' then -- non-anonymous?
+ self:_route_stanza(st.stanza("message", {from=to, to=from, type='groupchat'})
+ :tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("status", {code='100'}));
end
- pr.attr.to = from;
- self:_route_stanza(pr);
- self:send_history(from, stanza);
- elseif not affiliation then -- registration required for entering members-only room
- local reply = st.error_reply(stanza, "auth", "registration-required"):up();
- reply.tags[1].attr.code = "407";
- origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ self:send_history(from);
else -- banned
local reply = st.error_reply(stanza, "auth", "forbidden"):up();
reply.tags[1].attr.code = "403";
end
function room_mt:send_form(origin, stanza)
+ local title = "Configuration for "..self.jid;
origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
- :add_child(self:get_form_layout():form())
+ :tag("x", {xmlns='jabber:x:data', type='form'})
+ :tag("title"):text(title):up()
+ :tag("instructions"):text(title):up()
+ :tag("field", {type='hidden', var='FORM_TYPE'}):tag("value"):text("http://jabber.org/protocol/muc#roomconfig"):up():up()
+ :tag("field", {type='boolean', label='Make Room Persistent?', var='muc#roomconfig_persistentroom'})
+ :tag("value"):text(self._data.persistent and "1" or "0"):up()
+ :up()
+ :tag("field", {type='boolean', label='Make Room Publicly Searchable?', var='muc#roomconfig_publicroom'})
+ :tag("value"):text(self._data.hidden and "0" or "1"):up()
+ :up()
+ :tag("field", {type='list-single', label='Who May Discover Real JIDs?', var='muc#roomconfig_whois'})
+ :tag("value"):text(self._data.whois or 'moderators'):up()
+ :tag("option", {label = 'Moderators Only'})
+ :tag("value"):text('moderators'):up()
+ :up()
+ :tag("option", {label = 'Anyone'})
+ :tag("value"):text('anyone'):up()
+ :up()
+ :up()
);
end
-function room_mt:get_form_layout()
- local title = "Configuration for "..self.jid;
- return dataform.new({
- title = title,
- instructions = title,
- {
- name = 'FORM_TYPE',
- type = 'hidden',
- value = 'http://jabber.org/protocol/muc#roomconfig'
- },
- {
- name = 'muc#roomconfig_roomname',
- type = 'text-single',
- label = 'Name',
- value = self:get_name() or "",
- },
- {
- name = 'muc#roomconfig_roomdesc',
- type = 'text-single',
- label = 'Description',
- value = self:get_description() or "",
- },
- {
- name = 'muc#roomconfig_persistentroom',
- type = 'boolean',
- label = 'Make Room Persistent?',
- value = self:is_persistent()
- },
- {
- name = 'muc#roomconfig_publicroom',
- type = 'boolean',
- label = 'Make Room Publicly Searchable?',
- value = not self:is_hidden()
- },
- {
- name = 'muc#roomconfig_changesubject',
- type = 'boolean',
- label = 'Allow Occupants to Change Subject?',
- value = self:get_changesubject()
- },
- {
- name = 'muc#roomconfig_whois',
- type = 'list-single',
- label = 'Who May Discover Real JIDs?',
- value = {
- { value = 'moderators', label = 'Moderators Only', default = self._data.whois == 'moderators' },
- { value = 'anyone', label = 'Anyone', default = self._data.whois == 'anyone' }
- }
- },
- {
- name = 'muc#roomconfig_roomsecret',
- type = 'text-private',
- label = 'Password',
- value = self:get_password() or "",
- },
- {
- name = 'muc#roomconfig_moderatedroom',
- type = 'boolean',
- label = 'Make Room Moderated?',
- value = self:is_moderated()
- },
- {
- name = 'muc#roomconfig_membersonly',
- type = 'boolean',
- label = 'Make Room Members-Only?',
- value = self:is_members_only()
- }
- });
-end
-
local valid_whois = {
- moderators = true,
- anyone = true,
+ moderators = true,
+ anyone = true,
}
function room_mt:process_form(origin, stanza)
for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end
if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end
if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end
- if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); return; end
-
- local fields = self:get_form_layout():data(form);
- if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); return; end
-
- local dirty = false
-
- local name = fields['muc#roomconfig_roomname'];
- if name ~= self:get_name() then
- self:set_name(name);
+ if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ local fields = {};
+ for _, field in pairs(form.tags) do
+ if field.name == "field" and field.attr.var and field.tags[1].name == "value" and #field.tags[1].tags == 0 then
+ fields[field.attr.var] = field.tags[1][1] or "";
+ end
end
+ if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
- local description = fields['muc#roomconfig_roomdesc'];
- if description ~= self:get_description() then
- self:set_description(description);
- end
+ local dirty = false
local persistent = fields['muc#roomconfig_persistentroom'];
- dirty = dirty or (self:is_persistent() ~= persistent)
+ if persistent == "0" or persistent == "false" then persistent = nil; elseif persistent == "1" or persistent == "true" then persistent = true;
+ else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ dirty = dirty or (self._data.persistent ~= persistent)
+ self._data.persistent = persistent;
module:log("debug", "persistent=%s", tostring(persistent));
- local moderated = fields['muc#roomconfig_moderatedroom'];
- dirty = dirty or (self:is_moderated() ~= moderated)
- module:log("debug", "moderated=%s", tostring(moderated));
-
- local membersonly = fields['muc#roomconfig_membersonly'];
- dirty = dirty or (self:is_members_only() ~= membersonly)
- module:log("debug", "membersonly=%s", tostring(membersonly));
-
local public = fields['muc#roomconfig_publicroom'];
- dirty = dirty or (self:is_hidden() ~= (not public and true or nil))
-
- local changesubject = fields['muc#roomconfig_changesubject'];
- dirty = dirty or (self:get_changesubject() ~= (not changesubject and true or nil))
- module:log('debug', 'changesubject=%s', changesubject and "true" or "false")
+ if public == "0" or public == "false" then public = nil; elseif public == "1" or public == "true" then public = true;
+ else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ dirty = dirty or (self._data.hidden ~= (not public and true or nil))
+ self._data.hidden = not public and true or nil;
local whois = fields['muc#roomconfig_whois'];
if not valid_whois[whois] then
- origin.send(st.error_reply(stanza, 'cancel', 'bad-request', "Invalid value for 'whois'"));
+ origin.send(st.error_reply(stanza, 'cancel', 'bad-request'));
return;
end
local whois_changed = self._data.whois ~= whois
self._data.whois = whois
- module:log('debug', 'whois=%s', whois)
-
- local password = fields['muc#roomconfig_roomsecret'];
- if self:get_password() ~= password then
- self:set_password(password);
- end
- self:set_moderated(moderated);
- self:set_members_only(membersonly);
- self:set_persistent(persistent);
- self:set_hidden(not public);
- self:set_changesubject(changesubject);
+ module:log('debug', 'whois=%s', tostring(whois))
if self.save then self:save(true); end
origin.send(st.reply(stanza));
if dirty or whois_changed then
- local msg = st.message({type='groupchat', from=self.jid})
- :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up()
+ local msg = st.message({type='groupchat', from=self.jid})
+ :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up()
- if dirty then
- msg.tags[1]:tag('status', {code = '104'}):up();
- end
- if whois_changed then
- local code = (whois == 'moderators') and "173" or "172";
- msg.tags[1]:tag('status', {code = code}):up();
- end
+ if dirty then
+ msg.tags[1]:tag('status', {code = '104'})
+ end
+ if whois_changed then
+ local code = (whois == 'moderators') and 173 or 172
+ msg.tags[1]:tag('status', {code = code})
+ end
- self:broadcast_message(msg, false)
+ self:broadcast_message(msg, false)
end
end
end
self._occupants[nick] = nil;
end
- self:set_persistent(false);
+ self._data.persistent = nil;
+ if self.save then self:save(true); end
end
function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
end
elseif xmlns == "http://jabber.org/protocol/muc#owner" and (type == "get" or type == "set") and stanza.tags[1].name == "query" then
if self:get_affiliation(stanza.attr.from) ~= "owner" then
- origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms"));
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
elseif stanza.attr.type == "get" then
self:send_form(origin, stanza);
elseif stanza.attr.type == "set" then
stanza.attr.from = current_nick;
local subject = getText(stanza, {"subject"});
if subject then
- if occupant.role == "moderator" or
- ( self._data.changesubject and occupant.role == "participant" ) then -- and participant
+ if occupant.role == "moderator" then
self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza
else
stanza.attr.from = from;
:tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
:tag('invite', {from=_from})
:tag('reason'):text(_reason or ""):up()
- :up();
- if self:get_password() then
- invite:tag("password"):text(self:get_password()):up();
- end
- invite:up()
+ :up()
+ :up()
:tag('x', {xmlns="jabber:x:conference", jid=_to}) -- COMPAT: Some older clients expect this
:text(_reason or "")
:up()
:tag('body') -- Add a plain message for clients which don't support invites
:text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or ""))
:up();
- if self:is_members_only() and not self:get_affiliation(_invitee) then
- log("debug", "%s invited %s into members only room %s, granting membership", _from, _invitee, _to);
- self:set_affiliation(_from, _invitee, "member", nil, "Invited by " .. self._jid_nick[_from])
- end
self:_route_stanza(invite);
else
origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
if affiliation and affiliation ~= "outcast" and affiliation ~= "owner" and affiliation ~= "admin" and affiliation ~= "member" then
return nil, "modify", "not-acceptable";
end
- local actor_affiliation = self:get_affiliation(actor);
- local target_affiliation = self:get_affiliation(jid);
- if target_affiliation == affiliation then -- no change, shortcut
- if callback then callback(); end
- return true;
- end
- if actor_affiliation ~= "owner" then
- if actor_affiliation ~= "admin" or target_affiliation == "owner" or target_affiliation == "admin" then
- return nil, "cancel", "not-allowed";
- end
- elseif target_affiliation == "owner" and jid_bare(actor) == jid then -- self change
- local is_last = true;
- for j, aff in pairs(self._affiliations) do if j ~= jid and aff == "owner" then is_last = false; break; end end
- if is_last then
- return nil, "cancel", "conflict";
- end
- end
+ if self:get_affiliation(actor) ~= "owner" then return nil, "cancel", "not-allowed"; end
+ if jid_bare(actor) == jid then return nil, "cancel", "not-allowed"; end
self._affiliations[jid] = affiliation;
local role = self:get_default_role(affiliation);
- local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"})
+ local p = st.presence()
+ :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
:tag("item", {affiliation=affiliation or "none", role=role or "none"})
:tag("reason"):text(reason or ""):up()
:up();
- local presence_type = nil;
+ local x = p.tags[1];
+ local item = x.tags[1];
if not role then -- getting kicked
- presence_type = "unavailable";
+ p.attr.type = "unavailable";
if affiliation == "outcast" then
x:tag("status", {code="301"}):up(); -- banned
else
if not role then -- getting kicked
self._occupants[nick] = nil;
else
+ t_insert(modified_nicks, nick);
occupant.affiliation, occupant.role = affiliation, role;
end
- for jid,pres in pairs(occupant.sessions) do -- remove for all sessions of the nick
+ p.attr.from = nick;
+ for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick
if not role then self._jid_nick[jid] = nil; end
- local p = st.clone(pres);
- p.attr.from = nick;
- p.attr.type = presence_type;
p.attr.to = jid;
- p:add_child(x);
self:_route_stanza(p);
- if occupant.jid == jid then
- modified_nicks[nick] = p;
- end
end
end
end
if self.save then self:save(); end
if callback then callback(); end
- for nick,p in pairs(modified_nicks) do
+ for _, nick in ipairs(modified_nicks) do
p.attr.from = nick;
self:broadcast_except_nick(p, nick);
end
local session = self._occupants[nick];
return session and session.role or nil;
end
-function room_mt:can_set_role(actor_jid, occupant_jid, role)
- local actor = self._occupants[self._jid_nick[actor_jid]];
- local occupant = self._occupants[occupant_jid];
-
- if not occupant or not actor then return nil, "modify", "not-acceptable"; end
-
- if actor.role == "moderator" then
- if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then
- if actor.affiliation == "owner" or actor.affiliation == "admin" then
- return true;
- elseif occupant.role ~= "moderator" and role ~= "moderator" then
- return true;
- end
- end
- end
- return nil, "cancel", "not-allowed";
-end
function room_mt:set_role(actor, occupant_jid, role, callback, reason)
if role == "none" then role = nil; end
if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end
- local allowed, err_type, err_condition = self:can_set_role(actor, occupant_jid, role);
- if not allowed then return allowed, err_type, err_condition; end
+ if self:get_role(self._jid_nick[actor]) ~= "moderator" then return nil, "cancel", "not-allowed"; end
local occupant = self._occupants[occupant_jid];
- local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"})
+ if not occupant then return nil, "modify", "not-acceptable"; end
+ if occupant.affiliation == "owner" or occupant.affiliation == "admin" then return nil, "cancel", "not-allowed"; end
+ local p = st.presence({from = occupant_jid})
+ :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
:tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"})
:tag("reason"):text(reason or ""):up()
:up();
- local presence_type = nil;
if not role then -- kick
- presence_type = "unavailable";
+ p.attr.type = "unavailable";
self._occupants[occupant_jid] = nil;
for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick
self._jid_nick[jid] = nil;
end
- x:tag("status", {code = "307"}):up();
+ p:tag("status", {code = "307"}):up();
else
occupant.role = role;
end
- local bp;
- for jid,pres in pairs(occupant.sessions) do -- send to all sessions of the nick
- local p = st.clone(pres);
- p.attr.from = occupant_jid;
- p.attr.type = presence_type;
+ for jid in pairs(occupant.sessions) do -- send to all sessions of the nick
p.attr.to = jid;
- p:add_child(x);
self:_route_stanza(p);
- if occupant.jid == jid then
- bp = p;
- end
end
if callback then callback(); end
- if bp then
- self:broadcast_except_nick(bp, occupant_jid);
- end
+ self:broadcast_except_nick(p, occupant_jid);
return true;
end
local _M = {}; -- module "muc"
-function _M.new_room(jid, config)
+function _M.new_room(jid)
return setmetatable({
jid = jid;
_jid_nick = {};
_occupants = {};
_data = {
- whois = 'moderators';
- history_length = (config and config.history_length);
+ whois = 'moderators',
};
_affiliations = {};
}, room_mt);
-- COPYING file in the source package for more information.
--
--- prosody - main executable for Prosody XMPP server
-
-- Will be modified by configure script if run --
CFG_SOURCEDIR=os.getenv("PROSODY_SRCDIR");
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-local function is_relative(path)
- local path_sep = package.config:sub(1,1);
- return ((path_sep == "/" and path:sub(1,1) ~= "/")
- or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
-end
-
-- Tell Lua where to find our libraries
if CFG_SOURCEDIR then
- local function filter_relative_paths(path)
- if is_relative(path) then return ""; end
- end
- local function sanitise_paths(paths)
- return (paths:gsub("[^;]+;?", filter_relative_paths):gsub(";;+", ";"));
- end
- package.path = sanitise_paths(CFG_SOURCEDIR.."/?.lua;"..package.path);
- package.cpath = sanitise_paths(CFG_SOURCEDIR.."/?.so;"..package.cpath);
+ package.path = CFG_SOURCEDIR.."/?.lua;"..package.path;
+ package.cpath = CFG_SOURCEDIR.."/?.so;"..package.cpath;
end
+package.path = package.path..";"..(CFG_SOURCEDIR or ".").."/fallbacks/?.lua";
+package.cpath = package.cpath..";"..(CFG_SOURCEDIR or ".").."/fallbacks/?.so";
+
-- Substitute ~ with path to home directory in data path
if CFG_DATADIR then
if os.getenv("HOME") then
end
end
--- Global 'prosody' object
-local prosody = { events = require "util.events".new(); };
-_G.prosody = prosody;
-
--- Check dependencies
-local dependencies = require "util.dependencies";
-if not dependencies.check_dependencies() then
- os.exit(1);
-end
-
-- Load the config-parsing module
config = require "core.configmanager"
print("\n");
print("**************************");
if level == "parser" then
- print("A problem occured while reading the config file "..(CFG_CONFIGDIR or ".").."/prosody.cfg.lua"..":");
- print("");
+ print("A problem occured while reading the config file "..(CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
local err_line, err_message = tostring(err):match("%[string .-%]:(%d*): (.*)");
- if err:match("chunk has too many syntax levels$") then
- print("An Include statement in a config file is including an already-included");
- print("file and causing an infinite loop. An Include statement in a config file is...");
- else
- print("Error"..(err_line and (" on line "..err_line) or "")..": "..(err_message or tostring(err)));
- end
+ print("Error"..(err_line and (" on line "..err_line) or "")..": "..(err_message or tostring(err)));
print("");
elseif level == "file" then
print("Prosody was unable to find the configuration file.");
require "core.loggingmanager"
end
-function log_dependency_warnings()
- dependencies.log_warnings();
-end
-
-function sanity_check()
- for host, host_config in pairs(configmanager.getconfig()) do
- if host ~= "*"
- and host_config.core.enabled ~= false
- and not host_config.core.component_module then
- return;
- end
+function check_dependencies()
+ -- Check runtime dependencies
+ if not require "util.dependencies".check_dependencies() then
+ os.exit(1);
end
- log("error", "No enabled VirtualHost entries found in the config file.");
- log("error", "At least one active host is required for Prosody to function. Exiting...");
- os.exit(1);
end
function sandbox_require()
full_sessions = {};
hosts = {};
+ -- Global 'prosody' object
+ prosody = {};
+ local prosody = prosody;
+
prosody.bare_sessions = bare_sessions;
prosody.full_sessions = full_sessions;
prosody.hosts = hosts;
- local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data";
- local custom_plugin_paths = config.get("*", "core", "plugin_paths");
- if custom_plugin_paths then
- local path_sep = package.config:sub(3,3);
- -- path1;path2;path3;defaultpath...
- CFG_PLUGINDIR = table.concat(custom_plugin_paths, path_sep)..path_sep..(CFG_PLUGINDIR or "plugins");
- end
prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR,
- plugins = CFG_PLUGINDIR or "plugins", data = data_path };
-
+ plugins = CFG_PLUGINDIR, data = CFG_DATADIR };
+
prosody.arg = _G.arg;
+ prosody.events = require "util.events".new();
+
prosody.platform = "unknown";
if os.getenv("WINDIR") then
prosody.platform = "windows";
-- Function to reopen logfiles
function prosody.reopen_logfiles()
log("info", "Re-opening log files");
+ eventmanager.fire_event("reopen-log-files"); -- Handled by appropriate log sinks
prosody.events.fire_event("reopen-log-files");
end
function load_secondary_libraries()
--- Load and initialise core modules
require "util.import"
- require "util.xmppstream"
+ require "core.xmlhandlers"
require "core.rostermanager"
+ require "core.eventmanager"
require "core.hostmanager"
require "core.modulemanager"
require "core.usermanager"
require "core.sessionmanager"
require "core.stanza_router"
- package.loaded['core.componentmanager'] = setmetatable({},{__index=function()
- log("warn", "componentmanager is deprecated: %s", debug.traceback():match("\n[^\n]*\n[\s\t]*([^\n]*)"));
- return function() end
- end});
require "net.http"
]]
require "net.connlisteners";
- require "net.httpserver";
require "util.stanza"
require "util.jid"
end
function init_data_store()
- require "core.storagemanager";
+ local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data";
+ require "util.datamanager".set_data_path(data_path);
+ require "util.datamanager".add_callback(function(username, host, datastore, data)
+ if config.get(host, "core", "anonymous_login") then
+ return false;
+ end
+ return username, host, datastore, data;
+ end);
end
function prepare_to_start()
log("info", "Prosody is using the %s backend for connection handling", server.get_backend());
-- Signal to modules that we are ready to start
+ eventmanager.fire_event("server-starting");
prosody.events.fire_event("server-starting");
-- start listening on sockets
-- previous steps to have already been performed
read_config();
init_logging();
-sanity_check();
+check_dependencies();
sandbox_require();
set_function_metatable();
load_libraries();
init_global_state();
read_version();
log("info", "Hello and welcome to Prosody version %s", prosody.version);
-log_dependency_warnings();
load_secondary_libraries();
init_data_store();
init_global_protection();
prepare_to_start();
+eventmanager.fire_event("server-started");
prosody.events.fire_event("server-started");
loop();
log("info", "Shutting down...");
cleanup();
+eventmanager.fire_event("server-stopped");
prosody.events.fire_event("server-stopped");
log("info", "Shutdown complete");
-- Prosody Example Configuration File
---
+--
-- Information on configuring Prosody can be found on our
-- website at http://prosody.im/doc/configure
---
+--
-- Tip: You can check that the syntax of this file is correct
-- when you have finished by running: luac -p prosody.cfg.lua
-- If there are any errors, it will let you know what and where
"ping"; -- Replies to XMPP pings with pongs
"pep"; -- Enables users to publish their mood, activity, playing music and more
"register"; -- Allow users to register on this server using a client and change passwords
- "adhoc"; -- Support for "ad-hoc commands" that can be executed with an XMPP client
-
- -- Admin interfaces
- "admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
- --"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-- Other specific functionality
--"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
+ --"console"; -- Opens admin telnet interface on localhost port 5582
--"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
--"httpserver"; -- Serve static files from a directory over HTTP
--"groups"; -- Shared roster support
--"announce"; -- Send announcement to all online users
--"welcome"; -- Welcome users who register accounts
--"watchregistrations"; -- Alert admins of registrations
- --"motd"; -- Send a message to users when they log in
};
-- These modules are auto-loaded, should you
--- (for some mad reason) want to disable
+-- for (for some mad reason) want to disable
-- them then uncomment them below
modules_disabled = {
- -- "presence"; -- Route user/contact status information
- -- "message"; -- Route messages
- -- "iq"; -- Route info queries
- -- "offline"; -- Store offline messages
+ -- "presence";
+ -- "message";
+ -- "iq";
};
-- Disable account creation by default, for security
-- For more information see http://prosody.im/doc/creating_accounts
allow_registration = false;
-
+
-- These are the SSL/TLS-related settings. If you don't want
-- to use SSL/TLS, you may comment or remove this
ssl = {
certificate = "certs/localhost.cert";
}
--- Only allow encrypted streams? Encryption is already used when
--- available. These options will cause Prosody to deny connections that
--- are not encrypted. Note that some servers do not support s2s
--- encryption or have it disabled, including gmail.com and Google Apps
--- domains.
-
+-- Require encryption on client/server connections?
--c2s_require_encryption = false
--s2s_require_encryption = false
--- Select the authentication backend to use. The 'internal' providers
--- use Prosody's configured data storage to store the authentication data.
--- To allow Prosody to offer secure authentication mechanisms to clients, the
--- default provider stores passwords in plaintext. If you do not trust your
--- server please see http://prosody.im/doc/modules/mod_auth_internal_hashed
--- for information about using the hashed backend.
-
-authentication = "internal_plain"
-
--- Select the storage backend to use. By default Prosody uses flat files
--- in its configured data directory, but it also supports more backends
--- through modules. An "sql" backend is included by default, but requires
--- additional dependencies. See http://prosody.im/doc/storage for more info.
-
---storage = "sql" -- Default is "internal"
-
--- For the "sql" backend, you can uncomment *one* of the below to configure:
---sql = { driver = "SQLite3", database = "prosody.sqlite" } -- Default. 'database' is the filename.
---sql = { driver = "MySQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
---sql = { driver = "PostgreSQL", database = "prosody", username = "prosody", password = "secret", host = "localhost" }
-
-- Logging configuration
-- For advanced logging see http://prosody.im/doc/logging
-log = {
- info = "prosody.log"; -- Change 'info' to 'debug' for verbose logging
- error = "prosody.err";
- -- "*syslog"; -- Uncomment this for logging to syslog
- -- "*console"; -- Log to the console, useful for debugging with daemonize=false
-}
+log = "prosody.log";
+debug = false; -- Log debug messages?
----------- Virtual hosts -----------
-- You need to add a VirtualHost entry for each domain you wish Prosody to serve.
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
- ssl = {
+ ssl = {
key = "certs/example.com.key";
certificate = "certs/example.com.crt";
}
--Component "proxy.example.com" "proxy65"
---Set up an external component (default component port is 5347)
---
--- External components allow adding various services, such as gateways/
--- transports to other networks like ICQ, MSN and Yahoo. For more info
--- see: http://prosody.im/doc/components#adding_an_external_component
---
--Component "gateway.example.com"
-- component_secret = "password"
#!/usr/bin/env lua
-- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
+-- Copyright (C) 2008-2009 Matthew Wild
+-- Copyright (C) 2008-2009 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
-- Will be modified by configure script if run --
-CFG_SOURCEDIR=os.getenv("PROSODY_SRCDIR");
+CFG_SOURCEDIR=nil;
CFG_CONFIGDIR=os.getenv("PROSODY_CFGDIR");
-CFG_PLUGINDIR=os.getenv("PROSODY_PLUGINDIR");
+CFG_PLUGINDIR=nil;
CFG_DATADIR=os.getenv("PROSODY_DATADIR");
--- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- -- -- -- -- -- -- ---- -- -- -- -- -- -- -- --
-local function is_relative(path)
- local path_sep = package.config:sub(1,1);
- return ((path_sep == "/" and path:sub(1,1) ~= "/")
- or (path_sep == "\\" and (path:sub(1,1) ~= "/" and path:sub(2,3) ~= ":\\")))
-end
-
--- Tell Lua where to find our libraries
if CFG_SOURCEDIR then
- local function filter_relative_paths(path)
- if is_relative(path) then return ""; end
- end
- local function sanitise_paths(paths)
- return (paths:gsub("[^;]+;?", filter_relative_paths):gsub(";;+", ";"));
- end
- package.path = sanitise_paths(CFG_SOURCEDIR.."/?.lua;"..package.path);
- package.cpath = sanitise_paths(CFG_SOURCEDIR.."/?.so;"..package.cpath);
+ package.path = CFG_SOURCEDIR.."/?.lua;"..package.path
+ package.cpath = CFG_SOURCEDIR.."/?.so;"..package.cpath
end
--- Substitute ~ with path to home directory in data path
if CFG_DATADIR then
if os.getenv("HOME") then
CFG_DATADIR = CFG_DATADIR:gsub("^~", os.getenv("HOME"));
end
end
--- Global 'prosody' object
-local prosody = {
- hosts = {};
- events = require "util.events".new();
- platform = "posix";
- lock_globals = function () end;
- unlock_globals = function () end;
-};
-_G.prosody = prosody;
-
-local dependencies = require "util.dependencies";
-if not dependencies.check_dependencies() then
- os.exit(1);
-end
-
config = require "core.configmanager"
do
- local filenames = {};
-
- local filename;
- if arg[1] == "--config" and arg[2] then
- table.insert(filenames, arg[2]);
- table.remove(arg, 1); table.remove(arg, 1);
- if CFG_CONFIGDIR then
- table.insert(filenames, CFG_CONFIGDIR.."/"..arg[2]);
- end
- else
- for _, format in ipairs(config.parsers()) do
- table.insert(filenames, (CFG_CONFIGDIR or ".").."/prosody.cfg."..format);
- end
- end
- for _,_filename in ipairs(filenames) do
- filename = _filename;
- local file = io.open(filename);
- if file then
- file:close();
- CFG_CONFIGDIR = filename:match("^(.*)[\\/][^\\/]*$");
- break;
- end
- end
- local ok, level, err = config.load(filename);
+ -- TODO: Check for other formats when we add support for them
+ -- Use lfs? Make a new conf/ dir?
+ local ok, level, err = config.load((CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
if not ok then
print("\n");
print("**************************");
os.exit(1);
end
end
-local original_logging_config = config.get("*", "core", "log");
-config.set("*", "core", "log", { { levels = { min="info" }, to = "console" } });
-local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data";
-local custom_plugin_paths = config.get("*", "core", "plugin_paths");
-if custom_plugin_paths then
- local path_sep = package.config:sub(3,3);
- -- path1;path2;path3;defaultpath...
- CFG_PLUGINDIR = table.concat(custom_plugin_paths, path_sep)..path_sep..(CFG_PLUGINDIR or "plugins");
+require "core.loggingmanager"
+
+if not require "util.dependencies".check_dependencies() then
+ os.exit(1);
end
-prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR,
- plugins = CFG_PLUGINDIR or "plugins", data = data_path };
-require "core.loggingmanager"
+prosody = { hosts = {}, events = events, platform = "posix" };
-dependencies.log_warnings();
+local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data";
+require "util.datamanager".set_data_path(data_path);
-- Switch away from root and into the prosody user --
local switched_user, current_uid;
-local want_pposix_version = "0.3.5";
+local want_pposix_version = "0.3.3";
local ok, pposix = pcall(require, "util.pposix");
if ok and pposix then
local desired_user = config.get("*", "core", "prosody_user") or "prosody";
local desired_group = config.get("*", "core", "prosody_group") or desired_user;
local ok, err = pposix.setgid(desired_group);
- if ok then
- ok, err = pposix.initgroups(desired_user);
- end
if ok then
ok, err = pposix.setuid(desired_user);
if ok then
print(tostring(pposix))
end
-local function test_writeable(filename)
- local f, err = io.open(filename, "a");
- if not f then
- return false, err;
- end
- f:close();
- return true;
-end
-
-local unwriteable_files = {};
-if type(original_logging_config) == "string" and original_logging_config:sub(1,1) ~= "*" then
- local ok, err = test_writeable(original_logging_config);
- if not ok then
- table.insert(unwriteable_files, err);
- end
-elseif type(original_logging_config) == "table" then
- for _, rule in ipairs(original_logging_config) do
- if rule.filename then
- local ok, err = test_writeable(rule.filename);
- if not ok then
- table.insert(unwriteable_files, err);
- end
- end
- end
-end
-
-if #unwriteable_files > 0 then
- print("One of more of the Prosody log files are not");
- print("writeable, please correct the errors and try");
- print("starting prosodyctl again.");
- print("");
- for _, err in ipairs(unwriteable_files) do
- print(err);
- end
- print("");
- os.exit(1);
-end
-
-
local error_messages = setmetatable({
["invalid-username"] = "The given username is invalid in a Jabber ID";
["invalid-hostname"] = "The given hostname is invalid";
["no-such-user"] = "The given user does not exist on the server";
["unable-to-save-data"] = "Unable to store, perhaps you don't have permission?";
["no-pidfile"] = "There is no 'pidfile' option in the configuration file, see http://prosody.im/doc/prosodyctl#pidfile for help";
- ["no-posix"] = "The mod_posix module is not enabled in the Prosody config file, see http://prosody.im/doc/prosodyctl for more info";
["no-such-method"] = "This module has no commands";
["not-running"] = "Prosody is not running";
}, { __index = function (t,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end });
-hosts = prosody.hosts;
+local events = require "util.events".new();
-local function make_host(hostname)
- return {
- type = "local",
- events = prosody.events,
- users = require "core.usermanager".new_null_provider(hostname)
- };
-end
+hosts = prosody.hosts;
for hostname, config in pairs(config.getconfig()) do
- hosts[hostname] = make_host(hostname);
+ hosts[hostname] = { events = events };
end
require "core.modulemanager"
require "socket"
-----------------------
-local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning;
-local show_usage = prosodyctl.show_usage;
-local getchar, getpass = prosodyctl.getchar, prosodyctl.getpass;
-local show_yesno = prosodyctl.show_yesno;
-local read_password = prosodyctl.read_password;
+function show_message(msg, ...)
+ print(msg:format(...));
+end
+
+function show_warning(msg, ...)
+ print(msg:format(...));
+end
+
+function show_usage(usage, desc)
+ print("Usage: "..arg[0].." "..usage);
+ if desc then
+ print(" "..desc);
+ end
+end
+
+local function getchar(n)
+ local stty_ret = os.execute("stty raw -echo 2>/dev/null");
+ local ok, char;
+ if stty_ret == 0 then
+ ok, char = pcall(io.read, n or 1);
+ os.execute("stty sane");
+ else
+ ok, char = pcall(io.read, "*l");
+ if ok then
+ char = char:sub(1, n or 1);
+ end
+ end
+ if ok then
+ return char;
+ end
+end
+
+local function getpass()
+ local stty_ret = os.execute("stty -echo 2>/dev/null");
+ if stty_ret ~= 0 then
+ io.write("\027[08m"); -- ANSI 'hidden' text attribute
+ end
+ local ok, pass = pcall(io.read, "*l");
+ if stty_ret == 0 then
+ os.execute("stty sane");
+ else
+ io.write("\027[00m");
+ end
+ io.write("\n");
+ if ok then
+ return pass;
+ end
+end
+
+function show_yesno(prompt)
+ io.write(prompt, " ");
+ local choice = getchar():lower();
+ io.write("\n");
+ if not choice:match("%a") then
+ choice = prompt:match("%[.-(%U).-%]$");
+ if not choice then return nil; end
+ end
+ return (choice == "y");
+end
+
+local function read_password()
+ local password;
+ while true do
+ io.write("Enter new password: ");
+ password = getpass();
+ if not password then
+ show_message("No password - cancelled");
+ return;
+ end
+ io.write("Retype new password: ");
+ if getpass() ~= password then
+ if not show_yesno [=[Passwords did not match, try again? [Y/n]]=] then
+ return;
+ end
+ else
+ break;
+ end
+ end
+ return password;
+end
local prosodyctl_timeout = (config.get("*", "core", "prosodyctl_timeout") or 5) * 2;
-----------------------
return 1;
end
- if not hosts[host] then
- show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
- show_warning("The user will not be able to log in until this is changed.");
- hosts[host] = make_host(host);
- end
-
if prosodyctl.user_exists{ user = user, host = host } then
show_message [[That user already exists]];
return 1;
end
+ if not hosts[host] then
+ show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
+ show_warning("The user will not be able to log in until this is changed.");
+ end
+
local password = read_password();
if not password then return 1; end
if ok then return 0; end
- show_message(msg)
+ show_message(error_messages[msg])
return 1;
end
return 1;
end
- if not hosts[host] then
- show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
- show_warning("The user will not be able to log in until this is changed.");
- hosts[host] = make_host(host);
- end
-
if not prosodyctl.user_exists { user = user, host = host } then
show_message [[That user does not exist, use prosodyctl adduser to create a new user]]
return 1;
return 1;
end
- if not hosts[host] then
- show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
- show_warning("The user will not be able to log in until this is changed.");
- hosts[host] = make_host(host);
- end
-
if not prosodyctl.user_exists { user = user, host = host } then
show_message [[That user does not exist on this server]]
return 1;
return 1;
end
- commands.stop(arg);
- return commands.start(arg);
+ local ret = commands.stop(arg);
+ if ret == 0 then
+ ret = commands.start(arg);
+ end
+ return ret;
end
-- ejabberdctl compatibility
dotest "core.s2smanager"
dotest "core.configmanager"
dotest "util.stanza"
- dotest "util.sasl.scram"
dosingletest("test_sasl.lua", "latin1toutf8");
end
for line, active in pairs(lines_hit) do
if active ~= nil then total_active_lines = total_active_lines + 1; end
if coverage_file then
- if active == false then coverage_file:write(fn, "|", line, "|", name or "", "|miss\n");
+ if active == false then coverage_file:write(fn, "|", line, "|", name or "", "|miss\n");
else coverage_file:write(fn, "|", line, "|", name or "", "|", tostring(success), "\n"); end
end
end
local lxp = require "lxp";
local st = require "util.stanza";
-local xmppstream = require "util.xmppstream";
-local new_xmpp_handlers = xmppstream.new_sax_handlers;
+local init_xmlhandlers = require "core.xmlhandlers";
local dm = require "util.datamanager"
dm.set_data_path("data");
-local ns_separator = xmppstream.ns_separator;
-local ns_pattern = xmppstream.ns_pattern;
-
-local xmlns_xep227 = "http://www.xmpp.org/extensions/xep-0227.html#ns";
+local ns_separator = "\1";
+local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$";
+local ns_xep227 = "http://www.xmpp.org/extensions/xep-0227.html#ns";
-----------------------------------------------------------------------
--print("message :"..ch:pretty_print());
local ret, err = dm.list_append(username, host, "offline", st.preserialize(ch));
print("["..(err or "success").."] stored offline message: " ..username.."@"..host.." - "..ch.attr.from);
- end
+ end
end
local cb = {
stream_tag = "user",
- stream_ns = xmlns_xep227,
+ stream_ns = ns_xep227,
};
function cb.streamopened(session, attr)
session.notopen = false;
end
end
-local user_handlers = new_xmpp_handlers({ notopen = true }, cb);
+local user_handlers = init_xmlhandlers({ notopen = true, }, cb);
-----------------------------------------------------------------------
if curr_host ~= "" then
-- forward to xmlhandlers
user_handlers:StartElement(elementname, attributes);
- elseif (curr_ns == xmlns_xep227) and (name == "host") then
+ elseif (curr_ns == ns_xep227) and (name == "host") then
curr_host = attributes["jid"]; -- start of host element
print("Begin parsing host "..curr_host);
- elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
+ elseif (curr_ns ~= ns_xep227) or (name ~= "server-data") then
io.stderr:write("Unhandled XML element: ", name, "\n");
os.exit(1);
end
--count = count - 1;
--io.write("- ", string.rep(" ", count), name, " (", curr_ns, ")", "\n")
if curr_host ~= "" then
- if (curr_ns == xmlns_xep227) and (name == "host") then
+ if (curr_ns == ns_xep227) and (name == "host") then
print("End parsing host "..curr_host);
curr_host = "" -- end of host element
else
-- forward to xmlhandlers
user_handlers:EndElement(elementname);
end
- elseif (curr_ns ~= xmlns_xep227) or (name ~= "server-data") then
+ elseif (curr_ns ~= ns_xep227) or (name ~= "server-data") then
io.stderr:write("Unhandled XML element: ", name, "\n");
os.exit(1);
end
IDN_LIB?=idn
OPENSSL_LIB?=crypto
CC?=gcc
-CXX?=g++
LD?=gcc
.SUFFIXES: .c .o .so
-encodings.so: encodings.o
- MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET;
- $(CC) -o $@ $< $(LDFLAGS) $(IDNA_LIBS)
-
-hashes.so: hashes.o
- MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET;
- $(CC) -o $@ $< $(LDFLAGS) -l$(OPENSSL_LIB)
-
.c.o:
$(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o $@ $<
.o.so:
MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET;
- $(LD) -o $@ $< $(LDFLAGS)
+ $(LD) $(LDFLAGS) -o $@ $< -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) -lidn -lcrypto
all: encodings.so hashes.so pposix.so signal.so
* POSIX support functions for Lua
*/
-#define MODULE_VERSION "0.3.5"
+#define MODULE_VERSION "0.3.3"
#include <stdlib.h>
#include <math.h>
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/stat.h>
-#include <sys/utsname.h>
#include <fcntl.h>
#include <syslog.h>
return 2;
}
-int lc_initgroups(lua_State* L)
-{
- int ret;
- gid_t gid;
- struct passwd *p;
-
- if(!lua_isstring(L, 1))
- {
- lua_pushnil(L);
- lua_pushstring(L, "invalid-username");
- return 2;
- }
- p = getpwnam(lua_tostring(L, 1));
- if(!p)
- {
- lua_pushnil(L);
- lua_pushstring(L, "no-such-user");
- return 2;
- }
- if(lua_gettop(L) < 2)
- lua_pushnil(L);
- switch(lua_type(L, 2))
- {
- case LUA_TNIL:
- gid = p->pw_gid;
- break;
- case LUA_TNUMBER:
- gid = lua_tointeger(L, 2);
- break;
- default:
- lua_pushnil(L);
- lua_pushstring(L, "invalid-gid");
- return 2;
- }
- ret = initgroups(lua_tostring(L, 1), gid);
- switch(errno)
- {
- case 0:
- lua_pushboolean(L, 1);
- lua_pushnil(L);
- break;
- case ENOMEM:
- lua_pushnil(L);
- lua_pushstring(L, "no-memory");
- break;
- case EPERM:
- lua_pushnil(L);
- lua_pushstring(L, "permission-denied");
- break;
- default:
- lua_pushnil(L);
- lua_pushstring(L, "unknown-error");
- }
- return 2;
-}
-
int lc_umask(lua_State* L)
{
char old_mode_string[7];
return 0;
}
-int lc_uname(lua_State* L)
-{
- struct utsname uname_info;
- if(uname(&uname_info) != 0)
- {
- lua_pushnil(L);
- lua_pushstring(L, strerror(errno));
- return 2;
- }
- lua_newtable(L);
- lua_pushstring(L, uname_info.sysname);
- lua_setfield(L, -2, "sysname");
- lua_pushstring(L, uname_info.nodename);
- lua_setfield(L, -2, "nodename");
- lua_pushstring(L, uname_info.release);
- lua_setfield(L, -2, "release");
- lua_pushstring(L, uname_info.version);
- lua_setfield(L, -2, "version");
- lua_pushstring(L, uname_info.machine);
- lua_setfield(L, -2, "machine");
- return 1;
-}
-
/* Register functions */
int luaopen_util_pposix(lua_State *L)
{ "setuid", lc_setuid },
{ "setgid", lc_setgid },
- { "initgroups", lc_initgroups },
{ "umask", lc_umask },
{ "setrlimit", lc_setrlimit },
{ "getrlimit", lc_getrlimit },
- { "uname", lc_uname },
-
{ NULL, NULL }
};
lua_setfield(L, -2, "_VERSION");
return 1;
-}
+};
static void sighook(lua_State *L, lua_Debug *ar)
{
- struct signal_event *event;
/* restore the old hook */
lua_sethook(L, Hsig, Hmask, Hcount);
lua_pushstring(L, LUA_SIGNAL);
lua_gettable(L, LUA_REGISTRYINDEX);
+ struct signal_event *event;
while((event = signal_queue))
{
lua_pushnumber(L, event->Nsig);
return 1;
}
-#if defined(__unix__) || defined(__APPLE__)
+#if defined _POSIX_SOURCE || (defined(sun) || defined(__sun))
/* define some posix only functions */
static const struct luaL_Reg lsignal_lib[] = {
{"signal", l_signal},
{"raise", l_raise},
-#if defined(__unix__) || defined(__APPLE__)
+#if defined _POSIX_SOURCE || (defined(sun) || defined(__sun))
{"kill", l_kill},
#endif
{NULL, NULL}
local append = require "util.serialization".append;
local path_separator = "/"; if os.getenv("WINDIR") then path_separator = "\\" end
local lfs = require "lfs";
-local prosody = prosody;
local raw_mkdir;
if prosody.platform == "posix" then
return path;
end
-local data_path = (prosody and prosody.paths and prosody.paths.data) or ".";
+local data_path = "data";
local callbacks = {};
------- API -------------
if not data then
local mode = lfs.attributes(getpath(username, host, datastore), "mode");
if not mode then
- log("debug", "Assuming empty "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
+ log("debug", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
return nil;
else -- file exists, but can't be read
-- TODO more detailed error checking and logging?
function list_load(username, host, datastore)
local data, ret = loadfile(getpath(username, host, datastore, "list"));
if not data then
- local mode = lfs.attributes(getpath(username, host, datastore, "list"), "mode");
- if not mode then
- log("debug", "Assuming empty "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
- return nil;
- else -- file exists, but can't be read
- -- TODO more detailed error checking and logging?
- log("error", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
- return nil, "Error reading storage";
- end
+ log("debug", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
+ return nil;
end
local items = {};
setfenv(data, {item = function(i) t_insert(items, i); end});
local success, ret = pcall(data);
if not success then
log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
- return nil, "Error reading storage";
+ return nil;
end
return items;
end
print("");
end
--- COMPAT w/pre-0.8 Debian: The Debian config file used to use
--- util.ztact, which has been removed from Prosody in 0.8. This
--- is to log an error for people who still use it, so they can
--- update their configs.
-package.preload["util.ztact"] = function ()
- if not package.loaded["core.loggingmanager"] then
- error("util.ztact has been removed from Prosody and you need to fix your config "
- .."file. More information can be found at http://prosody.im/doc/packagers#ztact", 0);
- else
- error("module 'util.ztact' has been deprecated in Prosody 0.8.");
- end
-end;
-
function check_dependencies()
local fatal;
["luarocks"] = "luarocks install luasec";
["Source"] = "http://www.inf.puc-rio.br/~brunoos/luasec/";
}, "SSL/TLS support will not be available");
+ else
+ local major, minor, veryminor, patched = ssl._VERSION:match("(%d+)%.(%d+)%.?(%d*)(M?)");
+ if not major or ((tonumber(major) == 0 and (tonumber(minor) or 0) <= 3 and (tonumber(veryminor) or 0) <= 2) and patched ~= "M") then
+ log("error", "This version of LuaSec contains a known bug that causes disconnects, see http://prosody.im/doc/depends");
+ end
end
local encodings, err = softreq "util.encodings"
return not fatal;
end
-function log_warnings()
- if ssl then
- local major, minor, veryminor, patched = ssl._VERSION:match("(%d+)%.(%d+)%.?(%d*)(M?)");
- if not major or ((tonumber(major) == 0 and (tonumber(minor) or 0) <= 3 and (tonumber(veryminor) or 0) <= 2) and patched ~= "M") then
- log("error", "This version of LuaSec contains a known bug that causes disconnects, see http://prosody.im/doc/depends");
- end
- end
-end
return _M;
--
+local ipairs = ipairs;
local pairs = pairs;
local t_insert = table.insert;
local t_sort = table.sort;
-local setmetatable = setmetatable;
-local next = next;
+local select = select;
module "events"
function new()
+ local dispatchers = {};
local handlers = {};
local event_map = {};
- local function _rebuild_index(handlers, event)
+ local function _rebuild_index(event) -- TODO optimize index rebuilding
local _handlers = event_map[event];
- if not _handlers or next(_handlers) == nil then return; end
- local index = {};
+ local index = handlers[event];
+ if index then
+ for i=#index,1,-1 do index[i] = nil; end
+ else index = {}; handlers[event] = index; end
for handler in pairs(_handlers) do
t_insert(index, handler);
end
t_sort(index, function(a, b) return _handlers[a] > _handlers[b]; end);
- handlers[event] = index;
- return index;
end;
- setmetatable(handlers, { __index = _rebuild_index });
local function add_handler(event, handler, priority)
local map = event_map[event];
if map then
map = {[handler] = priority or 0};
event_map[event] = map;
end
- handlers[event] = nil;
+ _rebuild_index(event);
end;
local function remove_handler(event, handler)
local map = event_map[event];
if map then
map[handler] = nil;
- handlers[event] = nil;
- if next(map) == nil then
- event_map[event] = nil;
- end
+ _rebuild_index(event);
end
end;
local function add_handlers(handlers)
remove_handler(event, handler);
end
end;
- local function fire_event(event, ...)
+ local function _create_dispatcher(event) -- FIXME duplicate code in fire_event
+ local h = handlers[event];
+ if not h then h = {}; handlers[event] = h; end
+ local dispatcher = function(...)
+ for i=1,#h do
+ local ret = h[i](...);
+ if ret ~= nil then return ret; end
+ end
+ end;
+ dispatchers[event] = dispatcher;
+ return dispatcher;
+ end;
+ local function get_dispatcher(event)
+ return dispatchers[event] or _create_dispatcher(event);
+ end;
+ local function fire_event(event, ...) -- FIXME duplicates dispatcher code
local h = handlers[event];
if h then
for i=1,#h do
end
end
end;
+ local function get_named_arg_dispatcher(event, ...)
+ local dispatcher = get_dispatcher(event);
+ local keys = {...};
+ local data = {};
+ return function(...)
+ for i, key in ipairs(keys) do data[key] = select(i, ...); end
+ dispatcher(data);
+ end;
+ end;
return {
add_handler = add_handler;
remove_handler = remove_handler;
- add_handlers = add_handlers;
- remove_handlers = remove_handlers;
+ add_plugin = add_plugin;
+ remove_plugin = remove_plugin;
+ get_dispatcher = get_dispatcher;
fire_event = fire_event;
+ get_named_arg_dispatcher = get_named_arg_dispatcher;
+ _dispatchers = dispatchers;
_handlers = handlers;
_event_map = event_map;
};
blocksize
the blocksize for the hash function in bytes
hex
- return raw hash or hexadecimal string
+ return raw hash or hexadecimal string
--]]
function hmac(key, message, hash, blocksize, hex)
if #key > blocksize then
-- COPYING file in the source package for more information.
--
-local dir_sep, path_sep = package.config:match("^(%S+)%s(%S+)");
-local plugin_dir = {};
-for path in (CFG_PLUGINDIR or "./plugins/"):gsub("[/\\]", dir_sep):gmatch("[^"..path_sep.."]+") do
- path = path..dir_sep; -- add path separator to path end
- path = path:gsub(dir_sep..dir_sep.."+", dir_sep); -- coalesce multiple separaters
- plugin_dir[#plugin_dir + 1] = path;
-end
+
+local plugin_dir = CFG_PLUGINDIR or "./plugins/";
local io_open, os_time = io.open, os.time;
local loadstring, pairs = loadstring, pairs;
+local datamanager = require "util.datamanager";
+
module "pluginloader"
-local function load_file(names)
- local file, err, path;
- for i=1,#plugin_dir do
- for j=1,#names do
- path = plugin_dir[i]..names[j];
- file, err = io_open(path);
- if file then
- local content = file:read("*a");
- file:close();
- return content, path;
- end
- end
- end
- return file, err;
+local function load_file(name)
+ local file, err = io_open(plugin_dir..name);
+ if not file then return file, err; end
+ local content = file:read("*a");
+ file:close();
+ return content, name;
end
-function load_resource(plugin, resource)
- resource = resource or "mod_"..plugin..".lua";
-
- local names = {
- "mod_"..plugin.."/"..plugin.."/"..resource; -- mod_hello/hello/mod_hello.lua
- "mod_"..plugin.."/"..resource; -- mod_hello/mod_hello.lua
- plugin.."/"..resource; -- hello/mod_hello.lua
- resource; -- mod_hello.lua
- };
+function load_resource(plugin, resource, loader)
+ if not resource then
+ resource = "mod_"..plugin..".lua";
+ end
+ loader = loader or load_file;
- return load_file(names);
+ local content, err = loader(plugin.."/"..resource);
+ if not content then content, err = loader(resource); end
+ -- TODO add support for packed plugins
+
+ return content, err;
end
function load_code(plugin, resource)
local content, err = load_resource(plugin, resource);
if not content then return content, err; end
- local path = err;
- local f, err = loadstring(content, "@"..path);
- if not f then return f, err; end
- return f, path;
+ return loadstring(content, "@"..err);
end
return _M;
local config = require "core.configmanager";
local encodings = require "util.encodings";
local stringprep = encodings.stringprep;
-local storagemanager = require "core.storagemanager";
local usermanager = require "core.usermanager";
local signal = require "util.signal";
-local set = require "util.set";
local lfs = require "lfs";
-local pcall = pcall;
local nodeprep, nameprep = stringprep.nodeprep, stringprep.nameprep;
local io, os = io, os;
-local print = print;
local tostring, tonumber = tostring, tonumber;
local CFG_SOURCEDIR = _G.CFG_SOURCEDIR;
-local _G = _G;
-local prosody = prosody;
-
module "prosodyctl"
--- UI helpers
-function show_message(msg, ...)
- print(msg:format(...));
-end
-
-function show_warning(msg, ...)
- print(msg:format(...));
-end
-
-function show_usage(usage, desc)
- print("Usage: ".._G.arg[0].." "..usage);
- if desc then
- print(" "..desc);
- end
-end
-
-function getchar(n)
- local stty_ret = os.execute("stty raw -echo 2>/dev/null");
- local ok, char;
- if stty_ret == 0 then
- ok, char = pcall(io.read, n or 1);
- os.execute("stty sane");
- else
- ok, char = pcall(io.read, "*l");
- if ok then
- char = char:sub(1, n or 1);
- end
- end
- if ok then
- return char;
- end
-end
-
-function getpass()
- local stty_ret = os.execute("stty -echo 2>/dev/null");
- if stty_ret ~= 0 then
- io.write("\027[08m"); -- ANSI 'hidden' text attribute
- end
- local ok, pass = pcall(io.read, "*l");
- if stty_ret == 0 then
- os.execute("stty sane");
- else
- io.write("\027[00m");
- end
- io.write("\n");
- if ok then
- return pass;
- end
-end
-
-function show_yesno(prompt)
- io.write(prompt, " ");
- local choice = getchar():lower();
- io.write("\n");
- if not choice:match("%a") then
- choice = prompt:match("%[.-(%U).-%]$");
- if not choice then return nil; end
- end
- return (choice == "y");
-end
-
-function read_password()
- local password;
- while true do
- io.write("Enter new password: ");
- password = getpass();
- if not password then
- show_message("No password - cancelled");
- return;
- end
- io.write("Retype new password: ");
- if getpass() ~= password then
- if not show_yesno [=[Passwords did not match, try again? [Y/n]]=] then
- return;
- end
- else
- break;
- end
- end
- return password;
-end
-
--- Server control
function adduser(params)
local user, host, password = nodeprep(params.user), nameprep(params.host), params.password;
if not user then
elseif not host then
return false, "invalid-hostname";
end
-
- local provider = prosody.hosts[host].users;
- if not(provider) or provider.name == "null" then
- usermanager.initialize_host(host);
- end
- storagemanager.initialize_host(host);
- local ok, errmsg = usermanager.create_user(user, password, host);
+ local ok = usermanager.create_user(user, password, host);
if not ok then
- return false, errmsg;
+ return false, "unable-to-save-data";
end
return true;
end
function user_exists(params)
- local user, host, password = nodeprep(params.user), nameprep(params.host), params.password;
- local provider = prosody.hosts[host].users;
- if not(provider) or provider.name == "null" then
- usermanager.initialize_host(host);
- end
- storagemanager.initialize_host(host);
-
- return usermanager.user_exists(user, host);
+ return usermanager.user_exists(params.user, params.host);
end
function passwd(params)
return false, "no-pidfile";
end
- local modules_enabled = set.new(config.get("*", "core", "modules_enabled"));
- if not modules_enabled:contains("posix") then
- return false, "no-posix";
- end
-
local file, err = io.open(pidfile, "r+");
if not file then
return false, "pidfile-read-failed", err;
-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+local md5 = require "util.hashes".md5;
+local log = require "util.logger".init("sasl");
+local st = require "util.stanza";
+local set = require "util.set";
+local array = require "util.array";
+local to_unicode = require "util.encodings".idna.to_unicode;
+
+local tostring = tostring;
local pairs, ipairs = pairs, ipairs;
-local t_insert = table.insert;
+local t_insert, t_concat = table.insert, table.concat;
+local s_match = string.match;
local type = type
+local error = error
local setmetatable = setmetatable;
local assert = assert;
local require = require;
+require "util.iterators"
+local keys = keys
+
+local array = require "util.array"
module "sasl"
--[[
end
-- create a new SASL object which can be used to authenticate clients
-function new(realm, profile)
- local mechanisms = profile.mechanisms;
- if not mechanisms then
- mechanisms = {};
- for backend, f in pairs(profile) do
- if backend_mechanism[backend] then
- for _, mechanism in ipairs(backend_mechanism[backend]) do
- mechanisms[mechanism] = true;
- end
- end
- end
- profile.mechanisms = mechanisms;
- end
- return setmetatable({ profile = profile, realm = realm, mechs = mechanisms }, method);
+function new(realm, profile, forbidden)
+ local sasl_i = {profile = profile};
+ sasl_i.realm = realm;
+ local s = setmetatable(sasl_i, method);
+ if forbidden == nil then forbidden = {} end
+ s:forbidden(forbidden)
+ return s;
end
--- get a fresh clone with the same realm and profile
+-- get a fresh clone with the same realm, profiles and forbidden mechanisms
function method:clean_clone()
- return new(self.realm, self.profile)
+ return new(self.realm, self.profile, self:forbidden())
+end
+
+-- set the forbidden mechanisms
+function method:forbidden( restrict )
+ if restrict then
+ -- set forbidden
+ self.restrict = set.new(restrict);
+ else
+ -- get forbidden
+ return array.collect(self.restrict:items());
+ end
end
-- get a list of possible SASL mechanims to use
function method:mechanisms()
- return self.mechs;
+ local mechanisms = {}
+ for backend, f in pairs(self.profile) do
+ if backend_mechanism[backend] then
+ for _, mechanism in ipairs(backend_mechanism[backend]) do
+ if not self.restrict:contains(mechanism) then
+ mechanisms[mechanism] = true;
+ end
+ end
+ end
+ end
+ self["possible_mechanisms"] = mechanisms;
+ return array.collect(keys(mechanisms));
end
-- select a mechanism to use
function method:select(mechanism)
- if not self.selected and self.mechs[mechanism] then
- self.selected = mechanism;
- return true;
+ if self.mech_i then
+ return false;
end
+
+ self.mech_i = mechanisms[mechanism]
+ if self.mech_i == nil then
+ return false;
+ end
+ return true;
end
-- feed new messages to process into the library
function method:process(message)
--if message == "" or message == nil then return "failure", "malformed-request" end
- return mechanisms[self.selected](self, message);
+ return self.mech_i(self, message);
end
-- load the mechanisms
-require "util.sasl.plain" .init(registerMechanism);
-require "util.sasl.digest-md5".init(registerMechanism);
-require "util.sasl.anonymous" .init(registerMechanism);
-require "util.sasl.scram" .init(registerMechanism);
+local load_mechs = {"plain", "digest-md5", "anonymous", "scram"}
+for _, mech in ipairs(load_mechs) do
+ local name = "util.sasl."..mech;
+ local m = require(name);
+ m.init(registerMechanism)
+end
return _M;
local log = require "util.logger".init("sasl");
local generate_uuid = require "util.uuid".generate;
-module "sasl.anonymous"
+module "anonymous"
--=========================
--SASL ANONYMOUS according to RFC 4505
-
---[[
-Supported Authentication Backends
-
-anonymous:
- function(username, realm)
- return true; --for normal usage just return true; if you don't like the supplied username you can return false.
- end
-]]
-
local function anonymous(self, message)
local username;
repeat
username = generate_uuid();
- until self.profile.anonymous(self, username, self.realm);
- self.username = username;
+ until self.profile.anonymous(username, self.realm);
+ self["username"] = username;
return "success"
end
registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
end
-return _M;
+return _M;
\ No newline at end of file
local log = require "util.logger".init("sasl");
local generate_uuid = require "util.uuid".generate;
-module "sasl.digest-md5"
+module "digest-md5"
--=========================
--SASL DIGEST-MD5 according to RFC 2831
self.username = response["username"];
local Y, state;
if self.profile.plain then
- local password, state = self.profile.plain(self, response["username"], self.realm)
+ local password, state = self.profile.plain(response["username"], self.realm)
if state == nil then return "failure", "not-authorized"
elseif state == false then return "failure", "account-disabled" end
Y = md5(response["username"]..":"..response["realm"]..":"..password);
elseif self.profile["digest-md5"] then
- Y, state = self.profile["digest-md5"](self, response["username"], self.realm, response["realm"], response["charset"])
+ Y, state = self.profile["digest-md5"](response["username"], self.realm, response["realm"], response["charset"])
if state == nil then return "failure", "not-authorized"
elseif state == false then return "failure", "account-disabled" end
elseif self.profile["digest-md5-test"] then
registerMechanism("DIGEST-MD5", {"plain"}, digest);
end
-return _M;
+return _M;
\ No newline at end of file
local saslprep = require "util.encodings".stringprep.saslprep;
local log = require "util.logger".init("sasl");
-module "sasl.plain"
+module "plain"
-- ================================
-- SASL PLAIN according to RFC 4616
end
plain_test:
- function(username, password, realm)
+ function(username, realm, password)
return true or false, state;
end
]]
local correct, state = false, false;
if self.profile.plain then
local correct_password;
- correct_password, state = self.profile.plain(self, authentication, self.realm);
- correct = (correct_password == password);
+ correct_password, state = self.profile.plain(authentication, self.realm);
+ if correct_password == password then correct = true; else correct = false; end
elseif self.profile.plain_test then
- correct, state = self.profile.plain_test(self, authentication, password, self.realm);
+ correct, state = self.profile.plain_test(authentication, self.realm, password);
end
self.username = authentication
local char = string.char;
local byte = string.byte;
-module "sasl.scram"
+module "scram"
--=========================
---SASL SCRAM-SHA-1 according to RFC 5802
+--SASL SCRAM-SHA-1 according to draft-ietf-sasl-scram-10
--[[
Supported Authentication Backends
scram_{MECH}:
-- MECH being a standard hash name (like those at IANA's hash registry) with '-' replaced with '_'
function(username, realm)
- return stored_key, server_key, iteration_count, salt, state;
+ return salted_password, iteration_count, salt, state;
end
]]
end
-- hash algorithm independent Hi(PBKDF2) implementation
-function Hi(hmac, str, salt, i)
+local function Hi(hmac, str, salt, i)
local Ust = hmac(str, salt.."\0\0\0\1");
- local res = Ust;
+ local res = Ust;
for n=1,i-1 do
local Und = hmac(str, Ust)
res = binaryXOR(res, Und)
local function validate_username(username)
-- check for forbidden char sequences
for eq in username:gmatch("=(.?.?)") do
- if eq ~= "2C" and eq ~= "3D" then
- return false
- end
+ if eq ~= "2D" and eq ~= "3D" then
+ return false
+ end
end
- -- replace =2C with , and =3D with =
- username = username:gsub("=2C", ",");
+ -- replace =2D with , and =3D with =
+ username = username:gsub("=2D", ",");
username = username:gsub("=3D", "=");
-- apply SASLprep
return username;
end
-local function hashprep(hashname)
- return hashname:lower():gsub("-", "_");
+local function hashprep( hashname )
+ local hash = hashname:lower()
+ hash = hash:gsub("-", "_")
+ return hash
end
-function getAuthenticationDatabaseSHA1(password, salt, iteration_count)
+function saltedPasswordSHA1(password, salt, iteration_count)
+ local salted_password
if type(password) ~= "string" or type(salt) ~= "string" or type(iteration_count) ~= "number" then
return false, "inappropriate argument types"
end
if iteration_count < 4096 then
log("warn", "Iteration count < 4096 which is the suggested minimum according to RFC 5802.")
end
- local salted_password = Hi(hmac_sha1, password, salt, iteration_count);
- local stored_key = sha1(hmac_sha1(salted_password, "Client Key"))
- local server_key = hmac_sha1(salted_password, "Server Key");
- return true, stored_key, server_key
+
+ return true, Hi(hmac_sha1, password, salt, iteration_count);
end
local function scram_gen(hash_name, H_f, HMAC_f)
-- retreive credentials
if self.profile.plain then
- local password, state = self.profile.plain(self, self.state.name, self.realm)
+ local password, state = self.profile.plain(self.state.name, self.realm)
if state == nil then return "failure", "not-authorized"
elseif state == false then return "failure", "account-disabled" end
self.state.iteration_count = default_i;
local succ = false;
- succ, self.state.stored_key, self.state.server_key = getAuthenticationDatabaseSHA1(password, self.state.salt, default_i, self.state.iteration_count);
+ succ, self.state.salted_password = saltedPasswordSHA1(password, self.state.salt, default_i, self.state.iteration_count);
if not succ then
- log("error", "Generating authentication database failed. Reason: %s", self.state.stored_key);
+ log("error", "Generating salted password failed. Reason: %s", self.state.salted_password);
return "failure", "temporary-auth-failure";
end
elseif self.profile["scram_"..hashprep(hash_name)] then
- local stored_key, server_key, iteration_count, salt, state = self.profile["scram_"..hashprep(hash_name)](self, self.state.name, self.realm);
+ local salted_password, iteration_count, salt, state = self.profile["scram_"..hashprep(hash_name)](self.state.name, self.realm);
if state == nil then return "failure", "not-authorized"
elseif state == false then return "failure", "account-disabled" end
- self.state.stored_key = stored_key;
- self.state.server_key = server_key;
+ self.state.salted_password = salted_password;
self.state.iteration_count = iteration_count;
self.state.salt = salt
end
return "failure", "malformed-request", "Wrong nonce in client-final-message.";
end
- local ServerKey = self.state.server_key;
- local StoredKey = self.state.stored_key;
-
+ local SaltedPassword = self.state.salted_password;
+ local ClientKey = HMAC_f(SaltedPassword, "Client Key")
+ local ServerKey = HMAC_f(SaltedPassword, "Server Key")
+ local StoredKey = H_f(ClientKey)
local AuthMessage = "n=" .. s_match(self.state.client_first_message,"n=(.+)") .. "," .. self.state.server_first_message .. "," .. s_match(client_final_message, "(.+),p=.+")
local ClientSignature = HMAC_f(StoredKey, AuthMessage)
- local ClientKey = binaryXOR(ClientSignature, base64.decode(self.state.proof))
+ local ClientProof = binaryXOR(ClientKey, ClientSignature)
local ServerSignature = HMAC_f(ServerKey, AuthMessage)
- if StoredKey == H_f(ClientKey) then
+ if base64.encode(ClientProof) == self.state.proof then
local server_final_message = "v="..base64.encode(ServerSignature);
self["username"] = self.state.name;
return "success", server_final_message;
local cyrussasl = require "cyrussasl";
local log = require "util.logger".init("sasl_cyrus");
+local array = require "util.array";
+local tostring = tostring;
+local pairs, ipairs = pairs, ipairs;
+local t_insert, t_concat = table.insert, table.concat;
+local s_match = string.match;
local setmetatable = setmetatable
+local keys = keys;
+
+local print = print
local pcall = pcall
local s_match, s_gmatch = string.match, string.gmatch
-- create a new SASL object which can be used to authenticate clients
function new(realm, service_name, app_name)
+ local sasl_i = {};
init(app_name or service_name);
+ sasl_i.realm = realm;
+ sasl_i.service_name = service_name;
+
local st, ret = pcall(cyrussasl.server_new, service_name, nil, realm, nil, nil)
- if not st then
+ if st then
+ sasl_i.cyrus = ret;
+ else
log("error", "Creating SASL server connection failed: %s", ret);
return nil;
end
- local sasl_i = { realm = realm, service_name = service_name, cyrus = ret };
-
if cyrussasl.set_canon_cb then
local c14n_cb = function (user)
local node = s_match(user, "^([^@]+)");
end
cyrussasl.setssf(sasl_i.cyrus, 0, 0xffffffff)
- local mechanisms = {};
- local cyrus_mechs = cyrussasl.listmech(sasl_i.cyrus, nil, "", " ", "");
- for w in s_gmatch(cyrus_mechs, "[^ ]+") do
- mechanisms[w] = true;
- end
- sasl_i.mechs = mechanisms;
- return setmetatable(sasl_i, method);
+ local s = setmetatable(sasl_i, method);
+ return s;
end
--- get a fresh clone with the same realm and service name
+-- get a fresh clone with the same realm, profiles and forbidden mechanisms
function method:clean_clone()
return new(self.realm, self.service_name)
end
+-- set the forbidden mechanisms
+function method:forbidden( restrict )
+ log("warn", "Called method:forbidden. NOT IMPLEMENTED.")
+ return {}
+end
+
-- get a list of possible SASL mechanims to use
function method:mechanisms()
- return self.mechs;
+ local mechanisms = {}
+ local cyrus_mechs = cyrussasl.listmech(self.cyrus, nil, "", " ", "")
+ for w in s_gmatch(cyrus_mechs, "[^ ]+") do
+ mechanisms[w] = true;
+ end
+ self.mechs = mechanisms
+ return array.collect(keys(mechanisms));
end
-- select a mechanism to use
function method:select(mechanism)
- if not self.selected and self.mechs[mechanism] then
- self.selected = mechanism;
- return true;
- end
+ self.mechanism = mechanism;
+ if not self.mechs then self:mechanisms(); end
+ return self.mechs[mechanism];
end
-- feed new messages to process into the library
local err;
local data;
- if not self.first_step_done then
- err, data = cyrussasl.server_start(self.cyrus, self.selected, message or "")
- self.first_step_done = true;
+ if self.mechanism then
+ err, data = cyrussasl.server_start(self.cyrus, self.mechanism, message or "")
else
err, data = cyrussasl.server_step(self.cyrus, message or "")
end
self.username = cyrussasl.get_username(self.cyrus)
if (err == 0) then -- SASL_OK
- if self.require_provisioning and not self.require_provisioning(self.username) then
- return "failure", "not-authorized", "User authenticated successfully, but not provisioned for XMPP";
- end
- return "success", data
+ return "success", data
elseif (err == 1) then -- SASL_CONTINUE
- return "challenge", data
+ return "challenge", data
elseif (err == -4) then -- SASL_NOMECH
- log("debug", "SASL mechanism not available from remote end")
- return "failure", "invalid-mechanism", "SASL mechanism not available"
+ log("debug", "SASL mechanism not available from remote end")
+ return "failure", "invalid-mechanism", "SASL mechanism not available"
elseif (err == -13) then -- SASL_BADAUTH
- return "failure", "not-authorized", sasl_errstring[err];
+ return "failure", "not-authorized", sasl_errstring[err];
else
- log("debug", "Got SASL error condition %d: %s", err, sasl_errstring[err]);
- return "failure", "undefined-condition", sasl_errstring[err];
+ log("debug", "Got SASL error condition %d: %s", err, sasl_errstring[err]);
+ return "failure", "undefined-condition", sasl_errstring[err];
end
end
stanza_mt = { __type = "stanza" };
stanza_mt.__index = stanza_mt;
-local stanza_mt = stanza_mt;
function stanza(name, attr)
- local stanza = { name = name, attr = attr or {}, tags = {} };
+ local stanza = { name = name, attr = attr or {}, tags = {}, last_add = {}};
return setmetatable(stanza, stanza_mt);
end
-local stanza = stanza;
function stanza_mt:query(xmlns)
return self:tag("query", { xmlns = xmlns });
function stanza_mt:tag(name, attrs)
local s = stanza(name, attrs);
- local last_add = self.last_add;
- if not last_add then last_add = {}; self.last_add = last_add; end
- (last_add[#last_add] or self):add_direct_child(s);
- t_insert(last_add, s);
+ (self.last_add[#self.last_add] or self):add_direct_child(s);
+ t_insert(self.last_add, s);
return self;
end
function stanza_mt:text(text)
- local last_add = self.last_add;
- (last_add and last_add[#last_add] or self):add_direct_child(text);
+ (self.last_add[#self.last_add] or self):add_direct_child(text);
return self;
end
function stanza_mt:up()
- local last_add = self.last_add;
- if last_add then t_remove(last_add); end
+ t_remove(self.last_add);
return self;
end
function stanza_mt:reset()
- self.last_add = nil;
+ local last_add = self.last_add;
+ for i = 1,#last_add do
+ last_add[i] = nil;
+ end
return self;
end
end
function stanza_mt:add_child(child)
- local last_add = self.last_add;
- (last_add and last_add[#last_add] or self):add_direct_child(child);
+ (self.last_add[#self.last_add] or self):add_direct_child(child);
return self;
end
end
end
-function stanza_mt:get_child_text(name, xmlns)
- local tag = self:get_child(name, xmlns);
- if tag then
- return tag:get_text();
- end
- return nil;
-end
-
function stanza_mt:child_with_name(name)
for _, child in ipairs(self.tags) do
if child.name == name then return child; end
local i = 0;
return function (a)
i = i + 1
- return a[i];
+ local v = a[i]
+ if v then return v; end
end, self, i;
end
-
-function stanza_mt:childtags(name, xmlns)
- xmlns = xmlns or self.attr.xmlns;
- local tags = self.tags;
- local start_i, max_i = 1, #tags;
- return function ()
- for i = start_i, max_i do
- local v = tags[i];
- if (not name or v.name == name)
- and (not xmlns or xmlns == v.attr.xmlns) then
- start_i = i+1;
- return v;
- end
- end
- end;
-end
-
-function stanza_mt:maptags(callback)
- local tags, curr_tag = self.tags, 1;
- local n_children, n_tags = #self, #tags;
-
- local i = 1;
- while curr_tag <= n_tags do
- if self[i] == tags[curr_tag] then
- local ret = callback(self[i]);
- if ret == nil then
- t_remove(self, i);
- t_remove(tags, curr_tag);
- n_children = n_children - 1;
- n_tags = n_tags - 1;
- else
- self[i] = ret;
- tags[i] = ret;
- end
- i = i + 1;
- curr_tag = curr_tag + 1;
- end
- end
- return self;
+function stanza_mt:childtags()
+ local i = 0;
+ return function (a)
+ i = i + 1
+ local v = self.tags[i]
+ if v then return v; end
+ end, self.tags[1], i;
end
local xml_escape
end
type = error_tag.attr.type;
- for child in error_tag:childtags() do
+ for child in error_tag:children() do
if child.attr.xmlns == xmlns_stanzas then
if not text and child.name == "text" then
text = child:get_text();
end
end
end
- return type, condition or "undefined-condition", text;
+ return type, condition or "undefined-condition", text or "";
end
function stanza_mt.__add(s1, s2)
end
end
stanza.tags = tags;
+ if not stanza.last_add then
+ stanza.last_add = {};
+ end
end
end
return stanza;
end
-local function _clone(stanza)
- local attr, tags = {}, {};
- for k,v in pairs(stanza.attr) do attr[k] = v; end
- local new = { name = stanza.name, attr = attr, tags = tags };
- for i=1,#stanza do
- local child = stanza[i];
- if child.name then
- child = _clone(child);
- t_insert(tags, child);
+function clone(stanza)
+ local lookup_table = {};
+ local function _copy(object)
+ if type(object) ~= "table" then
+ return object;
+ elseif lookup_table[object] then
+ return lookup_table[object];
+ end
+ local new_table = {};
+ lookup_table[object] = new_table;
+ for index, value in pairs(object) do
+ new_table[_copy(index)] = _copy(value);
end
- t_insert(new, child);
+ return setmetatable(new_table, getmetatable(object));
end
- return setmetatable(new, stanza_mt);
+
+ return _copy(stanza)
end
-clone = _clone;
function message(attr, body)
if not body then
return stanza("message", attr);
else
- return stanza("message", attr):tag("body"):text(body):up();
+ return stanza("message", attr):tag("body"):text(body);
end
end
function iq(attr)
local event = require "net.server".event;
local event_base = require "net.server".event_base;
-local math_min = math.min
-local math_huge = math.huge
-local get_time = require "socket".gettime;
+local get_time = os.time;
local t_insert = table.insert;
local t_remove = table.remove;
local ipairs, pairs = ipairs, pairs;
new_data = {};
end
- local next_time = math_huge;
for i, d in pairs(data) do
local t, func = d[1], d[2];
if t <= current_time then
data[i] = nil;
local r = func(current_time);
- if type(r) == "number" then
- _add_task(r, func);
- next_time = math_min(next_time, r);
- end
- else
- next_time = math_min(next_time, t - current_time);
+ if type(r) == "number" then _add_task(r, func); end
end
end
- return next_time;
end);
else
local EVENT_LEAVE = (event.core and event.core.LEAVE) or -1;