aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/certmanager.lua118
-rw-r--r--core/configmanager.lua126
-rw-r--r--core/hostmanager.lua54
-rw-r--r--core/loggingmanager.lua110
-rw-r--r--core/moduleapi.lua371
-rw-r--r--core/modulemanager.lua474
-rw-r--r--core/portmanager.lua239
-rw-r--r--core/rostermanager.lua41
-rw-r--r--core/s2smanager.lua598
-rw-r--r--core/sessionmanager.lua132
-rw-r--r--core/stanza_router.lua68
-rw-r--r--core/storagemanager.lua115
-rw-r--r--core/usermanager.lua59
13 files changed, 1236 insertions, 1269 deletions
diff --git a/core/certmanager.lua b/core/certmanager.lua
index 0dc0bfd4..caa4afce 100644
--- a/core/certmanager.lua
+++ b/core/certmanager.lua
@@ -1,7 +1,7 @@
-- 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.
--
@@ -11,49 +11,109 @@ local log = require "util.logger".init("certmanager");
local ssl = ssl;
local ssl_newcontext = ssl and ssl.newcontext;
-local setmetatable, tostring = setmetatable, tostring;
+local tostring = tostring;
+local pairs = pairs;
+local type = type;
+local io_open = io.open;
local prosody = prosody;
local resolve_path = configmanager.resolve_relative_path;
local config_path = prosody.paths.config;
+local luasec_has_noticket, luasec_has_verifyext, luasec_has_no_compression;
+if ssl then
+ local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)");
+ luasec_has_noticket = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=4;
+ luasec_has_verifyext = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=5;
+ luasec_has_no_compression = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=5;
+end
+
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" };
+local global_ssl_config = configmanager.get("*", "ssl");
+
+local core_defaults = {
+ capath = "/etc/ssl/certs";
+ protocol = "sslv23";
+ verify = (ssl and ssl.x509 and { "peer", "client_once", }) or "none";
+ options = { "no_sslv2", luasec_has_noticket and "no_ticket" or nil };
+ verifyext = { "lsec_continue", "lsec_ignore_purpose" };
+ curve = "secp384r1";
+ ciphers = "HIGH:!DSS:!aNULL@STRENGTH";
+}
+local path_options = { -- These we pass through resolve_path()
+ key = true, certificate = true, cafile = true, capath = true, dhparam = true
+}
+
+if ssl and not luasec_has_verifyext and ssl.x509 then
+ -- COMPAT mw/luasec-hg
+ for i=1,#core_defaults.verifyext do -- Remove lsec_ prefix
+ core_defaults.verify[#core_defaults.verify+1] = core_defaults.verifyext[i]:sub(6);
+ end
+end
+
+if luasec_has_no_compression and configmanager.get("*", "ssl_compression") ~= true then
+ core_defaults.options[#core_defaults.options+1] = "no_compression";
+end
function create_context(host, mode, user_ssl_config)
- user_ssl_config = user_ssl_config or default_ssl_config;
+ user_ssl_config = user_ssl_config or {}
+ user_ssl_config.mode = mode;
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;
- };
-
- local ctx, err = ssl_newcontext(ssl_config);
+
+ if global_ssl_config then
+ for option,default_value in pairs(global_ssl_config) do
+ if not user_ssl_config[option] then
+ user_ssl_config[option] = default_value;
+ end
+ end
+ end
+ for option,default_value in pairs(core_defaults) do
+ if not user_ssl_config[option] then
+ user_ssl_config[option] = default_value;
+ end
+ end
+ user_ssl_config.password = user_ssl_config.password or function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end;
+ for option in pairs(path_options) do
+ if type(user_ssl_config[option]) == "string" then
+ user_ssl_config[option] = resolve_path(config_path, user_ssl_config[option]);
+ end
+ end
+
+ if not user_ssl_config.key then return nil, "No key present in SSL/TLS configuration for "..host; end
+ if not user_ssl_config.certificate then return nil, "No certificate present in SSL/TLS configuration for "..host; end
+
+ -- LuaSec expects dhparam to be a callback that takes two arguments.
+ -- We ignore those because it is mostly used for having a separate
+ -- set of params for EXPORT ciphers, which we don't have by default.
+ if type(user_ssl_config.dhparam) == "string" then
+ local f, err = io_open(user_ssl_config.dhparam);
+ if not f then return nil, "Could not open DH parameters: "..err end
+ local dhparam = f:read("*a");
+ f:close();
+ user_ssl_config.dhparam = function() return dhparam; end
+ end
+
+ local ctx, err = ssl_newcontext(user_ssl_config);
+
+ -- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care
+ -- of it ourselves (W/A for #x)
+ if ctx and user_ssl_config.ciphers then
+ local success;
+ success, err = ssl.context.setcipher(ctx, user_ssl_config.ciphers);
+ if not success then ctx = nil; end
+ end
+
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";
+ file = user_ssl_config.key or "your private key";
elseif file == "certificate" then
- file = ssl_config.certificate or "your certificate file";
+ file = user_ssl_config.certificate or "your certificate file";
end
local reason = err:match("%((.+)%)$") or "some reason";
if reason == "Permission denied" then
@@ -67,16 +127,16 @@ function create_context(host, mode, user_ssl_config)
else
reason = "Reason: "..tostring(reason):lower();
end
- log("error", "SSL/TLS: Failed to load %s: %s", file, reason);
+ log("error", "SSL/TLS: Failed to load '%s': %s (for %s)", file, reason, host);
else
- log("error", "SSL/TLS: Error initialising for host %s: %s", host, err );
+ log("error", "SSL/TLS: Error initialising for %s: %s", host, err);
end
end
return ctx, err;
end
function reload_ssl_config()
- default_ssl_config = configmanager.get("*", "core", "ssl");
+ global_ssl_config = configmanager.get("*", "ssl");
end
prosody.events.add_handler("config-reloaded", reload_ssl_config);
diff --git a/core/configmanager.lua b/core/configmanager.lua
index b703bb8c..d92120d0 100644
--- a/core/configmanager.lua
+++ b/core/configmanager.lua
@@ -1,18 +1,19 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local _G = _G;
-local setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table =
- setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table;
+local setmetatable, rawget, rawset, io, error, dofile, type, pairs, table =
+ setmetatable, rawget, rawset, io, error, dofile, type, pairs, table;
local format, math_max = string.format, math.max;
local fire_event = prosody and prosody.events.fire_event or function () end;
+local envload = require"util.envload".envload;
local lfs = require "lfs";
local path_sep = package.config:sub(1,1);
@@ -21,65 +22,62 @@ module "configmanager"
local parsers = {};
local config_mt = { __index = function (t, k) return rawget(t, "*"); end};
-local config = setmetatable({ ["*"] = { core = {} } }, config_mt);
+local config = setmetatable({ ["*"] = { } }, config_mt);
-- When host not found, use global
-local host_mt = { };
-
--- 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
- };
-end
+local host_mt = { __index = function(_, k) return config["*"][k] end }
function getconfig()
return config;
end
-function get(host, section, key)
- local sec = config[host][section];
- if sec then
- return sec[key];
+function get(host, key, _oldkey)
+ if key == "core" then
+ key = _oldkey; -- COMPAT with code that still uses "core"
+ end
+ return config[host][key];
+end
+function _M.rawget(host, key, _oldkey)
+ if key == "core" then
+ key = _oldkey; -- COMPAT with code that still uses "core"
+ end
+ local hostconfig = rawget(config, host);
+ if hostconfig then
+ return rawget(hostconfig, key);
end
- return nil;
end
-local function set(config, host, section, key, value)
- if host and section and key then
+local function set(config, host, key, value)
+ if host and key then
local hostconfig = rawget(config, host);
if not hostconfig then
hostconfig = rawset(config, host, setmetatable({}, host_mt))[host];
end
- if not rawget(hostconfig, section) then
- hostconfig[section] = setmetatable({}, section_mt(section));
- end
- hostconfig[section][key] = value;
+ hostconfig[key] = value;
return true;
end
return false;
end
-function _M.set(host, section, key, value)
- return set(config, host, section, key, value);
+function _M.set(host, key, value, _oldvalue)
+ if key == "core" then
+ key, value = value, _oldvalue; --COMPAT with code that still uses "core"
+ end
+ return set(config, host, 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
+ elseif path_sep == "\\" and (path:sub(1,1) ~= "/" and (path:sub(2,3) ~= ":\\" and path:sub(2,3) ~= ":/")) then
is_relative = true;
end
if is_relative then
@@ -87,7 +85,7 @@ do
end
end
return path;
- end
+ end
end
-- Helper function to convert a glob to a Lua pattern
@@ -109,7 +107,7 @@ function load(filename, format)
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 new_config = setmetatable({ ["*"] = { } }, config_mt);
local ok, err = parsers[format].load(f:read("*a"), filename, new_config);
f:close();
if ok then
@@ -152,8 +150,8 @@ end
-- Built-in Lua parser
do
- local loadstring, pcall, setmetatable = _G.loadstring, _G.pcall, _G.setmetatable;
- local setfenv, rawget, tostring = _G.setfenv, _G.rawget, _G.tostring;
+ local pcall, setmetatable = _G.pcall, _G.setmetatable;
+ local rawget = _G.rawget;
parsers.lua = {};
function parsers.lua.load(data, config_file, config)
local env;
@@ -163,61 +161,58 @@ do
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;
+ return rawget(_G, k);
end,
__newindex = function (t, k, v)
- set(config, env.__currenthost or "*", "core", k, v);
+ set(config, env.__currenthost or "*", k, v);
end
});
-
+
rawset(env, "__currenthost", "*") -- Default is global
function env.VirtualHost(name)
- if rawget(config, name) and rawget(config[name].core, "component_module") then
+ if rawget(config, name) and rawget(config[name], "component_module") then
error(format("Host %q clashes with previously defined %s Component %q, for services use a sub-domain like conference.%s",
- name, config[name].core.component_module:gsub("^%a+$", { component = "external", muc = "MUC"}), name, name), 0);
+ name, config[name].component_module:gsub("^%a+$", { component = "external", muc = "MUC"}), name, name), 0);
end
rawset(env, "__currenthost", name);
-- Needs at least one setting to logically exist :)
- set(config, name or "*", "core", "defined", true);
+ set(config, name or "*", "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);
+ set(config, name or "*", option_name, option_value);
end
end;
end
env.Host, env.host = env.VirtualHost, env.VirtualHost;
-
+
function env.Component(name)
- if rawget(config, name) and rawget(config[name].core, "defined") and not rawget(config[name].core, "component_module") then
+ if rawget(config, name) and rawget(config[name], "defined") and not rawget(config[name], "component_module") then
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(config, name, "component_module", "component");
-- Don't load the global modules by default
- set(config, name, "core", "load_global_modules", false);
+ set(config, name, "load_global_modules", false);
rawset(env, "__currenthost", name);
local function handle_config_options(config_options)
rawset(env, "__currenthost", "*"); -- Return to global scope
for option_name, option_value in pairs(config_options) do
- set(config, name or "*", "core", option_name, option_value);
+ set(config, name or "*", option_name, option_value);
end
end
-
+
return function (module)
if type(module) == "string" then
- set(config, name, "core", "component_module", module);
+ set(config, name, "component_module", module);
return handle_config_options;
end
return handle_config_options(module);
end
end
env.component = env.Component;
-
- function env.Include(file, wildcard)
+
+ function env.Include(file)
if file:match("[*?]") then
local path_pos, glob = file:match("()([^"..path_sep.."]+)$");
local path = file:sub(1, math_max(path_pos-2,0));
@@ -234,11 +229,10 @@ do
end
end
else
+ local file = resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file);
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);
+ local ret, err = parsers.lua.load(f:read("*a"), 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
@@ -246,28 +240,26 @@ do
end
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 = envload(data, "@"..config_file, env);
+
if not chunk then
return nil, err;
end
-
- setfenv(chunk, env);
-
+
local ok, err = pcall(chunk);
-
+
if not ok then
return nil, err;
end
-
+
return true;
end
-
+
end
return _M;
diff --git a/core/hostmanager.lua b/core/hostmanager.lua
index 9e74cd6b..91b052d1 100644
--- a/core/hostmanager.lua
+++ b/core/hostmanager.lua
@@ -1,7 +1,7 @@
-- 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.
--
@@ -12,18 +12,20 @@ local events_new = require "util.events".new;
local disco_items = require "util.multitable".new();
local NULL = {};
+local jid_split = require "util.jid".split;
local uuid_gen = require "util.uuid".generate;
local log = require "util.logger".init("hostmanager");
-local hosts = hosts;
+local hosts = prosody.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 core_route_stanza = _G.prosody.core_route_stanza;
-local pairs, setmetatable = pairs, setmetatable;
+local pairs, select, rawget = pairs, select, rawget;
local tostring, type = tostring, type;
module "hostmanager"
@@ -33,38 +35,50 @@ local hosts_loaded_once;
local function load_enabled_hosts(config)
local defined_hosts = config or configmanager.getconfig();
local activated_any_host;
-
+
for host, host_config in pairs(defined_hosts) do
- if host ~= "*" and host_config.core.enabled ~= false then
- if not host_config.core.component_module then
+ if host ~= "*" and host_config.enabled ~= false then
+ if not host_config.component_module then
activated_any_host = true;
end
activate(host, host_config);
end
end
-
+
if not activated_any_host then
log("error", "No active VirtualHost entries in the config file. This may cause unexpected behaviour as no modules will be loaded.");
end
-
+
prosody_events.fire_event("hosts-activated", defined_hosts);
hosts_loaded_once = true;
end
prosody_events.add_handler("server-starting", load_enabled_hosts);
+local function host_send(stanza)
+ local name, type = stanza.name, stanza.attr.type;
+ if type == "error" or (name == "iq" and type == "result") then
+ local dest_host_name = select(2, jid_split(stanza.attr.to));
+ local dest_host = hosts[dest_host_name] or { type = "unknown" };
+ log("warn", "Unhandled response sent to %s host %s: %s", dest_host.type, dest_host_name, tostring(stanza));
+ return;
+ end
+ core_route_stanza(nil, stanza);
+end
+
function activate(host, host_config)
- if hosts[host] then return nil, "The host "..host.." is already activated"; end
+ if rawget(hosts, host) then return nil, "The host "..host.." is already activated"; end
host_config = host_config or configmanager.getconfig()[host];
if not host_config then return nil, "Couldn't find the host "..tostring(host).." defined in the current config"; end
local host_session = {
host = host;
s2sout = {};
events = events_new();
- dialback_secret = configmanager.get(host, "core", "dialback_secret") or uuid_gen();
- disallow_s2s = configmanager.get(host, "core", "disallow_s2s");
+ dialback_secret = configmanager.get(host, "dialback_secret") or uuid_gen();
+ send = host_send;
+ modules = {};
};
- if not host_config.core.component_module then -- host
+ if not host_config.component_module then -- host
host_session.type = "local";
host_session.sessions = {};
else -- component
@@ -72,16 +86,16 @@ function activate(host, host_config)
end
hosts[host] = host_session;
if not host:match("[@/]") then
- disco_items:set(host:match("%.(.*)") or "*", host, true);
+ disco_items:set(host:match("%.(.*)") or "*", host, host_config.name or true);
end
- for option_name in pairs(host_config.core) do
+ for option_name in pairs(host_config) do
if option_name:match("_ports$") or option_name:match("_interface$") then
log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in the server-wide section instead", host, option_name);
end
end
-
+
log((hosts_loaded_once and "info") or "debug", "Activated host: %s", host);
- prosody_events.fire_event("host-activated", host, host_config);
+ prosody_events.fire_event("host-activated", host);
return true;
end
@@ -89,13 +103,14 @@ 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);
-
+ prosody_events.fire_event("host-deactivating", { host = host, host_session = host_session, reason = reason });
+
if type(reason) ~= "table" then
reason = { condition = "host-gone", text = tostring(reason or "This server has stopped serving "..host) };
end
-
+
-- Disconnect local users, s2s connections
+ -- TODO: These should move to mod_c2s and mod_s2s (how do they know they're being unloaded and not reloaded?)
if host_session.sessions then
for username, user in pairs(host_session.sessions) do
for resource, session in pairs(user.sessions) do
@@ -120,6 +135,7 @@ function deactivate(host, reason)
end
end
+ -- TODO: This should be done in modulemanager
if host_session.modules then
for module in pairs(host_session.modules) do
modulemanager.unload(host, module);
diff --git a/core/loggingmanager.lua b/core/loggingmanager.lua
index 40b96d52..c6361146 100644
--- a/core/loggingmanager.lua
+++ b/core/loggingmanager.lua
@@ -1,20 +1,18 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-local format, rep = string.format, string.rep;
-local pcall = pcall;
-local debug = debug;
-local tostring, setmetatable, rawset, pairs, ipairs, type =
- tostring, setmetatable, rawset, pairs, ipairs, type;
+local format = string.format;
+local setmetatable, rawset, pairs, ipairs, type =
+ 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 os_date = os.date;
local getstyle, setstyle = require "util.termcolours".getstyle, require "util.termcolours".setstyle;
if os.getenv("__FLUSH_LOG") then
@@ -27,8 +25,6 @@ local config = require "core.configmanager";
local logger = require "util.logger";
local prosody = prosody;
-local debug_mode = config.get("*", "core", "debug");
-
_G.log = logger.init("general");
module "loggingmanager"
@@ -43,41 +39,19 @@ local logging_config;
local apply_sink_rules;
local log_sink_types = setmetatable({}, { __newindex = function (t, k, v) rawset(t, k, v); apply_sink_rules(k); end; });
local get_levels;
-local logging_levels = { "debug", "info", "warn", "error", "critical" }
+local logging_levels = { "debug", "info", "warn", "error" }
-- Put a rule into action. Requires that the sink type has already been registered.
-- This function is called automatically when a new sink type is added [see apply_sink_rules()]
local function add_rule(sink_config)
local sink_maker = log_sink_types[sink_config.to];
if sink_maker then
- if sink_config.levels and not sink_config.source then
- -- Create sink
- local sink = sink_maker(sink_config);
-
- -- Set sink for all chosen levels
- for level in pairs(get_levels(sink_config.levels)) do
- logger.add_level_sink(level, sink);
- end
- elseif sink_config.source and not sink_config.levels then
- logger.add_name_sink(sink_config.source, sink_maker(sink_config));
- elseif sink_config.source and sink_config.levels then
- local levels = get_levels(sink_config.levels);
- local sink = sink_maker(sink_config);
- logger.add_name_sink(sink_config.source,
- function (name, level, ...)
- if levels[level] then
- return sink(name, level, ...);
- end
- end);
- else
- -- All sources
- -- Create sink
- local sink = sink_maker(sink_config);
-
- -- Set sink for all levels
- for _, level in pairs(logging_levels) do
- logger.add_level_sink(level, sink);
- end
+ -- Create sink
+ local sink = sink_maker(sink_config);
+
+ -- Set sink for all chosen levels
+ for level in pairs(get_levels(sink_config.levels or logging_levels)) do
+ logger.add_level_sink(level, sink);
end
else
-- No such sink type
@@ -89,21 +63,27 @@ end
-- the log_sink_types table.
function apply_sink_rules(sink_type)
if type(logging_config) == "table" then
-
- if sink_type == "file" then
- for _, level in ipairs(logging_levels) do
- if type(logging_config[level]) == "string" then
+
+ for _, level in ipairs(logging_levels) do
+ if type(logging_config[level]) == "string" then
+ local value = logging_config[level];
+ if sink_type == "file" and not value:match("^%*") then
+ add_rule({
+ to = sink_type;
+ filename = value;
+ timestamps = true;
+ levels = { min = level };
+ });
+ elseif value == "*"..sink_type then
add_rule({
- to = "file",
- filename = logging_config[level],
- timestamps = true,
- levels = { min = level },
+ to = sink_type;
+ levels = { min = level };
});
end
end
end
-
- for _, sink_config in pairs(logging_config) do
+
+ for _, sink_config in ipairs(logging_config) do
if (type(sink_config) == "table" and sink_config.to == sink_type) then
add_rule(sink_config);
elseif (type(sink_config) == "string" and sink_config:match("^%*(.+)") == sink_type) then
@@ -148,7 +128,7 @@ function get_levels(criteria, set)
end
end
end
-
+
for _, level in ipairs(criteria) do
set[level] = true;
end
@@ -158,27 +138,29 @@ 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("*", "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;
-
-
+ logging_config = config.get("*", "log") or default_logging;
+
+
for name, sink_maker in pairs(old_sink_types) do
log_sink_types[name] = sink_maker;
end
-
+
prosody.events.fire_event("logging-reloaded");
end
@@ -195,13 +177,13 @@ end
-- Column width for "source" (used by stdout and console)
local sourcewidth = 20;
-function log_sink_types.stdout()
+function log_sink_types.stdout(config)
local timestamps = config.timestamps;
-
+
if timestamps == true then
timestamps = default_timestamp; -- Default format
end
-
+
return function (name, level, message, ...)
sourcewidth = math_max(#name+2, sourcewidth);
local namelen = #name;
@@ -218,7 +200,7 @@ end
do
local do_pretty_printing = true;
-
+
local logstyles = {};
if do_pretty_printing then
logstyles["info"] = getstyle("bold");
@@ -230,7 +212,7 @@ do
if not do_pretty_printing then
return log_sink_types.stdout(config);
end
-
+
local timestamps = config.timestamps;
if timestamps == true then
@@ -240,7 +222,7 @@ do
return function (name, level, message, ...)
sourcewidth = math_max(#name+2, sourcewidth);
local namelen = #name;
-
+
if timestamps then
io_write(os_date(timestamps), " ");
end
@@ -266,12 +248,6 @@ function log_sink_types.file(config)
end
local write, flush = logfile.write, logfile.flush;
- prosody.events.add_handler("logging-reloading", function ()
- if logfile then
- logfile:close();
- end
- end);
-
local timestamps = config.timestamps;
if timestamps == nil or timestamps == true then
diff --git a/core/moduleapi.lua b/core/moduleapi.lua
new file mode 100644
index 00000000..65e00d41
--- /dev/null
+++ b/core/moduleapi.lua
@@ -0,0 +1,371 @@
+-- Prosody IM
+-- Copyright (C) 2008-2012 Matthew Wild
+-- Copyright (C) 2008-2012 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local config = require "core.configmanager";
+local modulemanager = require "modulemanager"; -- This is necessary to avoid require loops
+local array = require "util.array";
+local set = require "util.set";
+local logger = require "util.logger";
+local pluginloader = require "util.pluginloader";
+local timer = require "util.timer";
+
+local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
+local error, setmetatable, type = error, setmetatable, type;
+local ipairs, pairs, select, unpack = ipairs, pairs, select, unpack;
+local tonumber, tostring = tonumber, tostring;
+
+local prosody = prosody;
+local hosts = prosody.hosts;
+
+-- FIXME: This assert() is to try and catch an obscure bug (2013-04-05)
+local core_post_stanza = assert(prosody.core_post_stanza,
+ "prosody.core_post_stanza is nil, please report this as a bug");
+
+-- Registry of shared module data
+local shared_data = setmetatable({}, { __mode = "v" });
+
+local NULL = {};
+
+local api = {};
+
+-- Returns the name of the current module
+function api:get_name()
+ return self.name;
+end
+
+-- Returns the host that the current module is serving
+function api:get_host()
+ return self.host;
+end
+
+function api:get_host_type()
+ return (self.host == "*" and "global") or hosts[self.host].type or "local";
+end
+
+function api:set_global()
+ self.host = "*";
+ -- Update the logger
+ local _log = logger.init("mod_"..self.name);
+ self.log = function (self, ...) return _log(...); end;
+ self._log = _log;
+ self.global = true;
+end
+
+function api:add_feature(xmlns)
+ self:add_item("feature", xmlns);
+end
+function api:add_identity(category, type, name)
+ self:add_item("identity", {category = category, type = type, name = name});
+end
+function api:add_extension(data)
+ self:add_item("extension", data);
+end
+function api:has_feature(xmlns)
+ for _, feature in ipairs(self:get_host_items("feature")) do
+ if feature == xmlns then return true; end
+ end
+ return false;
+end
+function api:has_identity(category, type, name)
+ for _, id in ipairs(self:get_host_items("identity")) do
+ if id.category == category and id.type == type and id.name == name then
+ return true;
+ end
+ end
+ return false;
+end
+
+function api:fire_event(...)
+ return (hosts[self.host] or prosody).events.fire_event(...);
+end
+
+function api:hook_object_event(object, event, handler, priority)
+ self.event_handlers:set(object, event, handler, true);
+ return object.add_handler(event, handler, priority);
+end
+
+function api:unhook_object_event(object, event, handler)
+ return object.remove_handler(event, handler);
+end
+
+function api:hook(event, handler, priority)
+ return self:hook_object_event((hosts[self.host] or prosody).events, event, handler, priority);
+end
+
+function api:hook_global(event, handler, priority)
+ return self:hook_object_event(prosody.events, event, handler, priority);
+end
+
+function api:hook_tag(xmlns, name, handler, priority)
+ if not handler and type(name) == "function" then
+ -- If only 2 options then they specified no xmlns
+ xmlns, name, handler, priority = nil, xmlns, name, handler;
+ elseif not (handler and name) then
+ self:log("warn", "Error: Insufficient parameters to module:hook_stanza()");
+ return;
+ end
+ return self:hook("stanza/"..(xmlns and (xmlns..":") or "")..name, function (data) return handler(data.origin, data.stanza, data); end, priority);
+end
+api.hook_stanza = api.hook_tag; -- COMPAT w/pre-0.9
+
+function api:unhook(event, handler)
+ return self:unhook_object_event((hosts[self.host] or prosody).events, event, handler);
+end
+
+function api:require(lib)
+ local f, n = pluginloader.load_code(self.name, lib..".lib.lua", self.environment);
+ if not f then
+ f, n = pluginloader.load_code(lib, lib..".lib.lua", self.environment);
+ end
+ if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message
+ return f();
+end
+
+function api:depends(name)
+ if not self.dependencies then
+ self.dependencies = {};
+ self:hook("module-reloaded", function (event)
+ if self.dependencies[event.module] and not self.reloading then
+ self:log("info", "Auto-reloading due to reload of %s:%s", event.host, event.module);
+ modulemanager.reload(self.host, self.name);
+ return;
+ end
+ end);
+ self:hook("module-unloaded", function (event)
+ if self.dependencies[event.module] then
+ self:log("info", "Auto-unloading due to unload of %s:%s", event.host, event.module);
+ modulemanager.unload(self.host, self.name);
+ end
+ end);
+ end
+ local mod = modulemanager.get_module(self.host, name) or modulemanager.get_module("*", name);
+ if mod and mod.module.host == "*" and self.host ~= "*"
+ and modulemanager.module_has_method(mod, "add_host") then
+ mod = nil; -- Target is a shared module, so we still want to load it on our host
+ end
+ if not mod then
+ local err;
+ mod, err = modulemanager.load(self.host, name);
+ if not mod then
+ return error(("Unable to load required module, mod_%s: %s"):format(name, ((err or "unknown error"):gsub("%-", " ")) ));
+ end
+ end
+ self.dependencies[name] = true;
+ return mod;
+end
+
+-- Returns one or more shared tables at the specified virtual paths
+-- Intentionally does not allow the table at a path to be _set_, it
+-- is auto-created if it does not exist.
+function api:shared(...)
+ if not self.shared_data then self.shared_data = {}; end
+ local paths = { n = select("#", ...), ... };
+ local data_array = {};
+ local default_path_components = { self.host, self.name };
+ for i = 1, paths.n do
+ local path = paths[i];
+ if path:sub(1,1) ~= "/" then -- Prepend default components
+ local n_components = select(2, path:gsub("/", "%1"));
+ path = (n_components<#default_path_components and "/" or "")..t_concat(default_path_components, "/", 1, #default_path_components-n_components).."/"..path;
+ end
+ local shared = shared_data[path];
+ if not shared then
+ shared = {};
+ if path:match("%-cache$") then
+ setmetatable(shared, { __mode = "kv" });
+ end
+ shared_data[path] = shared;
+ end
+ t_insert(data_array, shared);
+ self.shared_data[path] = shared;
+ end
+ return unpack(data_array);
+end
+
+function api:get_option(name, default_value)
+ local value = config.get(self.host, name);
+ if value == nil then
+ value = default_value;
+ end
+ return value;
+end
+
+function api:get_option_string(name, default_value)
+ local value = self:get_option(name, default_value);
+ if type(value) == "table" then
+ if #value > 1 then
+ self:log("error", "Config option '%s' does not take a list, using just the first item", name);
+ end
+ value = value[1];
+ end
+ if value == nil then
+ return nil;
+ end
+ return tostring(value);
+end
+
+function api:get_option_number(name, ...)
+ local value = self:get_option(name, ...);
+ if type(value) == "table" then
+ if #value > 1 then
+ self:log("error", "Config option '%s' does not take a list, using just the first item", name);
+ end
+ value = value[1];
+ end
+ local ret = tonumber(value);
+ if value ~= nil and ret == nil then
+ self:log("error", "Config option '%s' not understood, expecting a number", name);
+ end
+ return ret;
+end
+
+function api:get_option_boolean(name, ...)
+ local value = self:get_option(name, ...);
+ if type(value) == "table" then
+ if #value > 1 then
+ self:log("error", "Config option '%s' does not take a list, using just the first item", name);
+ end
+ value = value[1];
+ end
+ if value == nil then
+ return nil;
+ end
+ local ret = value == true or value == "true" or value == 1 or nil;
+ if ret == nil then
+ ret = (value == false or value == "false" or value == 0);
+ if ret then
+ ret = false;
+ else
+ ret = nil;
+ end
+ end
+ if ret == nil then
+ self:log("error", "Config option '%s' not understood, expecting true/false", name);
+ end
+ return ret;
+end
+
+function api:get_option_array(name, ...)
+ local value = self:get_option(name, ...);
+
+ if value == nil then
+ return nil;
+ end
+
+ if type(value) ~= "table" then
+ return array{ value }; -- Assume any non-list is a single-item list
+ end
+
+ return array():append(value); -- Clone
+end
+
+function api:get_option_set(name, ...)
+ local value = self:get_option_array(name, ...);
+
+ if value == nil then
+ return nil;
+ end
+
+ return set.new(value);
+end
+
+function api:get_option_inherited_set(name, ...)
+ local value = self:get_option_set(name, ...);
+ local global_value = self:context("*"):get_option_set(name, ...);
+ if not value then
+ return global_value;
+ elseif not global_value then
+ return value;
+ end
+ value:include(global_value);
+ return value;
+end
+
+function api:context(host)
+ return setmetatable({host=host or "*"}, {__index=self,__newindex=self});
+end
+
+function api:add_item(key, value)
+ self.items = self.items or {};
+ self.items[key] = self.items[key] or {};
+ t_insert(self.items[key], value);
+ self:fire_event("item-added/"..key, {source = self, item = value});
+end
+function api:remove_item(key, value)
+ local t = self.items and self.items[key] or NULL;
+ for i = #t,1,-1 do
+ if t[i] == value then
+ t_remove(self.items[key], i);
+ self:fire_event("item-removed/"..key, {source = self, item = value});
+ return value;
+ end
+ end
+end
+
+function api:get_host_items(key)
+ local result = modulemanager.get_items(key, self.host) or {};
+ return result;
+end
+
+function api:handle_items(type, added_cb, removed_cb, existing)
+ self:hook("item-added/"..type, added_cb);
+ self:hook("item-removed/"..type, removed_cb);
+ if existing ~= false then
+ for _, item in ipairs(self:get_host_items(type)) do
+ added_cb({ item = item });
+ end
+ end
+end
+
+function api:provides(name, item)
+ -- if not item then item = setmetatable({}, { __index = function(t,k) return rawget(self.environment, k); end }); end
+ if not item then
+ item = {}
+ for k,v in pairs(self.environment) do
+ if k ~= "module" then item[k] = v; end
+ end
+ end
+ if not item.name then
+ local item_name = self.name;
+ -- Strip a provider prefix to find the item name
+ -- (e.g. "auth_foo" -> "foo" for an auth provider)
+ if item_name:find(name.."_", 1, true) == 1 then
+ item_name = item_name:sub(#name+2);
+ end
+ item.name = item_name;
+ end
+ item._provided_by = self.name;
+ self:add_item(name.."-provider", item);
+end
+
+function api:send(stanza)
+ return core_post_stanza(hosts[self.host], stanza);
+end
+
+function api:add_timer(delay, callback)
+ return timer.add_task(delay, function (t)
+ if self.loaded == false then return; end
+ return callback(t);
+ end);
+end
+
+local path_sep = package.config:sub(1,1);
+function api:get_directory()
+ return self.path and (self.path:gsub("%"..path_sep.."[^"..path_sep.."]*$", "")) or nil;
+end
+
+function api:load_resource(path, mode)
+ path = config.resolve_relative_path(self:get_directory(), path);
+ return io.open(path, mode);
+end
+
+function api:open_store(name, type)
+ return storagemanager.open(self.host, name or self.name, type);
+end
+
+return api;
diff --git a/core/modulemanager.lua b/core/modulemanager.lua
index 0af9a271..2ad2fc17 100644
--- a/core/modulemanager.lua
+++ b/core/modulemanager.lua
@@ -1,33 +1,25 @@
-- 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 plugin_dir = CFG_PLUGINDIR or "./plugins/";
-
local logger = require "util.logger";
local log = logger.init("modulemanager");
local config = require "core.configmanager";
-local multitable_new = require "util.multitable".new;
-local st = require "util.stanza";
local pluginloader = require "util.pluginloader";
+local set = require "util.set";
+
+local new_multitable = require "util.multitable".new;
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;
-local pairs, ipairs = pairs, ipairs;
-local t_insert, t_concat = table.insert, table.concat;
-local type = type;
-local next = next;
-local rawget = rawget;
-local error = error;
-local tostring, tonumber = tostring, tonumber;
+
+local pcall, xpcall = pcall, xpcall;
+local setmetatable, rawget = setmetatable, rawget;
+local ipairs, pairs, type, tostring, t_insert = ipairs, pairs, type, tostring, table.insert;
local debug_traceback = debug.traceback;
local unpack, select = unpack, select;
@@ -37,53 +29,44 @@ pcall = function(f, ...)
return xpcall(function() return 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", "offline", "c2s", "s2s"};
+local component_inheritable_modules = {"tls", "dialback", "iq", "s2s"};
-- We need this to let modules access the real global namespace
local _G = _G;
module "modulemanager"
-api = {};
-local api = api; -- Module API container
+local api = _G.require "core.moduleapi"; -- Module API container
+-- [host] = { [module] = module_env }
local modulemap = { ["*"] = {} };
-local modulehelpers = setmetatable({}, { __index = _G });
-
-local 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");
-
+ local component = config.get(host, "component_module");
+
+ local global_modules_enabled = config.get("*", "modules_enabled");
+ local global_modules_disabled = config.get("*", "modules_disabled");
+ local host_modules_enabled = config.get(host, "modules_enabled");
+ local host_modules_disabled = config.get(host, "modules_disabled");
+
if host_modules_enabled == global_modules_enabled then host_modules_enabled = nil; end
if host_modules_disabled == global_modules_disabled then host_modules_disabled = nil; end
-
- local host_modules = set.new(host_modules_enabled) - set.new(host_modules_disabled);
+
local global_modules = set.new(autoload_modules) + set.new(global_modules_enabled) - set.new(global_modules_disabled);
if component then
global_modules = set.intersection(set.new(component_inheritable_modules), global_modules);
end
- local modules = global_modules + host_modules;
-
+ 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");
end
-
+
if component then
load(host, component);
end
@@ -91,111 +74,134 @@ function load_modules_for_host(host)
load(host, module);
end
end
-prosody_events.add_handler("host-activated", load_modules_for_host);
---
+prosody.events.add_handler("host-activated", load_modules_for_host);
+prosody.events.add_handler("host-deactivated", function (host)
+ modulemap[host] = nil;
+end);
+
+--- Private helpers ---
+
+local function do_unload_module(host, name)
+ local mod = get_module(host, name);
+ if not mod then return nil, "module-not-loaded"; end
+
+ if module_has_method(mod, "unload") then
+ local ok, err = call_module_method(mod, "unload");
+ if (not ok) and err then
+ log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
+ end
+ end
+
+ for object, event, handler in mod.module.event_handlers:iter(nil, nil, nil) do
+ object.remove_handler(event, handler);
+ end
+
+ if mod.module.items then -- remove items
+ local events = (host == "*" and prosody.events) or hosts[host].events;
+ for key,t in pairs(mod.module.items) do
+ for i = #t,1,-1 do
+ local value = t[i];
+ t[i] = nil;
+ events.fire_event("item-removed/"..key, {source = mod.module, item = value});
+ end
+ end
+ end
+ mod.module.loaded = false;
+ modulemap[host][name] = nil;
+ return true;
+end
-function load(host, module_name, config)
+local function do_load_module(host, module_name, state)
if not (host and module_name) then
return nil, "insufficient-parameters";
- elseif not hosts[host] then
+ elseif not hosts[host] and host ~= "*"then
return nil, "unknown-host";
end
-
+
if not modulemap[host] then
- modulemap[host] = {};
+ modulemap[host] = hosts[host].modules;
end
-
+
if modulemap[host][module_name] then
log("warn", "%s is already loaded for %s, so not loading again", module_name, host);
return nil, "module-already-loaded";
elseif modulemap["*"][module_name] then
+ local mod = modulemap["*"][module_name];
+ if module_has_method(mod, "add_host") then
+ local _log = logger.init(host..":"..module_name);
+ local host_module_api = setmetatable({
+ host = host, event_handlers = new_multitable(), items = {};
+ _log = _log, log = function (self, ...) return _log(...); end;
+ },{
+ __index = modulemap["*"][module_name].module;
+ });
+ local host_module = setmetatable({ module = host_module_api }, { __index = mod });
+ host_module_api.environment = host_module;
+ modulemap[host][module_name] = host_module;
+ local ok, result, module_err = call_module_method(mod, "add_host", host_module_api);
+ if not ok or result == false then
+ modulemap[host][module_name] = nil;
+ return nil, ok and module_err or result;
+ end
+ return host_module;
+ end
return nil, "global-module-already-loaded";
end
-
- local mod, err = pluginloader.load_code(module_name);
+
+
+ local _log = logger.init(host..":"..module_name);
+ local api_instance = setmetatable({ name = module_name, host = host,
+ _log = _log, log = function (self, ...) return _log(...); end, event_handlers = new_multitable(),
+ reloading = not not state, saved_state = state~=true and state or nil }
+ , { __index = api });
+
+ local pluginenv = setmetatable({ module = api_instance }, { __index = _G });
+ api_instance.environment = pluginenv;
+
+ local mod, err = pluginloader.load_code(module_name, nil, pluginenv);
if not mod then
log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil");
return nil, err;
end
- local _log = logger.init(host..":"..module_name);
- local api_instance = setmetatable({ name = module_name, host = host, config = config, _log = _log, log = function (self, ...) return _log(...); end }, { __index = api });
+ api_instance.path = err;
- local pluginenv = setmetatable({ module = api_instance }, { __index = _G });
- api_instance.environment = pluginenv;
-
- setfenv(mod, pluginenv);
- hosts[host].modules = modulemap[host];
modulemap[host][module_name] = pluginenv;
-
- local success, err = pcall(mod);
- if success then
+ local ok, err = pcall(mod);
+ if ok then
+ -- Call module's "load"
if module_has_method(pluginenv, "load") then
- success, err = call_module_method(pluginenv, "load");
- if not success then
+ ok, err = call_module_method(pluginenv, "load");
+ if not ok then
log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err or "nil");
end
end
-
- -- Use modified host, if the module set one
- if api_instance.host == "*" and host ~= "*" then
+ api_instance.reloading, api_instance.saved_state = nil, nil;
+
+ if api_instance.host == "*" then
+ if not api_instance.global then -- COMPAT w/pre-0.9
+ if host ~= "*" then
+ log("warn", "mod_%s: Setting module.host = '*' deprecated, call module:set_global() instead", module_name);
+ end
+ api_instance:set_global();
+ end
modulemap[host][module_name] = nil;
- modulemap["*"][module_name] = pluginenv;
- api_instance:set_global();
- end
- else
- log("error", "Error initializing module '%s' on '%s': %s", module_name, host, err or "nil");
- end
- if success then
- (hosts[api_instance.host] or prosody).events.fire_event("module-loaded", { module = module_name, host = host });
- return true;
- else -- load failed, unloading
- unload(api_instance.host, module_name);
- return nil, err;
- end
-end
-
-function get_module(host, name)
- return modulemap[host] and modulemap[host][name];
-end
-
-function is_loaded(host, name)
- return modulemap[host] and modulemap[host][name] and true;
-end
-
-function unload(host, name, ...)
- local mod = get_module(host, name);
- if not mod then return nil, "module-not-loaded"; end
-
- if module_has_method(mod, "unload") then
- local ok, err = call_module_method(mod, "unload");
- if (not ok) and err then
- log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
- end
- end
- -- 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
- (hosts[host] or prosody).events.remove_handler(event, handler);
- end
- end
- hooks:remove(host, name);
- if mod.module.items then -- remove items
- for key,t in pairs(mod.module.items) do
- for i = #t,1,-1 do
- local value = t[i];
- t[i] = nil;
- hosts[host].events.fire_event("item-removed/"..key, {source = self, item = value});
+ modulemap[api_instance.host][module_name] = pluginenv;
+ if host ~= api_instance.host and module_has_method(pluginenv, "add_host") then
+ -- Now load the module again onto the host it was originally being loaded on
+ ok, err = do_load_module(host, module_name);
end
end
end
- modulemap[host][name] = nil;
- (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host });
- return true;
+ if not ok then
+ modulemap[api_instance.host][module_name] = nil;
+ log("error", "Error initializing module '%s' on '%s': %s", module_name, host, err or "nil");
+ end
+ return ok and pluginenv, err;
end
-function reload(host, name, ...)
+local function do_reload_module(host, name)
local mod = get_module(host, name);
if not mod then return nil, "module-not-loaded"; end
@@ -206,14 +212,13 @@ function reload(host, name, ...)
end
local saved;
-
if module_has_method(mod, "save") then
local ok, ret, err = call_module_method(mod, "save");
if ok then
saved = ret;
else
- log("warn", "Error saving module '%s:%s' state: %s", host, module, ret);
- if not config.get(host, "core", "force_module_reload") then
+ log("warn", "Error saving module '%s:%s' state: %s", host, name, ret);
+ if not config.get(host, "force_module_reload") then
log("warn", "Aborting reload due to error, set force_module_reload to ignore this");
return nil, "save-state-failed";
else
@@ -222,8 +227,9 @@ function reload(host, name, ...)
end
end
- unload(host, name, ...);
- local ok, err = load(host, name, ...);
+ mod.module.reloading = true;
+ do_unload_module(host, name);
+ local ok, err = do_load_module(host, name, saved or true);
if ok then
mod = get_module(host, name);
if module_has_method(mod, "restore") then
@@ -232,214 +238,82 @@ function reload(host, name, ...)
log("warn", "Error restoring module '%s' from '%s': %s", name, host, err);
end
end
- return true;
end
- return ok, err;
+ return ok and mod, err;
end
-function module_has_method(module, method)
- return type(module.module[method]) == "function";
-end
+--- Public API ---
-function call_module_method(module, method, ...)
- if module_has_method(module, method) then
- local f = module.module[method];
- return pcall(f, ...);
- else
- return false, "no-such-method";
+-- Load a module and fire module-loaded event
+function load(host, name)
+ local mod, err = do_load_module(host, name);
+ if mod then
+ (hosts[mod.module.host] or prosody).events.fire_event("module-loaded", { module = name, host = mod.module.host });
end
+ return mod, err;
end
------ API functions exposed to modules -----------
--- Must all be in api.*
-
--- Returns the name of the current module
-function api:get_name()
- return self.name;
-end
-
--- Returns the host that the current module is serving
-function api:get_host()
- return self.host;
-end
-
-function api:get_host_type()
- return hosts[self.host].type;
-end
-
-function api:set_global()
- self.host = "*";
- -- Update the logger
- local _log = logger.init("mod_"..self.name);
- self.log = function (self, ...) return _log(...); end;
- self._log = _log;
-end
-
-function api:add_feature(xmlns)
- self:add_item("feature", xmlns);
-end
-function api:add_identity(category, type, name)
- self:add_item("identity", {category = category, type = type, name = name});
-end
-
-function api:fire_event(...)
- return (hosts[self.host] or prosody).events.fire_event(...);
-end
-
-function api:hook(event, handler, priority)
- hooks:set(self.host, self.name, event, handler, true);
- (hosts[self.host] or prosody).events.add_handler(event, handler, priority);
-end
-
-function api:hook_stanza(xmlns, name, handler, priority)
- if not handler and type(name) == "function" then
- -- If only 2 options then they specified no xmlns
- xmlns, name, handler, priority = nil, xmlns, name, handler;
- elseif not (handler and name) then
- self:log("warn", "Error: Insufficient parameters to module:hook_stanza()");
- return;
+-- Unload a module and fire module-unloaded
+function unload(host, name)
+ local ok, err = do_unload_module(host, name);
+ if ok then
+ (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host });
end
- return api.hook(self, "stanza/"..(xmlns and (xmlns..":") or "")..name, function (data) return handler(data.origin, data.stanza, data); end, priority);
+ return ok, err;
end
-function api:require(lib)
- local f, n = pluginloader.load_code(self.name, lib..".lib.lua");
- if not f then
- f, n = pluginloader.load_code(lib, lib..".lib.lua");
+function reload(host, name)
+ local mod, err = do_reload_module(host, name);
+ if mod then
+ modulemap[host][name].module.reloading = true;
+ (hosts[host] or prosody).events.fire_event("module-reloaded", { module = name, host = host });
+ mod.module.reloading = nil;
+ elseif not is_loaded(host, name) then
+ (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host });
end
- if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message
- setfenv(f, self.environment);
- return f();
+ return mod, err;
end
-function api:get_option(name, default_value)
- local value = config.get(self.host, self.name, name);
- if value == nil then
- value = config.get(self.host, "core", name);
- if value == nil then
- value = default_value;
- end
- end
- return value;
-end
-
-function api:get_option_string(name, default_value)
- local value = self:get_option(name, default_value);
- if type(value) == "table" then
- if #value > 1 then
- self:log("error", "Config option '%s' does not take a list, using just the first item", name);
- end
- value = value[1];
- end
- if value == nil then
- return nil;
- end
- return tostring(value);
+function get_module(host, name)
+ return modulemap[host] and modulemap[host][name];
end
-function api:get_option_number(name, ...)
- local value = self:get_option(name, ...);
- if type(value) == "table" then
- if #value > 1 then
- self:log("error", "Config option '%s' does not take a list, using just the first item", name);
+function get_items(key, host)
+ local result = {};
+ local modules = modulemap[host];
+ if not key or not host or not modules then return nil; end
+
+ for _, module in pairs(modules) do
+ local mod = module.module;
+ if mod.items and mod.items[key] then
+ for _, value in ipairs(mod.items[key]) do
+ t_insert(result, value);
+ end
end
- value = value[1];
end
- local ret = tonumber(value);
- if value ~= nil and ret == nil then
- self:log("error", "Config option '%s' not understood, expecting a number", name);
- end
- return ret;
-end
-function api:get_option_boolean(name, ...)
- local value = self:get_option(name, ...);
- if type(value) == "table" then
- if #value > 1 then
- self:log("error", "Config option '%s' does not take a list, using just the first item", name);
- end
- value = value[1];
- end
- if value == nil then
- return nil;
- end
- local ret = value == true or value == "true" or value == 1 or nil;
- if ret == nil then
- ret = (value == false or value == "false" or value == 0);
- if ret then
- ret = false;
- else
- ret = nil;
- end
- end
- if ret == nil then
- self:log("error", "Config option '%s' not understood, expecting true/false", name);
- end
- return ret;
+ return result;
end
-function api:get_option_array(name, ...)
- local value = self:get_option(name, ...);
-
- if value == nil then
- return nil;
- end
-
- if type(value) ~= "table" then
- return array{ value }; -- Assume any non-list is a single-item list
- end
-
- return array():append(value); -- Clone
+function get_modules(host)
+ return modulemap[host];
end
-function api:get_option_set(name, ...)
- local value = self:get_option_array(name, ...);
-
- if value == nil then
- return nil;
- end
-
- return set.new(value);
+function is_loaded(host, name)
+ return modulemap[host] and modulemap[host][name] and true;
end
-local t_remove = _G.table.remove;
-local module_items = multitable_new();
-function api:add_item(key, value)
- self.items = self.items or {};
- self.items[key] = self.items[key] or {};
- t_insert(self.items[key], value);
- self:fire_event("item-added/"..key, {source = self, item = value});
-end
-function api:remove_item(key, value)
- local t = self.items and self.items[key] or NULL;
- for i = #t,1,-1 do
- if t[i] == value then
- t_remove(self.items[key], i);
- self:fire_event("item-removed/"..key, {source = self, item = value});
- return value;
- end
- end
+function module_has_method(module, method)
+ return type(rawget(module.module, method)) == "function";
end
-function api:get_host_items(key)
- local result = {};
- for mod_name, module in pairs(modulemap[self.host]) do
- module = module.module;
- if module.items then
- for _, item in ipairs(module.items[key] or NULL) do
- t_insert(result, item);
- end
- end
- end
- for mod_name, module in pairs(modulemap["*"]) do
- module = module.module;
- if module.items then
- for _, item in ipairs(module.items[key] or NULL) do
- t_insert(result, item);
- end
- end
+function call_module_method(module, method, ...)
+ local f = rawget(module.module, method);
+ if type(f) == "function" then
+ return pcall(f, ...);
+ else
+ return false, "no-such-method";
end
- return result;
end
return _M;
diff --git a/core/portmanager.lua b/core/portmanager.lua
new file mode 100644
index 00000000..95900c08
--- /dev/null
+++ b/core/portmanager.lua
@@ -0,0 +1,239 @@
+local config = require "core.configmanager";
+local certmanager = require "core.certmanager";
+local server = require "net.server";
+local socket = require "socket";
+
+local log = require "util.logger".init("portmanager");
+local multitable = require "util.multitable";
+local set = require "util.set";
+
+local table = table;
+local setmetatable, rawset, rawget = setmetatable, rawset, rawget;
+local type, tonumber, tostring, ipairs, pairs = type, tonumber, tostring, ipairs, pairs;
+
+local prosody = prosody;
+local fire_event = prosody.events.fire_event;
+
+module "portmanager";
+
+--- Config
+
+local default_interfaces = { };
+local default_local_interfaces = { };
+if config.get("*", "use_ipv4") ~= false then
+ table.insert(default_interfaces, "*");
+ table.insert(default_local_interfaces, "127.0.0.1");
+end
+if socket.tcp6 and config.get("*", "use_ipv6") ~= false then
+ table.insert(default_interfaces, "::");
+ table.insert(default_local_interfaces, "::1");
+end
+
+--- Private state
+
+-- service_name -> { service_info, ... }
+local services = setmetatable({}, { __index = function (t, k) rawset(t, k, {}); return rawget(t, k); end });
+
+-- service_name, interface (string), port (number)
+local active_services = multitable.new();
+
+--- Private helpers
+
+local function error_to_friendly_message(service_name, port, err)
+ local friendly_message = err;
+ if err:match(" in use") then
+ -- FIXME: Use service_name here
+ if port == 5222 or port == 5223 or port == 5269 then
+ friendly_message = "check that Prosody or another XMPP server is "
+ .."not already running and using this port";
+ elseif port == 80 or port == 81 then
+ friendly_message = "check that a HTTP server is not already using "
+ .."this port";
+ elseif port == 5280 then
+ friendly_message = "check that Prosody or a BOSH connection manager "
+ .."is not already running";
+ else
+ friendly_message = "this port is in use by another application";
+ end
+ elseif err:match("permission") then
+ friendly_message = "Prosody does not have sufficient privileges to use this port";
+ end
+ return friendly_message;
+end
+
+prosody.events.add_handler("item-added/net-provider", function (event)
+ local item = event.item;
+ register_service(item.name, item);
+end);
+prosody.events.add_handler("item-removed/net-provider", function (event)
+ local item = event.item;
+ unregister_service(item.name, item);
+end);
+
+local function duplicate_ssl_config(ssl_config)
+ local ssl_config = type(ssl_config) == "table" and ssl_config or {};
+
+ local _config = {};
+ for k, v in pairs(ssl_config) do
+ _config[k] = v;
+ end
+ return _config;
+end
+
+--- Public API
+
+function activate(service_name)
+ local service_info = services[service_name][1];
+ if not service_info then
+ return nil, "Unknown service: "..service_name;
+ end
+
+ local listener = service_info.listener;
+
+ local config_prefix = (service_info.config_prefix or service_name).."_";
+ if config_prefix == "_" then
+ config_prefix = "";
+ end
+
+ local bind_interfaces = config.get("*", config_prefix.."interfaces")
+ or config.get("*", config_prefix.."interface") -- COMPAT w/pre-0.9
+ or (service_info.private and (config.get("*", "local_interfaces") or default_local_interfaces))
+ or config.get("*", "interfaces")
+ or config.get("*", "interface") -- COMPAT w/pre-0.9
+ or listener.default_interface -- COMPAT w/pre0.9
+ or default_interfaces
+ bind_interfaces = set.new(type(bind_interfaces)~="table" and {bind_interfaces} or bind_interfaces);
+
+ local bind_ports = config.get("*", config_prefix.."ports")
+ or service_info.default_ports
+ or {service_info.default_port
+ or listener.default_port -- COMPAT w/pre-0.9
+ }
+ bind_ports = set.new(type(bind_ports) ~= "table" and { bind_ports } or bind_ports );
+
+ local mode, ssl = listener.default_mode or "*a";
+ local hooked_ports = {};
+
+ for interface in bind_interfaces do
+ for port in bind_ports do
+ local port_number = tonumber(port);
+ if not port_number then
+ log("error", "Invalid port number specified for service '%s': %s", service_info.name, tostring(port));
+ elseif #active_services:search(nil, interface, port_number) > 0 then
+ log("error", "Multiple services configured to listen on the same port ([%s]:%d): %s, %s", interface, port, active_services:search(nil, interface, port)[1][1].service.name or "<unnamed>", service_name or "<unnamed>");
+ else
+ local err;
+ -- Create SSL context for this service/port
+ if service_info.encryption == "ssl" then
+ local ssl_config = duplicate_ssl_config((config.get("*", config_prefix.."ssl") and config.get("*", config_prefix.."ssl")[interface])
+ or (config.get("*", config_prefix.."ssl") and config.get("*", config_prefix.."ssl")[port])
+ or config.get("*", config_prefix.."ssl")
+ or (config.get("*", "ssl") and config.get("*", "ssl")[interface])
+ or (config.get("*", "ssl") and config.get("*", "ssl")[port])
+ or config.get("*", "ssl"));
+ -- add default entries for, or override ssl configuration
+ if ssl_config and service_info.ssl_config then
+ for key, value in pairs(service_info.ssl_config) do
+ if not service_info.ssl_config_override and not ssl_config[key] then
+ ssl_config[key] = value;
+ elseif service_info.ssl_config_override then
+ ssl_config[key] = value;
+ end
+ end
+ end
+
+ ssl, err = certmanager.create_context(service_info.name.." port "..port, "server", ssl_config);
+ if not ssl then
+ log("error", "Error binding encrypted port for %s: %s", service_info.name, error_to_friendly_message(service_name, port_number, err) or "unknown error");
+ end
+ end
+ if not err then
+ -- Start listening on interface+port
+ local handler, err = server.addserver(interface, port_number, listener, mode, ssl);
+ if not handler then
+ log("error", "Failed to open server port %d on %s, %s", port_number, interface, error_to_friendly_message(service_name, port_number, err));
+ else
+ table.insert(hooked_ports, "["..interface.."]:"..port_number);
+ log("debug", "Added listening service %s to [%s]:%d", service_name, interface, port_number);
+ active_services:add(service_name, interface, port_number, {
+ server = handler;
+ service = service_info;
+ });
+ end
+ end
+ end
+ end
+ end
+ log("info", "Activated service '%s' on %s", service_name, #hooked_ports == 0 and "no ports" or table.concat(hooked_ports, ", "));
+ return true;
+end
+
+function deactivate(service_name, service_info)
+ for name, interface, port, n, active_service
+ in active_services:iter(service_name or service_info and service_info.name, nil, nil, nil) do
+ if service_info == nil or active_service.service == service_info then
+ close(interface, port);
+ end
+ end
+ log("info", "Deactivated service '%s'", service_name or service_info.name);
+end
+
+function register_service(service_name, service_info)
+ table.insert(services[service_name], service_info);
+
+ if not active_services:get(service_name) then
+ log("debug", "No active service for %s, activating...", service_name);
+ local ok, err = activate(service_name);
+ if not ok then
+ log("error", "Failed to activate service '%s': %s", service_name, err or "unknown error");
+ end
+ end
+
+ fire_event("service-added", { name = service_name, service = service_info });
+ return true;
+end
+
+function unregister_service(service_name, service_info)
+ log("debug", "Unregistering service: %s", service_name);
+ local service_info_list = services[service_name];
+ for i, service in ipairs(service_info_list) do
+ if service == service_info then
+ table.remove(service_info_list, i);
+ end
+ end
+ deactivate(nil, service_info);
+ if #service_info_list > 0 then -- Other services registered with this name
+ activate(service_name); -- Re-activate with the next available one
+ end
+ fire_event("service-removed", { name = service_name, service = service_info });
+end
+
+function close(interface, port)
+ local service, server = get_service_at(interface, port);
+ if not service then
+ return false, "port-not-open";
+ end
+ server:close();
+ active_services:remove(service.name, interface, port);
+ log("debug", "Removed listening service %s from [%s]:%d", service.name, interface, port);
+ return true;
+end
+
+function get_service_at(interface, port)
+ local data = active_services:search(nil, interface, port)[1][1];
+ return data.service, data.server;
+end
+
+function get_service(service_name)
+ return (services[service_name] or {})[1];
+end
+
+function get_active_services(...)
+ return active_services;
+end
+
+function get_registered_services()
+ return services;
+end
+
+return _M;
diff --git a/core/rostermanager.lua b/core/rostermanager.lua
index 59ba6579..5266afb5 100644
--- a/core/rostermanager.lua
+++ b/core/rostermanager.lua
@@ -1,7 +1,7 @@
-- 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.
--
@@ -11,16 +11,14 @@
local log = require "util.logger".init("rostermanager");
-local setmetatable = setmetatable;
-local format = string.format;
-local loadfile, setfenv, pcall = loadfile, setfenv, pcall;
-local pairs, ipairs = pairs, ipairs;
+local pairs = pairs;
local tostring = tostring;
local hosts = hosts;
local bare_sessions = bare_sessions;
local datamanager = require "util.datamanager"
+local um_user_exists = require "core.usermanager".user_exists;
local st = require "util.stanza";
module "rostermanager"
@@ -83,15 +81,15 @@ end
function load_roster(username, host)
local jid = username.."@"..host;
- log("debug", "load_roster: asked for: "..jid);
+ log("debug", "load_roster: asked for: %s", jid);
local user = bare_sessions[jid];
local roster;
if user then
roster = user.roster;
if roster then return roster; end
- log("debug", "load_roster: loading for new user: "..username.."@"..host);
+ log("debug", "load_roster: loading for new user: %s@%s", username, host);
else -- Attempt to load roster for non-loaded user
- log("debug", "load_roster: loading for offline user: "..username.."@"..host);
+ log("debug", "load_roster: loading for offline user: %s@%s", username, host);
end
local data, err = datamanager.load(username, host, "roster");
roster = data or {};
@@ -99,16 +97,21 @@ function load_roster(username, host)
if not roster[false] then roster[false] = { broken = err or nil }; end
if roster[jid] then
roster[jid] = nil;
- log("warn", "roster for "..jid.." has a self-contact");
+ log("warn", "roster for %s has a self-contact", jid);
end
if not err then
- hosts[host].events.fire_event("roster-load", username, host, roster);
+ hosts[host].events.fire_event("roster-load", { username = username, host = host, roster = roster });
end
return roster, err;
end
function save_roster(username, host, roster)
- log("debug", "save_roster: saving roster for "..username.."@"..host);
+ if not um_user_exists(username, host) then
+ log("debug", "not saving roster for %s@%s: the user doesn't exist", username, host);
+ return nil;
+ end
+
+ log("debug", "save_roster: saving roster for %s@%s", username, host);
if not roster then
roster = hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster;
--if not roster then
@@ -238,7 +241,7 @@ function set_contact_pending_out(username, host, jid) -- subscribe
roster[jid] = item;
end
item.ask = "subscribe";
- log("debug", "set_contact_pending_out: saving roster; set "..username.."@"..host..".roster["..jid.."].ask=subscribe");
+ log("debug", "set_contact_pending_out: saving roster; set %s@%s.roster[%q].ask=subscribe", username, host, jid);
return save_roster(username, host, roster);
end
function unsubscribe(username, host, jid)
@@ -278,23 +281,21 @@ function unsubscribed(username, host, jid)
local roster = load_roster(username, host);
local item = roster[jid];
local pending = is_contact_pending_in(username, host, jid);
- local changed = nil;
- if is_contact_pending_in(username, host, jid) then
+ if pending then
roster.pending[jid] = nil; -- TODO maybe delete roster.pending if empty?
- changed = true;
end
+ local subscribed;
if item then
if item.subscription == "from" then
item.subscription = "none";
- changed = true;
+ subscribed = true;
elseif item.subscription == "both" then
item.subscription = "to";
- changed = true;
+ subscribed = true;
end
end
- if changed then
- return save_roster(username, host, roster);
- end
+ local success = (pending or subscribed) and save_roster(username, host, roster);
+ return success, pending, subscribed;
end
function process_outbound_subscription_request(username, host, jid)
diff --git a/core/s2smanager.lua b/core/s2smanager.lua
index 7e6f8135..59c1831b 100644
--- a/core/s2smanager.lua
+++ b/core/s2smanager.lua
@@ -1,599 +1,43 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-local hosts = hosts;
-local sessions = sessions;
-local core_process_stanza = function(a, b) core_process_stanza(a, b); end
-local add_task = require "util.timer".add_task;
-local socket = require "socket";
-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 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 uuid_gen = require "util.uuid".generate;
+local hosts = prosody.hosts;
+local tostring, pairs, setmetatable
+ = tostring, pairs, setmetatable;
local logger_init = require "util.logger".init;
local log = logger_init("s2smanager");
-local sha256_hash = require "util.hashes".sha256;
-
-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 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;
local incoming_s2s = incoming_s2s;
+local fire_event = prosody.events.fire_event;
module "s2smanager"
-function compare_srv_priorities(a,b)
- 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
- session.log("info", "sending error replies for "..#sendq.." queued stanzas because of failed outgoing connection to "..tostring(session.to_host));
- local dummy = {
- type = "s2sin";
- send = function(s)
- (session.log or log)("error", "Replying to to an s2s error reply, please report this! Traceback: %s", get_traceback());
- end;
- dummy = true;
- };
- for i, data in ipairs(sendq) do
- local reply = data[2];
- local xmlns = reply.attr.xmlns;
- if not(xmlns) and bouncy_stanzas[reply.name] 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("Connection failed: "..reason):up();
- end
- core_process_stanza(dummy, reply);
- end
- sendq[i] = nil;
- end
- session.sendq = nil;
- end
-end
-
-function send_to_host(from_host, to_host, data)
- if not hosts[from_host] then
- log("warn", "Attempt to send stanza from %s - a host we don't serve", from_host);
- return false;
- end
- local host = hosts[from_host].s2sout[to_host];
- if host then
- -- We have a connection to this host already
- if host.type == "s2sout_unauthed" and (data.name ~= "db:verify" or not host.dialback_key) then
- (host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host);
-
- -- Queue stanza until we are able to send it
- if 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
- if host.from_host ~= from_host then
- log("error", "WARNING! This might, possibly, be a bug, but it might not...");
- log("error", "We are going to send from %s instead of %s", tostring(host.from_host), tostring(from_host));
- end
- host.sends2s(data);
- host.log("debug", "stanza sent over "..host.type);
- end
- else
- log("debug", "opening a new outgoing connection for this stanza");
- local host_session = new_outgoing(from_host, to_host);
-
- -- Store in buffer
- 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;
- end
- end
- return true;
-end
-
-local open_sessions = 0;
-
function new_incoming(conn)
local session = { conn = conn, type = "s2sin_unauthed", direction = "incoming", hosts = {} };
- if true then
- session.trace = newproxy(true);
- getmetatable(session.trace).__gc = function () open_sessions = open_sessions - 1; end;
- end
- 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.log = logger_init("s2sin"..tostring(session):match("[a-f0-9]+$"));
incoming_s2s[session] = true;
- add_task(connect_timeout, function ()
- if session.conn ~= conn or
- session.type == "s2sin" then
- return; -- Ok, we're connect[ed|ing]
- end
- -- Not connected, need to close session and clean up
- (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);
return session;
end
-function new_outgoing(from_host, to_host, connect)
- local host_session = { to_host = to_host, from_host = from_host, host = from_host,
- notopen = true, type = "s2sout_unauthed", direction = "outgoing",
- open_stream = session_open_stream };
-
- 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]*$");
- log = logger_init(conn_name);
- 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
- end
-
- if not host_session.sends2s then
- -- A sends2s which buffers data (until the stream is opened)
- -- note that data in this buffer will be sent before the stream is authed
- -- and will not be ack'd in any way, successful or otherwise
- local buffer;
- function host_session.sends2s(data)
- if not buffer then
- buffer = {};
- host_session.send_buffer = buffer;
- end
- log("debug", "Buffering data on unconnected s2sout to %s", to_host);
- buffer[#buffer+1] = data;
- log("debug", "Buffered item %d: %s", #buffer, tostring(data));
- end
- end
-
- return host_session;
-end
-
-
-function attempt_connection(host_session, err)
- local from_host, to_host = host_session.from_host, host_session.to_host;
- local connect_host, connect_port = to_host and idna_to_ascii(to_host), 5269;
-
- if not connect_host then
- return false;
- end
-
- if not err then -- This is our first attempt
- log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host);
- host_session.connecting = true;
- local handle;
- handle = adns.lookup(function (answer)
- handle = nil;
- host_session.connecting = nil;
- if answer then
- log("debug", to_host.." has SRV records, handling...");
- local srv_hosts = {};
- host_session.srv_hosts = srv_hosts;
- for _, record in ipairs(answer) do
- t_insert(srv_hosts, record.srv);
- end
- t_sort(srv_hosts, compare_srv_priorities);
-
- local srv_choice = srv_hosts[1];
- host_session.srv_choice = 1;
- if srv_choice then
- connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
- log("debug", "Best record found, will connect to %s:%d", connect_host, connect_port);
- end
- else
- log("debug", to_host.." has no SRV records, falling back to A");
- end
- -- Try with SRV, or just the plain hostname if no SRV
- local ok, err = try_connect(host_session, connect_host, connect_port);
- if not ok then
- if not attempt_connection(host_session, err) then
- -- No more attempts will be made
- destroy_session(host_session, err);
- end
- end
- end, "_xmpp-server._tcp."..connect_host..".", "SRV");
-
- 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;
- local srv_choice = host_session.srv_hosts[host_session.srv_choice];
- connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
- host_session.log("info", "Connection failed (%s). Attempt #%d: This time to %s:%d", tostring(err), host_session.srv_choice, connect_host, connect_port);
- else
- host_session.log("info", "Out of connection options, can't connect to %s", tostring(host_session.to_host));
- -- We're out of options
- return false;
- end
-
- if not (connect_host and connect_port) then
- -- Likely we couldn't resolve DNS
- log("warn", "Hmm, we're without a host (%s) and port (%s) to connect to for %s, giving up :(", tostring(connect_host), tostring(connect_port), tostring(to_host));
- return false;
- end
-
- return try_connect(host_session, connect_host, connect_port);
-end
-
-function try_connect(host_session, connect_host, connect_port)
- host_session.connecting = true;
- local handle;
- handle = adns.lookup(function (reply, err)
- handle = nil;
- host_session.connecting = nil;
-
- -- COMPAT: This is a compromise for all you CNAME-(ab)users :)
- if not (reply and reply[#reply] and reply[#reply].a) then
- local count = max_dns_depth;
- reply = dns.peek(connect_host, "CNAME", "IN");
- while count > 0 and reply and reply[#reply] and not reply[#reply].a and reply[#reply].cname do
- log("debug", "Looking up %s (DNS depth is %d)", tostring(reply[#reply].cname), count);
- reply = dns.peek(reply[#reply].cname, "A", "IN") or dns.peek(reply[#reply].cname, "CNAME", "IN");
- count = count - 1;
- end
- end
- -- end of CNAME resolving
-
- 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
- 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
- end
- end
- end, connect_host, "A", "IN");
-
- return true;
-end
-
-function make_connect(host_session, connect_host, connect_port)
- (host_session.log or log)("info", "Beginning new connection attempt to %s (%s:%d)", host_session.to_host, connect_host, connect_port);
- -- Ok, we're going to try to connect
-
- local from_host, to_host = host_session.from_host, host_session.to_host;
-
- local conn, handler = socket.tcp();
-
- if not conn then
- log("warn", "Failed to create outgoing connection, system error: %s", handler);
- return false, handler;
- end
-
- conn:settimeout(0);
- local success, err = conn:connect(connect_host, connect_port);
- if not success and err ~= "timeout" then
- log("warn", "s2s connect() to %s (%s:%d) failed: %s", host_session.to_host, connect_host, connect_port, err);
- return false, err;
- end
-
- local cl = connlisteners_get("xmppserver");
- 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);
-
- host_session:open_stream(from_host, to_host);
-
- log("debug", "Connection attempt in progress...");
- add_task(connect_timeout, function ()
- if host_session.conn ~= conn or
- host_session.type == "s2sout" or
- host_session.connecting then
- return; -- Ok, we're connect[ed|ing]
- end
- -- Not connected, need to close session and clean up
- (host_session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity",
- host_session.from_host or "(unknown)", host_session.to_host or "(unknown)");
- host_session:close("connection-timeout");
- end);
- return true;
-end
-
-function session_open_stream(session, from, to)
- session.sends2s(st.stanza("stream:stream", {
- xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
- ["xmlns:stream"]='http://etherx.jabber.org/streams',
- 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.from_host = attr.from and nameprep(attr.from);
-
- 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
- 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 session.version >= 1.0 then
- local features = st.stanza("stream:features");
-
- if session.to_host then
- hosts[session.to_host].events.fire_event("s2s-stream-features", { origin = session, features = features });
- else
- (session.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", session.from_host or "unknown host");
- end
-
- log("debug", "Sending stream features: %s", tostring(features));
- send(features);
- end
- elseif session.direction == "outgoing" then
- -- 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
- -- we can only send if auth succeeds) :)
- local send_buffer = session.send_buffer;
- if send_buffer and #send_buffer > 0 then
- log("debug", "Sending s2s send_buffer now...");
- for i, data in ipairs(send_buffer) do
- session.sends2s(tostring(data));
- send_buffer[i] = nil;
- end
- end
- session.send_buffer = nil;
-
- -- If server is pre-1.0, don't wait for features, just do dialback
- if session.version < 1.0 then
- if not session.dialback_verifying then
- log("debug", "Initiating dialback...");
- initiate_dialback(session);
- else
- mark_connected(session);
- end
- end
- end
- session.notopen = nil;
-end
-
-function streamclosed(session)
- (session.log or log)("debug", "Received </stream:stream>");
- session:close();
-end
-
-function initiate_dialback(session)
- -- generate dialback key
- session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host);
- session.sends2s(format("<db:result from='%s' to='%s'>%s</db:result>", session.from_host, session.to_host, session.dialback_key));
- session.log("info", "sent dialback key on outgoing s2s stream");
-end
-
-function generate_dialback(id, to, from)
- return sha256_hash(id..to..from..hosts[from].dialback_secret, true);
-end
-
-function verify_dialback(id, to, from, key)
- return key == generate_dialback(id, to, from);
-end
-
-function make_authenticated(session, host)
- if not session.secure then
- local local_host = session.direction == "incoming" and session.to_host or session.from_host;
- if config.get(local_host, "core", "s2s_require_encryption") then
- session:close({
- condition = "policy-violation",
- text = "Encrypted server-to-server communication is required but was not "
- ..((session.direction == "outgoing" and "offered") or "used")
- });
- end
- end
- if session.type == "s2sout_unauthed" then
- session.type = "s2sout";
- 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;
- end
- session.log("debug", "connection %s->%s is now authenticated", session.from_host or "(unknown)", session.to_host or "(unknown)");
-
- mark_connected(session);
-
- return true;
-end
-
--- Stream is authorised, and ready for normal stanzas
-function mark_connected(session)
- local sendq, send = session.sendq, session.sends2s;
-
- local from, to = session.from_host, session.to_host;
-
- 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
-
- 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
- session.log("debug", "sending "..#sendq.." queued stanzas across new outgoing connection to "..session.to_host);
- for i, data in ipairs(sendq) do
- send(data[1]);
- sendq[i] = nil;
- end
- session.sendq = nil;
- end
-
- session.srv_hosts = nil;
- end
+function new_outgoing(from_host, to_host)
+ local host_session = { to_host = to_host, from_host = from_host, host = from_host,
+ notopen = true, type = "s2sout_unauthed", direction = "outgoing" };
+ hosts[from_host].s2sout[to_host] = host_session;
+ local conn_name = "s2sout"..tostring(host_session):match("[a-f0-9]*$");
+ host_session.log = logger_init(conn_name);
+ return host_session;
end
local resting_session = { -- Resting, not dead
@@ -611,7 +55,7 @@ local resting_session = { -- Resting, not dead
function retire_session(session, reason)
local log = session.log or log;
for k in pairs(session) do
- if k ~= "trace" and k ~= "log" and k ~= "id" then
+ if k ~= "log" and k ~= "id" and k ~= "conn" then
session[k] = nil;
end
end
@@ -625,28 +69,28 @@ end
function destroy_session(session, reason)
if session.destroyed then return; end
- (session.log or log)("info", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host));
-
+ (session.log or log)("debug", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host)..(reason and (": "..reason) or ""));
+
if session.direction == "outgoing" then
hosts[session.from_host].s2sout[session.to_host] = nil;
- bounce_sendq(session, reason);
+ session:bounce_sendq(reason);
elseif session.direction == "incoming" then
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);
+ 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);
+ fire_event("s2sin-destroyed", event_data);
if hosts[session.to_host] then
hosts[session.to_host].events.fire_event("s2sin-destroyed", event_data);
end
end
-
+
retire_session(session, reason); -- Clean session until it is GC'd
return true;
end
diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua
index d36591bf..5f7f688e 100644
--- a/core/sessionmanager.lua
+++ b/core/sessionmanager.lua
@@ -1,57 +1,33 @@
-- 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 tonumber, tostring, setmetatable = tonumber, tostring, setmetatable;
-local ipairs, pairs, print, next= ipairs, pairs, print, next;
-local format = import("string", "format");
+local tostring, setmetatable = tostring, setmetatable;
+local pairs, next= pairs, next;
local hosts = hosts;
local full_sessions = full_sessions;
local bare_sessions = bare_sessions;
-local modulemanager = require "core.modulemanager";
local logger = require "util.logger";
local log = logger.init("sessionmanager");
-local error = error;
-local uuid_generate = require "util.uuid".generate;
local rm_load_roster = require "core.rostermanager".load_roster;
local config_get = require "core.configmanager".get;
-local nameprep = require "util.encodings".stringprep.nameprep;
local resourceprep = require "util.encodings".stringprep.resourceprep;
local nodeprep = require "util.encodings".stringprep.nodeprep;
+local uuid_generate = require "util.uuid".generate;
local initialize_filters = require "util.filters".initialize;
-local fire_event = prosody.events.fire_event;
-local add_task = require "util.timer".add_task;
local gettime = require "socket".gettime;
-local st = require "util.stanza";
-
-local c2s_timeout = config_get("*", "core", "c2s_timeout");
-
-local newproxy = newproxy;
-local getmetatable = getmetatable;
-
module "sessionmanager"
-local open_sessions = 0;
-
function new_session(conn)
local session = { conn = conn, type = "c2s_unauthed", conntime = gettime() };
- if true then
- session.trace = newproxy(true);
- getmetatable(session.trace).__gc = function () open_sessions = open_sessions - 1; end;
- 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)
@@ -66,17 +42,9 @@ function new_session(conn)
end
end
session.ip = conn:ip();
- local conn_name = "c2s"..tostring(conn):match("[a-f0-9]+$");
+ local conn_name = "c2s"..tostring(session):match("[a-f0-9]+$");
session.log = logger.init(conn_name);
-
- if c2s_timeout then
- add_task(c2s_timeout, function ()
- if session.type == "c2s_unauthed" then
- session:close("connection-timeout");
- end
- end);
- end
-
+
return session;
end
@@ -92,34 +60,41 @@ local resting_session = { -- Resting, not dead
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
+ if k ~= "log" and k ~= "id" then
session[k] = nil;
end
end
- function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); end
+ function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); return false; end
function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end
return setmetatable(session, resting_session);
end
function destroy_session(session, err)
- (session.log or log)("info", "Destroying session for %s (%s@%s)", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)");
+ (session.log or log)("debug", "Destroying session for %s (%s@%s)%s", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)", err and (": "..err) or "");
if session.destroyed then return; end
-
+
-- Remove session/resource from user's session list
if session.full_jid then
- hosts[session.host].sessions[session.username].sessions[session.resource] = nil;
+ 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;
full_sessions[session.full_jid] = nil;
-
- if not next(hosts[session.host].sessions[session.username].sessions) then
+
+ if not next(host_session.sessions[session.username].sessions) then
log("debug", "All resources of %s are now offline", session.username);
- hosts[session.host].sessions[session.username] = nil;
+ host_session.sessions[session.username] = nil;
bare_sessions[session.username..'@'..session.host] = nil;
end
- hosts[session.host].events.fire_event("resource-unbind", {session=session, error=err});
+ host_session.events.fire_event("resource-unbind", {session=session, error=err});
end
-
+
retire_session(session);
end
@@ -144,20 +119,16 @@ function bind_resource(session, resource)
resource = resourceprep(resource);
resource = resource ~= "" and resource or uuid_generate();
--FIXME: Randomly-generated resources must be unique per-user, and never conflict with existing
-
+
if not hosts[session.host].sessions[session.username] then
local sessions = { sessions = {} };
hosts[session.host].sessions[session.username] = sessions;
bare_sessions[session.username..'@'..session.host] = sessions;
else
local sessions = hosts[session.host].sessions[session.username].sessions;
- local limit = config_get(session.host, "core", "max_resources") or 10;
- if #sessions >= limit then
- return nil, "cancel", "resource-constraint", "Resource limit reached; only "..limit.." resources allowed";
- end
if sessions[resource] then
-- Resource conflict
- local policy = config_get(session.host, "core", "conflict_resolve");
+ local policy = config_get(session.host, "conflict_resolve");
local increment;
if policy == "random" then
resource = uuid_generate();
@@ -185,12 +156,12 @@ function bind_resource(session, resource)
end
end
end
-
+
session.resource = resource;
session.full_jid = session.username .. '@' .. session.host .. '/' .. resource;
hosts[session.host].sessions[session.username].sessions[resource] = session;
full_sessions[session.full_jid] = session;
-
+
local err;
session.roster, err = rm_load_roster(session.username, session.host);
if err then
@@ -202,56 +173,13 @@ function bind_resource(session, resource)
bare_sessions[session.username..'@'..session.host] = nil;
hosts[session.host].sessions[session.username] = nil;
end
+ session.log("error", "Roster loading failed: %s", err);
return nil, "cancel", "internal-server-error", "Error loading roster";
end
-
- hosts[session.host].events.fire_event("resource-bind", {session=session});
-
- return true;
-end
-
-function streamopened(session, attr)
- local send = session.send;
- session.host = attr.to;
- if not session.host then
- session:close{ condition = "improper-addressing",
- text = "A 'to' attribute is required on stream headers" };
- return;
- end
- session.host = nameprep(session.host);
- session.version = tonumber(attr.version) or 0;
- session.streamid = uuid_generate();
- (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host);
-
- if not hosts[session.host] then
- -- We don't serve this host...
- session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)};
- return;
- end
-
- send("<?xml version='1.0'?>");
- send(format("<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='%s' from='%s' version='1.0' xml:lang='en'>", session.streamid, session.host));
-
- (session.log or log)("debug", "Sent reply <stream:stream> to client");
- session.notopen = nil;
-
- -- If session.secure is *false* (not nil) then it means we /were/ encrypting
- -- since we now have a new stream header, session is secured
- if session.secure == false then
- session.secure = true;
- end
- local features = st.stanza("stream:features");
- hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
- fire_event("stream-features", session, features);
-
- send(features);
-
-end
+ hosts[session.host].events.fire_event("resource-bind", {session=session});
-function streamclosed(session)
- session.log("debug", "Received </stream:stream>");
- session:close();
+ return true;
end
function send_to_available_resources(user, host, stanza)
diff --git a/core/stanza_router.lua b/core/stanza_router.lua
index 97d328a1..c78a657a 100644
--- a/core/stanza_router.lua
+++ b/core/stanza_router.lua
@@ -1,7 +1,7 @@
-- 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.
--
@@ -11,13 +11,24 @@ local log = require "util.logger".init("stanzarouter")
local hosts = _G.prosody.hosts;
local tostring = tostring;
local st = require "util.stanza";
-local send_s2s = require "core.s2smanager".send_to_host;
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 core_post_stanza, core_process_stanza, core_route_stanza;
+
+function deprecated_warning(f)
+ _G[f] = function(...)
+ log("warn", "Using the global %s() is deprecated, use module:send() or prosody.%s(). %s", f, f, debug.traceback());
+ return prosody[f](...);
+ end
+end
+deprecated_warning"core_post_stanza";
+deprecated_warning"core_process_stanza";
+deprecated_warning"core_route_stanza";
+
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
@@ -29,17 +40,18 @@ local function handle_unhandled_stanza(host, origin, stanza)
return true;
end
end
- if stanza.attr.xmlns == nil then
+ if stanza.attr.xmlns == nil and origin.send 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
+ log("warn", "Unhandled %s stream element or stanza: %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())
@@ -47,8 +59,8 @@ function core_process_stanza(origin, stanza)
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 (stanza.attr.type == "set" or stanza.attr.type == "get") and (#stanza.tags ~= 1) then
- origin.send(st.error_reply(stanza, "modify", "bad-request"));
+ 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"));
return;
end
end
@@ -104,17 +116,17 @@ function core_process_stanza(origin, stanza)
stanza.attr.from = from;
end
- --[[if to and not(hosts[to]) and not(hosts[to_bare]) and (hosts[host] and hosts[host].type ~= "local") then -- not for us?
- log("warn", "stanza recieved for a non-local server");
- return; -- FIXME what should we do here?
- end]] -- FIXME
-
if (origin.type == "s2sin" or origin.type == "c2s" or origin.type == "component") and xmlns == nil then
if origin.type == "s2sin" and not origin.dummy then
local host_status = origin.hosts[from_host];
if not host_status or not host_status.authed then -- remote server trying to impersonate some other server?
log("warn", "Received a stanza claiming to be from %s, over a stream authed for %s!", from_host, origin.from_host);
- return; -- FIXME what should we do here? does this work with subdomains?
+ origin:close("not-authorized");
+ return;
+ elseif not hosts[host] then
+ log("warn", "Remote server %s sent us a stanza for %s, closing stream", origin.from_host, host);
+ origin:close("host-unknown");
+ return;
end
end
core_post_stanza(origin, stanza, origin.full_jid);
@@ -184,30 +196,28 @@ function core_route_stanza(origin, stanza)
-- Auto-detect origin if not specified
origin = origin or hosts[from_host];
if not origin then return false; end
-
+
if hosts[host] then
-- old stanza routing code removed
core_post_stanza(origin, stanza);
- elseif origin.type == "c2s" then
- -- Remote host
- if not hosts[from_host] then
+ else
+ log("debug", "Routing to remote...");
+ local host_session = hosts[from_host];
+ if not host_session then
log("error", "No hosts[from_host] (please report): %s", tostring(stanza));
- end
- if (not hosts[from_host]) or (not hosts[from_host].disallow_s2s) then
+ else
local xmlns = stanza.attr.xmlns;
- --stanza.attr.xmlns = "jabber:server";
stanza.attr.xmlns = nil;
- log("debug", "sending s2s stanza: %s", tostring(stanza.top_tag and stanza:top_tag()) or stanza);
- send_s2s(origin.host, host, stanza); -- TODO handle remote routing errors
+ local routed = host_session.events.fire_event("route/remote", { origin = origin, stanza = stanza, from_host = from_host, to_host = host });
stanza.attr.xmlns = xmlns; -- reset
- else
- core_route_stanza(hosts[from_host], st.error_reply(stanza, "cancel", "not-allowed", "Communication with remote servers is not allowed"));
+ if not routed then
+ log("debug", "... no, just kidding.");
+ if stanza.attr.type == "error" or (stanza.name == "iq" and stanza.attr.type == "result") then return; end
+ core_route_stanza(host_session, st.error_reply(stanza, "cancel", "not-allowed", "Communication with remote domains is not enabled"));
+ end
end
- elseif origin.type == "component" or origin.type == "local" then
- -- Route via s2s for components and modules
- log("debug", "Routing outgoing stanza for %s to %s", from_host, host);
- send_s2s(from_host, host, stanza);
- else
- log("warn", "received %s stanza from unhandled connection type: %s", tostring(stanza.name), tostring(origin.type));
end
end
+prosody.core_process_stanza = core_process_stanza;
+prosody.core_post_stanza = core_post_stanza;
+prosody.core_route_stanza = core_route_stanza;
diff --git a/core/storagemanager.lua b/core/storagemanager.lua
index 43409960..5674ff32 100644
--- a/core/storagemanager.lua
+++ b/core/storagemanager.lua
@@ -1,5 +1,5 @@
-local error, type = error, type;
+local error, type, pairs = error, type, pairs;
local setmetatable = setmetatable;
local config = require "core.configmanager";
@@ -9,55 +9,57 @@ local multitable = require "util.multitable";
local hosts = hosts;
local log = require "util.logger".init("storagemanager");
-local olddm = {}; -- maintain old datamanager, for backwards compatibility
-for k,v in pairs(datamanager) do olddm[k] = v; end
local prosody = prosody;
module("storagemanager")
-local default_driver_mt = { name = "internal" };
-default_driver_mt.__index = default_driver_mt;
-function default_driver_mt:open(store)
- return setmetatable({ host = self.host, store = store }, default_driver_mt);
-end
-function default_driver_mt:get(user) return olddm.load(user, self.host, self.store); end
-function default_driver_mt:set(user, data) return olddm.store(user, self.host, self.store, data); end
+local olddm = {}; -- maintain old datamanager, for backwards compatibility
+for k,v in pairs(datamanager) do olddm[k] = v; end
+_M.olddm = olddm;
+
+local null_storage_method = function () return false, "no data storage active"; end
+local null_storage_driver = setmetatable(
+ {
+ name = "null",
+ open = function (self) return self; end
+ }, {
+ __index = function (self, method)
+ return null_storage_method;
+ end
+ }
+);
local stores_available = multitable.new();
function initialize_host(host)
local host_session = hosts[host];
- host_session.events.add_handler("item-added/data-driver", function (event)
+ host_session.events.add_handler("item-added/storage-provider", function (event)
local item = event.item;
stores_available:set(host, item.name, item);
end);
-
- host_session.events.add_handler("item-removed/data-driver", function (event)
+
+ host_session.events.add_handler("item-removed/storage-provider", function (event)
local item = event.item;
stores_available:set(host, item.name, nil);
end);
end
prosody.events.add_handler("host-activated", initialize_host, 101);
-local function load_driver(host, driver_name)
- if not driver_name then
- return;
+function load_driver(host, driver_name)
+ if driver_name == "null" then
+ return null_storage_driver;
end
local driver = stores_available:get(host, driver_name);
if driver then return driver; end
- if driver_name ~= "internal" then
- local ok, err = modulemanager.load(host, "storage_"..driver_name);
- if not ok then
- log("error", "Failed to load storage driver plugin %s: %s", driver_name, err);
- end
- return stores_available:get(host, driver_name);
- else
- return setmetatable({host = host}, default_driver_mt);
+ local ok, err = modulemanager.load(host, "storage_"..driver_name);
+ if not ok then
+ log("error", "Failed to load storage driver plugin %s on %s: %s", driver_name, host, err);
end
+ return stores_available:get(host, driver_name);
end
-function open(host, store, typ)
- local storage = config.get(host, "core", "storage");
+function get_driver(host, store)
+ local storage = config.get(host, "storage");
local driver_name;
local option_type = type(storage);
if option_type == "string" then
@@ -65,38 +67,69 @@ function open(host, store, typ)
elseif option_type == "table" then
driver_name = storage[store];
end
-
+ if not driver_name then
+ driver_name = config.get(host, "default_storage") or "internal";
+ end
+
local driver = load_driver(host, driver_name);
if not driver then
- driver_name = config.get(host, "core", "default_storage");
- driver = load_driver(host, driver_name);
- if not driver then
- if driver_name or (type(storage) == "string"
- or type(storage) == "table" and storage[store]) then
- log("warn", "Falling back to default driver for %s storage on %s", store, host);
- end
- driver_name = "internal";
- driver = load_driver(host, driver_name);
- end
+ log("warn", "Falling back to null driver for %s storage on %s", store, host);
+ driver_name = "null";
+ driver = null_storage_driver;
end
-
+ return driver, driver_name;
+end
+
+function open(host, store, typ)
+ local driver, driver_name = get_driver(host, store);
local ret, err = driver:open(store, typ);
if not ret then
if err == "unsupported-store" then
- log("debug", "Storage driver %s does not support store %s (%s), falling back to internal driver",
- driver_name, store, typ);
- ret = setmetatable({ host = host, store = store }, default_driver_mt); -- default to default driver
+ log("debug", "Storage driver %s does not support store %s (%s), falling back to null driver",
+ driver_name, store, typ or "<nil>");
+ ret = null_storage_driver;
err = nil;
end
end
return ret, err;
end
+function purge(user, host)
+ local storage = config.get(host, "storage");
+ if type(storage) == "table" then
+ -- multiple storage backends in use that we need to purge
+ local purged = {};
+ for store, driver in pairs(storage) do
+ if not purged[driver] then
+ purged[driver] = get_driver(host, store):purge(user);
+ end
+ end
+ end
+ get_driver(host):purge(user); -- and the default driver
+
+ olddm.purge(user, host); -- COMPAT list stores, like offline messages end up in the old datamanager
+
+ return true;
+end
+
function datamanager.load(username, host, datastore)
return open(host, datastore):get(username);
end
function datamanager.store(username, host, datastore, data)
return open(host, datastore):set(username, data);
end
+function datamanager.users(host, datastore, typ)
+ local driver = open(host, datastore, typ);
+ if not driver.users then
+ return function() log("warn", "storage driver %s does not support listing users", driver.name) end
+ end
+ return driver:users();
+end
+function datamanager.stores(username, host, typ)
+ return get_driver(host):stores(username, typ);
+end
+function datamanager.purge(username, host)
+ return purge(username, host);
+end
return _M;
diff --git a/core/usermanager.lua b/core/usermanager.lua
index 2e64af8c..4ac288a4 100644
--- a/core/usermanager.lua
+++ b/core/usermanager.lua
@@ -11,9 +11,11 @@ local log = require "util.logger".init("usermanager");
local type = type;
local ipairs = ipairs;
local jid_bare = require "util.jid".bare;
+local jid_prep = require "util.jid".prep;
local config = require "core.configmanager";
local hosts = hosts;
local sasl_new = require "util.sasl".new;
+local storagemanager = require "core.storagemanager";
local prosody = _G.prosody;
@@ -24,21 +26,28 @@ local default_provider = "internal_plain";
module "usermanager"
function new_null_provider()
- local function dummy() end;
+ 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() return dummy; end });
+ return setmetatable({name = "null", get_sasl_handler = dummy_get_sasl_handler}, {
+ __index = function(self, method) return dummy; end
+ });
end
+local provider_mt = { __index = new_null_provider() };
+
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
+ local auth_provider = config.get(host, "authentication") or default_provider;
+ if config.get(host, "anonymous_login") then
+ log("error", "Deprecated config option 'anonymous_login'. Use authentication = 'anonymous' instead.");
+ auth_provider = "anonymous";
+ end -- COMPAT 0.7
if provider.name == auth_provider then
- host_session.users = provider;
+ 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);
@@ -51,8 +60,8 @@ function initialize_host(host)
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
+ local auth_provider = config.get(host, "authentication") or default_provider;
+ if config.get(host, "anonymous_login") then auth_provider = "anonymous"; end -- COMPAT 0.7
if auth_provider ~= "null" then
modulemanager.load(host, "auth_"..auth_provider);
end
@@ -79,8 +88,19 @@ function create_user(username, password, host)
return hosts[host].users.create_user(username, password);
end
-function get_sasl_handler(host)
- return hosts[host].users.get_sasl_handler();
+function delete_user(username, host)
+ local ok, err = hosts[host].users.delete_user(username);
+ if not ok then return nil, err; end
+ prosody.events.fire_event("user-deleted", { username = username, host = host });
+ return storagemanager.purge(username, host);
+end
+
+function users(host)
+ return hosts[host].users.users();
+end
+
+function get_sasl_handler(host, session)
+ return hosts[host].users.get_sasl_handler(session);
end
function get_provider(host)
@@ -88,17 +108,20 @@ function get_provider(host)
end
function is_admin(jid, host)
+ if host and not hosts[host] then return false; end
+ if type(jid) ~= "string" 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");
-
+
+ local host_admins = config.get(host, "admins");
+ local global_admins = config.get("*", "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
+ if jid_prep(admin) == jid then
is_admin = true;
break;
end
@@ -107,11 +130,11 @@ function is_admin(jid, host)
log("error", "Option 'admins' for host '%s' is not a list", host);
end
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
+ if jid_prep(admin) == jid then
is_admin = true;
break;
end
@@ -120,7 +143,7 @@ function is_admin(jid, host)
log("error", "Global option 'admins' is not a list");
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);