aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/actions.lua27
-rw-r--r--core/certmanager.lua108
-rw-r--r--core/componentmanager.lua141
-rw-r--r--core/configmanager.lua224
-rw-r--r--core/discomanager.lua66
-rw-r--r--core/eventmanager.lua33
-rw-r--r--core/hostmanager.lua141
-rw-r--r--core/loggingmanager.lua166
-rw-r--r--core/moduleapi.lua367
-rw-r--r--core/modulemanager.lua511
-rw-r--r--core/objectmanager.lua68
-rw-r--r--core/offlinemanager.lua41
-rw-r--r--core/portmanager.lua239
-rw-r--r--core/rostermanager.lua130
-rw-r--r--core/s2smanager.lua419
-rw-r--r--core/sessionmanager.lua200
-rw-r--r--core/stanza_router.lua131
-rw-r--r--core/storagemanager.lua135
-rw-r--r--core/usermanager.lua167
-rw-r--r--core/xmlhandlers.lua140
20 files changed, 1853 insertions, 1601 deletions
diff --git a/core/actions.lua b/core/actions.lua
deleted file mode 100644
index 5c2525e0..00000000
--- a/core/actions.lua
+++ /dev/null
@@ -1,27 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-local actions = {};
-
-function register(path, t)
- local curr = actions;
- for comp in path:gmatch("([^/]+)/") do
- if curr[comp] == nil then
- curr[comp] = {};
- end
- curr = curr[comp];
- if type(curr) ~= "table" then
- return nil, "path-taken";
- end
- end
- curr[path:match("/([^/]+)$")] = t;
- return true;
-end
-
-return { actions = actions, register= register }; \ No newline at end of file
diff --git a/core/certmanager.lua b/core/certmanager.lua
new file mode 100644
index 00000000..b91f7110
--- /dev/null
+++ b/core/certmanager.lua
@@ -0,0 +1,108 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local configmanager = require "core.configmanager";
+local log = require "util.logger".init("certmanager");
+local ssl = ssl;
+local ssl_newcontext = ssl and ssl.newcontext;
+
+local tostring = tostring;
+
+local prosody = prosody;
+local resolve_path = configmanager.resolve_relative_path;
+local config_path = prosody.paths.config;
+
+local luasec_has_noticket, luasec_has_verifyext;
+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;
+end
+
+module "certmanager"
+
+-- Global SSL options if not overridden per-host
+local default_ssl_config = configmanager.get("*", "ssl");
+local default_capath = "/etc/ssl/certs";
+local default_verify = (ssl and ssl.x509 and { "peer", "client_once", }) or "none";
+local default_options = { "no_sslv2", luasec_has_noticket and "no_ticket" or nil };
+local default_verifyext = { "lsec_continue", "lsec_ignore_purpose" };
+
+if ssl and not luasec_has_verifyext and ssl.x509 then
+ -- COMPAT mw/luasec-hg
+ for i=1,#default_verifyext do -- Remove lsec_ prefix
+ default_verify[#default_verify+1] = default_verifyext[i]:sub(6);
+ end
+end
+
+function create_context(host, mode, user_ssl_config)
+ user_ssl_config = user_ssl_config or default_ssl_config;
+
+ 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 or function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end;
+ 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;
+ verifyext = user_ssl_config.verifyext or default_verifyext;
+ options = user_ssl_config.options or default_options;
+ depth = user_ssl_config.depth;
+ };
+
+ local ctx, err = ssl_newcontext(ssl_config);
+
+ -- 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";
+ elseif file == "certificate" then
+ file = ssl_config.certificate or "your certificate file";
+ end
+ local reason = err:match("%((.+)%)$") or "some reason";
+ if reason == "Permission denied" then
+ reason = "Check that the permissions allow Prosody to read this file.";
+ elseif reason == "No such file or directory" then
+ reason = "Check that the path is correct, and the file exists.";
+ elseif reason == "system lib" then
+ reason = "Previous error (see logs), or other system error.";
+ elseif reason == "(null)" or not reason then
+ reason = "Check that the file exists and the permissions are correct";
+ else
+ reason = "Reason: "..tostring(reason):lower();
+ end
+ log("error", "SSL/TLS: Failed to load '%s': %s (for %s)", file, reason, host);
+ else
+ 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("*", "ssl");
+end
+
+prosody.events.add_handler("config-reloaded", reload_ssl_config);
+
+return _M;
diff --git a/core/componentmanager.lua b/core/componentmanager.lua
deleted file mode 100644
index 08868236..00000000
--- a/core/componentmanager.lua
+++ /dev/null
@@ -1,141 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-
-local prosody = prosody;
-local log = require "util.logger".init("componentmanager");
-local configmanager = require "core.configmanager";
-local modulemanager = require "core.modulemanager";
-local core_route_stanza = core_route_stanza;
-local jid_split = require "util.jid".split;
-local events_new = require "util.events".new;
-local st = require "util.stanza";
-local hosts = hosts;
-local serialize = require "util.serialization".serialize
-
-local pairs, type, tostring = pairs, type, tostring;
-
-local components = {};
-
-local disco_items = require "util.multitable".new();
-local NULL = {};
-require "core.discomanager".addDiscoItemsHandler("*host", function(reply, to, from, node)
- if #node == 0 and hosts[to] then
- for jid in pairs(disco_items:get(to) or NULL) do
- reply:tag("item", {jid = jid}):up();
- end
- return true;
- end
-end);
-
-prosody.events.add_handler("server-starting", function () core_route_stanza = _G.core_route_stanza; end);
-
-module "componentmanager"
-
-local function default_component_handler(origin, stanza)
- log("warn", "Stanza being handled by default component, bouncing error");
- if stanza.attr.type ~= "error" then
- core_route_stanza(nil, st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable"));
- end
-end
-
-local components_loaded_once;
-function load_enabled_components(config)
- local defined_hosts = config or configmanager.getconfig();
-
- for host, host_config in pairs(defined_hosts) do
- if host ~= "*" and ((host_config.core.enabled == nil or host_config.core.enabled) and type(host_config.core.component_module) == "string") then
- hosts[host] = { type = "component", host = host, connected = false, s2sout = {}, events = events_new() };
- components[host] = default_component_handler;
- local ok, err = modulemanager.load(host, host_config.core.component_module);
- if not ok then
- log("error", "Error loading %s component %s: %s", tostring(host_config.core.component_module), tostring(host), tostring(err));
- else
- log("debug", "Activated %s component: %s", host_config.core.component_module, host);
- end
- end
- end
-end
-
-prosody.events.add_handler("server-starting", load_enabled_components);
-
-function handle_stanza(origin, stanza)
- local node, host = jid_split(stanza.attr.to);
- local component = nil;
- if host then
- if node then component = components[node.."@"..host]; end -- hack to allow hooking node@server
- if not component then component = components[host]; end
- end
- if component then
- log("debug", "%s stanza being handled by component: %s", stanza.name, host);
- component(origin, stanza, hosts[host]);
- else
- log("error", "Component manager recieved a stanza for a non-existing component: " .. (stanza.attr.to or serialize(stanza)));
- end
-end
-
-function create_component(host, component)
- -- TODO check for host well-formedness
- return { type = "component", host = host, connected = true, s2sout = {}, events = events_new() };
-end
-
-function register_component(host, component, session)
- if not hosts[host] or (hosts[host].type == 'component' and not hosts[host].connected) then
- local old_events = hosts[host] and hosts[host].events;
-
- components[host] = component;
- hosts[host] = session or create_component(host, component);
-
- -- Add events object if not already one
- if not hosts[host].events then
- hosts[host].events = old_events or events_new();
- end
-
- -- add to disco_items
- if not(host:find("@", 1, true) or host:find("/", 1, true)) and host:find(".", 1, true) then
- disco_items:set(host:sub(host:find(".", 1, true)+1), host, true);
- end
- -- FIXME only load for a.b.c if b.c has dialback, and/or check in config
- modulemanager.load(host, "dialback");
- log("debug", "component added: "..host);
- return session or hosts[host];
- else
- log("error", "Attempt to set component for existing host: "..host);
- end
-end
-
-function deregister_component(host)
- if components[host] then
- modulemanager.unload(host, "dialback");
- hosts[host].connected = nil;
- local host_config = configmanager.getconfig()[host];
- if host_config and ((host_config.core.enabled == nil or host_config.core.enabled) and type(host_config.core.component_module) == "string") then
- -- Set default handler
- components[host] = default_component_handler;
- else
- -- Component not in config, or disabled, remove
- hosts[host] = nil;
- components[host] = nil;
- end
- -- remove from disco_items
- if not(host:find("@", 1, true) or host:find("/", 1, true)) and host:find(".", 1, true) then
- disco_items:remove(host:sub(host:find(".", 1, true)+1), host);
- end
- log("debug", "component removed: "..host);
- return true;
- else
- log("error", "Attempt to remove component for non-existing host: "..host);
- end
-end
-
-function set_component_handler(host, handler)
- components[host] = handler;
-end
-
-return _M;
diff --git a/core/configmanager.lua b/core/configmanager.lua
index b7ee605f..9720f48a 100644
--- a/core/configmanager.lua
+++ b/core/configmanager.lua
@@ -1,77 +1,122 @@
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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 =
- setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type;
+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 eventmanager = require "core.eventmanager";
+local envload = require"util.envload".envload;
+local lfs = require "lfs";
+local path_sep = package.config:sub(1,1);
module "configmanager"
local parsers = {};
-local config = { ["*"] = { core = {} } };
-
-local global_config = config["*"];
+local config_mt = { __index = function (t, k) return rawget(t, "*"); end};
+local config = setmetatable({ ["*"] = { } }, config_mt);
-- When host not found, use global
-setmetatable(config, { __index = function () return global_config; end});
-local host_mt = { __index = global_config };
-
--- When key not found in section, check key in global's section
-function section_mt(section_name)
- return { __index = function (t, k)
- local section = rawget(global_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
-function set(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, 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
+ 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) ~= ":\\" or path:sub(2,3) ~= ":/")) then
+ is_relative = true;
+ end
+ if is_relative then
+ return parent_path..path_sep..path;
+ end
+ end
+ return path;
+ end
+end
+
+-- Helper function to convert a glob to a Lua pattern
+local function glob_to_pattern(glob)
+ return "^"..glob:gsub("[%p*?]", function (c)
+ if c == "*" then
+ return ".*";
+ elseif c == "?" then
+ return ".";
+ else
+ return "%"..c;
+ end
+ end).."$";
+end
+
function load(filename, format)
format = format or filename:match("%w+$");
if parsers[format] and parsers[format].load then
local f, err = io.open(filename);
- if f then
- local ok, err = parsers[format].load(f:read("*a"));
+ if f then
+ local new_config = setmetatable({ ["*"] = { } }, config_mt);
+ local ok, err = parsers[format].load(f:read("*a"), filename, new_config);
f:close();
if ok then
- eventmanager.fire_event("config-reloaded", { filename = filename, format = format });
+ config = new_config;
+ fire_event("config-reloaded", {
+ filename = filename,
+ format = format,
+ config = config
+ });
end
return ok, "parser", err;
end
@@ -94,67 +139,118 @@ function addparser(format, parser)
end
end
+-- _M needed to avoid name clash with local 'parsers'
+function _M.parsers()
+ local p = {};
+ for format in pairs(parsers) do
+ table.insert(p, format);
+ end
+ return p;
+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)
+ function parsers.lua.load(data, config_file, config)
local env;
-- The ' = true' are needed so as not to set off __newindex when we assign the functions below
- env = setmetatable({ Host = true; host = true; Component = true, component = true,
- Include = true, include = true, RunScript = dofile }, { __index = function (t, k)
- return rawget(_G, k) or
- function (settings_table)
- config[__currenthost or "*"][k] = settings_table;
- end;
- end,
- __newindex = function (t, k, v)
- set(env.__currenthost or "*", "core", k, v);
- end});
+ env = setmetatable({
+ Host = true, host = true, VirtualHost = true,
+ Component = true, component = true,
+ Include = true, include = true, RunScript = true }, {
+ __index = function (t, k)
+ return rawget(_G, k);
+ end,
+ __newindex = function (t, k, v)
+ set(config, env.__currenthost or "*", k, v);
+ end
+ });
rawset(env, "__currenthost", "*") -- Default is global
- function env.Host(name)
+ function env.VirtualHost(name)
+ 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].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(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 "*", option_name, option_value);
+ end
+ end;
end
- env.host = env.Host;
+ env.Host, env.host = env.VirtualHost, env.VirtualHost;
function env.Component(name)
- set(name, "core", "component_module", "component");
+ 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, "component_module", "component");
-- Don't load the global modules by default
- set(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 "*", option_name, option_value);
+ end
+ end
return function (module)
if type(module) == "string" then
- set(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)
- local f, err = io.open(file);
- if f then
- local data = f:read("*a");
- local ok, err = parsers.lua.load(data);
- if not ok then error(err:gsub("%[string.-%]", file), 0); end
+ if file:match("[*?]") then
+ local path_pos, glob = file:match("()([^"..path_sep.."]+)$");
+ local path = file:sub(1, math_max(path_pos-2,0));
+ local config_path = config_file:gsub("[^"..path_sep.."]+$", "");
+ if #path > 0 then
+ path = resolve_relative_path(config_path, path);
+ else
+ path = config_path;
+ end
+ local patt = glob_to_pattern(glob);
+ for f in lfs.dir(path) do
+ if f:sub(1,1) ~= "." and f:match(patt) then
+ env.Include(path..path_sep..f);
+ end
+ end
+ else
+ local file = resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file);
+ local f, err = io.open(file);
+ if f then
+ 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
+ return f, err;
end
- if not f then error("Error loading included "..file..": "..err, 0); end
- return f, err;
end
env.include = env.Include;
- local chunk, err = loadstring(data);
+ function env.RunScript(file)
+ return dofile(resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file));
+ end
+
+ 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
diff --git a/core/discomanager.lua b/core/discomanager.lua
deleted file mode 100644
index 742907dd..00000000
--- a/core/discomanager.lua
+++ /dev/null
@@ -1,66 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-
-local helper = require "util.discohelper".new();
-local hosts = hosts;
-local jid_split = require "util.jid".split;
-local jid_bare = require "util.jid".bare;
-local usermanager_user_exists = require "core.usermanager".user_exists;
-local rostermanager_is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
-local print = print;
-
-do
- helper:addDiscoInfoHandler("*host", function(reply, to, from, node)
- if hosts[to] then
- reply:tag("identity", {category="server", type="im", name="Prosody"}):up();
- return true;
- end
- end);
- helper:addDiscoInfoHandler("*node", function(reply, to, from, node)
- local node, host = jid_split(to);
- if hosts[host] and rostermanager_is_contact_subscribed(node, host, jid_bare(from)) then
- reply:tag("identity", {category="account", type="registered"}):up();
- return true;
- end
- end);
- helper:addDiscoItemsHandler("*host", function(reply, to, from, node)
- if hosts[to] and hosts[to].type == "local" then
- return true;
- end
- end);
-end
-
-module "discomanager"
-
-function handle(stanza)
- return helper:handle(stanza);
-end
-
-function addDiscoItemsHandler(jid, func)
- return helper:addDiscoItemsHandler(jid, func);
-end
-
-function addDiscoInfoHandler(jid, func)
- return helper:addDiscoInfoHandler(jid, func);
-end
-
-function set(plugin, var, origin)
- -- TODO handle origin and host based on plugin.
- local handler = function(reply, to, from, node) -- service discovery
- if #node == 0 then
- reply:tag("feature", {var = var}):up();
- return true;
- end
- end
- addDiscoInfoHandler("*node", handler);
- addDiscoInfoHandler("*host", handler);
-end
-
-return _M;
diff --git a/core/eventmanager.lua b/core/eventmanager.lua
deleted file mode 100644
index e1cc9d2e..00000000
--- a/core/eventmanager.lua
+++ /dev/null
@@ -1,33 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-local t_insert = table.insert;
-local ipairs = ipairs;
-
-module "eventmanager"
-
-local event_handlers = {};
-
-function add_event_hook(name, handler)
- if not event_handlers[name] then
- event_handlers[name] = {};
- end
- t_insert(event_handlers[name] , handler);
-end
-
-function fire_event(name, ...)
- local event_handlers = event_handlers[name];
- if event_handlers then
- for name, handler in ipairs(event_handlers) do
- handler(...);
- end
- end
-end
-
-return _M; \ No newline at end of file
diff --git a/core/hostmanager.lua b/core/hostmanager.lua
index ba363273..06ba72a1 100644
--- a/core/hostmanager.lua
+++ b/core/hostmanager.lua
@@ -1,20 +1,32 @@
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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 configmanager = require "core.configmanager";
-local eventmanager = require "core.eventmanager";
+local modulemanager = require "core.modulemanager";
local events_new = require "util.events".new;
+local disco_items = require "util.multitable".new();
+local NULL = {};
+
+local jid_split = require "util.jid".split;
+local uuid_gen = require "util.uuid".generate;
local log = require "util.logger".init("hostmanager");
-local pairs = pairs;
+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, select, rawget = pairs, select, rawget;
+local tostring, type = tostring, type;
module "hostmanager"
@@ -22,52 +34,125 @@ 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 == nil or host_config.core.enabled) 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
- eventmanager.fire_event("hosts-activated", defined_hosts);
+
+ 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
-eventmanager.add_event_hook("server-starting", load_enabled_hosts);
+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)
- hosts[host] = {type = "local", connected = true, sessions = {},
- host = host, s2sout = {}, events = events_new(),
- disallow_s2s = configmanager.get(host, "core", "disallow_s2s")
- or (configmanager.get(host, "core", "anonymous_login")
- and (configmanager.get(host, "core", "disallow_s2s") ~= false))
- };
- for option_name in pairs(host_config.core) do
- if option_name:match("_ports$") then
- log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in global Host \"*\" instead", host, option_name);
+ 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, "dialback_secret") or uuid_gen();
+ send = host_send;
+ modules = {};
+ };
+ if not host_config.component_module then -- host
+ host_session.type = "local";
+ host_session.sessions = {};
+ else -- component
+ host_session.type = "component";
+ end
+ hosts[host] = host_session;
+ if not host:match("[@/]") then
+ disco_items:set(host:match("%.(.*)") or "*", host, host_config.name or true);
+ end
+ for option_name in pairs(host_config) do
+ if option_name:match("_ports$") or option_name:match("_interface$") then
+ log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in the server-wide section instead", host, option_name);
end
end
+
log((hosts_loaded_once and "info") or "debug", "Activated host: %s", host);
- eventmanager.fire_event("host-activated", host, host_config);
+ prosody_events.fire_event("host-activated", host);
+ return true;
end
-function deactivate(host)
+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);
- eventmanager.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
- for user, session_list in pairs(host_session.sessions) do
- for resource, session in pairs(session_list) do
- session:close("host-gone");
+ -- TODO: These should move to mod_c2s and mod_s2s (how do they know they're being unloaded and not reloaded?)
+ if host_session.sessions then
+ for username, user in pairs(host_session.sessions) do
+ for resource, session in pairs(user.sessions) do
+ log("debug", "Closing connection for %s@%s/%s", username, host, resource);
+ session:close(reason);
+ end
end
end
- -- Components?
-
+ if host_session.s2sout then
+ for remotehost, session in pairs(host_session.s2sout) do
+ if session.close then
+ log("debug", "Closing outgoing connection to %s", remotehost);
+ if session.srv_hosts then session.srv_hosts = nil; end
+ session:close(reason);
+ end
+ end
+ end
+ for remote_session in pairs(incoming_s2s) do
+ if remote_session.to_host == host then
+ log("debug", "Closing incoming connection from %s", remote_session.from_host or "<unknown>");
+ remote_session:close(reason);
+ end
+ end
+
+ -- TODO: This should be done in modulemanager
+ if host_session.modules then
+ for module in pairs(host_session.modules) do
+ modulemanager.unload(host, module);
+ end
+ end
+
hosts[host] = nil;
- eventmanager.fire_event("host-deactivated", host);
+ if not host:match("[@/]") then
+ disco_items:remove(host:match("%.(.*)") or "*", host);
+ end
+ prosody_events.fire_event("host-deactivated", host);
log("info", "Deactivated host: %s", host);
+ return true;
end
-function getconfig(name)
+function get_children(host)
+ return disco_items:get(host) or NULL;
end
+return _M;
diff --git a/core/loggingmanager.lua b/core/loggingmanager.lua
index d701511e..c69dede8 100644
--- a/core/loggingmanager.lua
+++ b/core/loggingmanager.lua
@@ -1,76 +1,57 @@
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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 getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring;
+local os_date = os.date;
+local getstyle, setstyle = require "util.termcolours".getstyle, require "util.termcolours".setstyle;
+
+if os.getenv("__FLUSH_LOG") then
+ local io_flush = io.flush;
+ local _io_write = io_write;
+ io_write = function(...) _io_write(...); io_flush(); end
+end
local config = require "core.configmanager";
-local eventmanager = require "core.eventmanager";
local logger = require "util.logger";
-local debug_mode = config.get("*", "core", "debug");
+local prosody = prosody;
_G.log = logger.init("general");
module "loggingmanager"
--- The log config used if none specified in the config file
-local default_logging = { { to = "console" } };
-local default_file_logging = { { to = "file", levels = { min = (debug_mode and "debug") or "info" }, timestamps = true } };
-local default_timestamp = "%b %d %T";
+-- The log config used if none specified in the config file (see reload_logging for initialization)
+local default_logging;
+local default_file_logging;
+local default_timestamp = "%b %d %H:%M:%S";
-- The actual config loggingmanager is using
-local logging_config = config.get("*", "core", "log") or default_logging;
+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
@@ -82,13 +63,35 @@ end
-- the log_sink_types table.
function apply_sink_rules(sink_type)
if type(logging_config) == "table" then
- for _, sink_config in pairs(logging_config) do
- if sink_config.to == sink_type then
+
+ for _, level in ipairs(logging_levels) do
+ if type(logging_config[level]) == "string" then
+ local value = logging_config[level];
+ if sink_type == "file" and not value:match("^%*") then
+ add_rule({
+ to = sink_type;
+ filename = value;
+ timestamps = true;
+ levels = { min = level };
+ });
+ elseif value == "*"..sink_type then
+ add_rule({
+ to = sink_type;
+ levels = { min = level };
+ });
+ end
+ end
+ end
+
+ for _, sink_config in ipairs(logging_config) do
+ if (type(sink_config) == "table" and sink_config.to == sink_type) then
add_rule(sink_config);
+ elseif (type(sink_config) == "string" and sink_config:match("^%*(.+)") == sink_type) then
+ add_rule({ levels = { min = "debug" }, to = sink_type });
end
end
elseif type(logging_config) == "string" and (not logging_config:match("^%*")) and sink_type == "file" then
- -- User specified simply a filename, and the "file" sink type
+ -- User specified simply a filename, and the "file" sink type
-- was just added
for _, sink_config in pairs(default_file_logging) do
sink_config.filename = logging_config;
@@ -122,7 +125,7 @@ function get_levels(criteria, set)
return set;
elseif in_range then
set[level] = true;
- end
+ end
end
end
@@ -132,6 +135,38 @@ function get_levels(criteria, set)
return set;
end
+-- Initialize config, etc. --
+function reload_logging()
+ local old_sink_types = {};
+
+ for name, sink_maker in pairs(log_sink_types) do
+ old_sink_types[name] = sink_maker;
+ log_sink_types[name] = nil;
+ end
+
+ logger.reset();
+
+ local debug_mode = config.get("*", "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("*", "log") or default_logging;
+
+
+ for name, sink_maker in pairs(old_sink_types) do
+ log_sink_types[name] = sink_maker;
+ end
+
+ prosody.events.fire_event("logging-reloaded");
+end
+
+reload_logging();
+prosody.events.add_handler("config-reloaded", reload_logging);
+
--- Definition of built-in logging sinks ---
-- Null sink, must enter log_sink_types *first*
@@ -142,7 +177,7 @@ 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
@@ -155,16 +190,16 @@ function log_sink_types.stdout()
if timestamps then
io_write(os_date(timestamps), " ");
end
- if ... then
+ if ... then
io_write(name, rep(" ", sourcewidth-namelen), level, "\t", format(message, ...), "\n");
else
io_write(name, rep(" ", sourcewidth-namelen), level, "\t", message, "\n");
end
- end
+ end
end
do
- local do_pretty_printing = not os_getenv("WINDIR");
+ local do_pretty_printing = true;
local logstyles = {};
if do_pretty_printing then
@@ -187,13 +222,18 @@ do
return function (name, level, message, ...)
sourcewidth = math_max(#name+2, sourcewidth);
local namelen = #name;
+
if timestamps then
io_write(os_date(timestamps), " ");
end
- if ... then
- io_write(name, rep(" ", sourcewidth-namelen), getstring(logstyles[level], level), "\t", format(message, ...), "\n");
+ io_write(name, rep(" ", sourcewidth-namelen));
+ setstyle(logstyles[level]);
+ io_write(level);
+ setstyle();
+ if ... then
+ io_write("\t", format(message, ...), "\n");
else
- io_write(name, rep(" ", sourcewidth-namelen), getstring(logstyles[level], level), "\t", message, "\n");
+ io_write("\t", message, "\n");
end
end
end
@@ -208,18 +248,6 @@ function log_sink_types.file(config)
end
local write, flush = logfile.write, logfile.flush;
- eventmanager.add_event_hook("reopen-log-files", function ()
- if logfile then
- logfile:close();
- end
- logfile = io_open(log, "a+");
- if not logfile then
- write, flush = empty_function, empty_function;
- else
- write, flush = logfile.write, logfile.flush;
- end
- end);
-
local timestamps = config.timestamps;
if timestamps == nil or timestamps == true then
@@ -230,7 +258,7 @@ function log_sink_types.file(config)
if timestamps then
write(logfile, os_date(timestamps), " ");
end
- if ... then
+ if ... then
write(logfile, name, "\t", level, "\t", format(message, ...), "\n");
else
write(logfile, name, "\t" , level, "\t", message, "\n");
diff --git a/core/moduleapi.lua b/core/moduleapi.lua
new file mode 100644
index 00000000..ed75669b
--- /dev/null
+++ b/core/moduleapi.lua
@@ -0,0 +1,367 @@
+-- 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 hosts[self.host].type or nil;
+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: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 c2e6e68e..535c227b 100644
--- a/core/modulemanager.lua
+++ b/core/modulemanager.lua
@@ -1,198 +1,207 @@
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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 addDiscoInfoHandler = require "core.discomanager".addDiscoInfoHandler;
-local eventmanager = require "core.eventmanager";
local config = require "core.configmanager";
-local multitable_new = require "util.multitable".new;
-local register_actions = require "core.actions".register;
-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 loadfile, pcall = loadfile, pcall;
-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 = tostring;
+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;
+pcall = function(f, ...)
+ local n = select("#", ...);
+ local params = {...};
+ return xpcall(function() return f(unpack(params, 1, n)) end, function(e) return tostring(e).."\n"..debug_traceback(); end);
+end
-local autoload_modules = {"presence", "message", "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 stanza_handlers = multitable_new();
-local handler_info = {};
-
-local modulehelpers = setmetatable({}, { __index = _G });
-
-local features_table = multitable_new();
-local identities_table = multitable_new();
-local handler_table = multitable_new();
-local hooked = multitable_new();
-local hooks = multitable_new();
-local event_hooks = multitable_new();
-
-local NULL = {};
-
-- Load modules when a host is activated
function load_modules_for_host(host)
- if config.get(host, "core", "load_global_modules") ~= false then
- -- Load modules from global section
- local modules_enabled = config.get("*", "core", "modules_enabled");
- local modules_disabled = config.get(host, "core", "modules_disabled");
- local disabled_set = {};
- if modules_enabled then
- if modules_disabled then
- for _, module in ipairs(modules_disabled) do
- disabled_set[module] = true;
- end
- end
- for _, module in ipairs(autoload_modules) do
- if not disabled_set[module] then
- load(host, module);
- end
- end
- for _, module in ipairs(modules_enabled) do
- if not disabled_set[module] and not is_loaded(host, module) then
- load(host, module);
- end
- end
+ local component = config.get(host, "component_module");
+
+ local global_modules_enabled = config.get("*", "modules_enabled");
+ local global_modules_disabled = config.get("*", "modules_disabled");
+ local host_modules_enabled = config.get(host, "modules_enabled");
+ local host_modules_disabled = config.get(host, "modules_disabled");
+
+ if host_modules_enabled == global_modules_enabled then host_modules_enabled = nil; end
+ if host_modules_disabled == global_modules_disabled then host_modules_disabled = nil; end
+
+ local global_modules = set.new(autoload_modules) + set.new(global_modules_enabled) - set.new(global_modules_disabled);
+ if component then
+ global_modules = set.intersection(set.new(component_inheritable_modules), global_modules);
+ end
+ local modules = (global_modules + set.new(host_modules_enabled)) - set.new(host_modules_disabled);
+
+ -- 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
+ for module in modules do
+ load(host, module);
+ end
+end
+prosody.events.add_handler("host-activated", load_modules_for_host);
+prosody.events.add_handler("host-deactivated", function (host)
+ modulemap[host] = nil;
+end);
+
+--- Private helpers ---
+
+local function do_unload_module(host, name)
+ local mod = get_module(host, name);
+ if not mod then return nil, "module-not-loaded"; end
+
+ if module_has_method(mod, "unload") then
+ local ok, err = call_module_method(mod, "unload");
+ if (not ok) and err then
+ log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
end
end
- -- Load modules from just this host
- local modules_enabled = config.get(host, "core", "modules_enabled");
- if modules_enabled and modules_enabled ~= config.get("*", "core", "modules_enabled") then
- for _, module in pairs(modules_enabled) do
- if not is_loaded(host, module) then
- load(host, module);
+ 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
-eventmanager.add_event_hook("host-activated", load_modules_for_host);
---
-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] 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);
- 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 });
+ 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;
- setfenv(mod, pluginenv);
- if not hosts[host] then hosts[host] = { type = "component", host = host, connected = false, s2sout = {} }; end
-
- local success, ret = pcall(mod);
- if not success then
- log("error", "Error initialising module '%s': %s", module_name or "nil", ret or "nil");
- return nil, ret;
- end
-
- if module_has_method(pluginenv, "load") then
- local ok, err = call_module_method(pluginenv, "load");
- if (not ok) and err then
- log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err);
- end
- end
-
- -- Use modified host, if the module set one
- modulemap[api_instance.host][module_name] = pluginenv;
-
- if api_instance.host == "*" and host ~= "*" then
- api_instance:set_global();
+ 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
-
- return true;
-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
+ api_instance.path = err;
-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);
+ modulemap[host][module_name] = pluginenv;
+ local ok, err = pcall(mod);
+ if ok then
+ -- Call module's "load"
+ if module_has_method(pluginenv, "load") then
+ ok, err = call_module_method(pluginenv, "load");
+ if not ok then
+ log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err or "nil");
+ end
end
- end
- modulemap[host][name] = nil;
- features_table:remove(host, name);
- identities_table:remove(host, name);
- local params = handler_table:get(host, name); -- , {module.host, origin_type, tag, xmlns}
- for _, param in pairs(params or NULL) do
- local handlers = stanza_handlers:get(param[1], param[2], param[3], param[4]);
- if handlers then
- handler_info[handlers[1]] = nil;
- stanza_handlers:remove(param[1], param[2], param[3], param[4]);
+ api_instance.reloading, api_instance.saved_state = nil, nil;
+
+ if api_instance.host == "*" then
+ if not api_instance.global then -- COMPAT w/pre-0.9
+ if host ~= "*" then
+ log("warn", "mod_%s: Setting module.host = '*' deprecated, call module:set_global() instead", module_name);
+ end
+ api_instance:set_global();
+ end
+ modulemap[host][module_name] = nil;
+ modulemap[api_instance.host][module_name] = pluginenv;
+ if host ~= api_instance.host and module_has_method(pluginenv, "add_host") then
+ -- Now load the module again onto the host it was originally being loaded on
+ ok, err = do_load_module(host, module_name);
+ end
end
end
- event_hooks:remove(host, name);
- -- unhook event handlers hooked by module:hook
- for event, handlers in pairs(hooks:get(host, name) or NULL) do
- for handler in pairs(handlers or NULL) do
- (hosts[host] or prosody).events.remove_handler(event, handler);
- end
+ if not ok then
+ modulemap[api_instance.host][module_name] = nil;
+ log("error", "Error initializing module '%s' on '%s': %s", module_name, host, err or "nil");
end
- hooks:remove(host, name);
- return true;
+ 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
@@ -203,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
@@ -219,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
@@ -229,212 +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 handle_stanza(host, origin, stanza)
- local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns, origin.type;
- if name == "iq" and xmlns == "jabber:client" then
- if stanza.attr.type == "get" or stanza.attr.type == "set" then
- xmlns = stanza.tags[1].attr.xmlns or "jabber:client";
- log("debug", "Stanza of type %s from %s has xmlns: %s", name, origin_type, xmlns);
- else
- log("debug", "Discarding %s from %s of type: %s", name, origin_type, stanza.attr.type);
- return true;
- end
- end
- local handlers = stanza_handlers:get(host, origin_type, name, xmlns);
- if not handlers then handlers = stanza_handlers:get("*", origin_type, name, xmlns); end
- if handlers then
- log("debug", "Passing stanza to mod_%s", handler_info[handlers[1]].name);
- (handlers[1])(origin, stanza);
- return true;
- else
- log("debug", "Unhandled %s stanza: %s; xmlns=%s", origin.type, stanza.name, xmlns); -- we didn't handle it
- if stanza.attr.xmlns == "jabber:client" then
- 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
- origin:close("unsupported-stanza-type");
- end
- end
-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
-
-local function _add_handler(module, origin_type, tag, xmlns, handler)
- local handlers = stanza_handlers:get(module.host, origin_type, tag, xmlns);
- local msg = (tag == "iq") and "namespace" or "payload namespace";
- if not handlers then
- stanza_handlers:add(module.host, origin_type, tag, xmlns, handler);
- handler_info[handler] = module;
- handler_table:add(module.host, module.name, {module.host, origin_type, tag, xmlns});
- --module:log("debug", "I now handle tag '%s' [%s] with %s '%s'", tag, origin_type, msg, xmlns);
- else
- module:log("warn", "I wanted to handle tag '%s' [%s] with %s '%s' but mod_%s already handles that", tag, origin_type, msg, xmlns, handler_info[handlers[1]].module.name);
+-- Unload a module and fire module-unloaded
+function unload(host, name)
+ local ok, err = do_unload_module(host, name);
+ if ok then
+ (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host });
end
+ return ok, err;
end
-function api:add_handler(origin_type, tag, xmlns, handler)
- if not (origin_type and tag and xmlns and handler) then return false; end
- if type(origin_type) == "table" then
- for _, origin_type in ipairs(origin_type) do
- _add_handler(self, origin_type, tag, xmlns, handler);
- end
- else
- _add_handler(self, origin_type, tag, xmlns, handler);
+function reload(host, name)
+ local mod, err = do_reload_module(host, name);
+ if mod then
+ modulemap[host][name].module.reloading = true;
+ (hosts[host] or prosody).events.fire_event("module-reloaded", { module = name, host = host });
+ mod.module.reloading = nil;
+ elseif not is_loaded(host, name) then
+ (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host });
end
+ return mod, err;
end
-function api:add_iq_handler(origin_type, xmlns, handler)
- self:add_handler(origin_type, "iq", xmlns, handler);
+
+function get_module(host, name)
+ return modulemap[host] and modulemap[host][name];
end
-addDiscoInfoHandler("*host", function(reply, to, from, node)
- if #node == 0 then
- local done = {};
- for module, identities in pairs(identities_table:get(to) or NULL) do -- for each module
- for identity, attr in pairs(identities) do
- if not done[identity] then
- reply:tag("identity", attr):up(); -- TODO cache
- done[identity] = true;
- end
- end
- end
- for module, identities in pairs(identities_table:get("*") or NULL) do -- for each module
- for identity, attr in pairs(identities) do
- if not done[identity] then
- reply:tag("identity", attr):up(); -- TODO cache
- done[identity] = true;
- end
- end
- end
- for module, features in pairs(features_table:get(to) or NULL) do -- for each module
- for feature in pairs(features) do
- if not done[feature] then
- reply:tag("feature", {var = feature}):up(); -- TODO cache
- done[feature] = true;
- end
- end
- end
- for module, features in pairs(features_table:get("*") or NULL) do -- for each module
- for feature in pairs(features) do
- if not done[feature] then
- reply:tag("feature", {var = feature}):up(); -- TODO cache
- done[feature] = true;
- end
+function get_items(key, host)
+ local result = {};
+ local modules = modulemap[host];
+ if not key or not host or not modules then return nil; end
+
+ for _, module in pairs(modules) do
+ local mod = module.module;
+ if mod.items and mod.items[key] then
+ for _, value in ipairs(mod.items[key]) do
+ t_insert(result, value);
end
end
- return next(done) ~= nil;
end
-end);
-function api:add_feature(xmlns)
- features_table:set(self.host, self.name, xmlns, true);
-end
-function api:add_identity(category, type)
- identities_table:set(self.host, self.name, category.."\0"..type, {category = category, type = type});
+ return result;
end
-local event_hook = function(host, mod_name, event_name, ...)
- if type((...)) == "table" and (...).host and (...).host ~= host then return; end
- for handler in pairs(event_hooks:get(host, mod_name, event_name) or NULL) do
- handler(...);
- end
-end;
-function api:add_event_hook(name, handler)
- if not hooked:get(self.host, self.name, name) then
- eventmanager.add_event_hook(name, function(...) event_hook(self.host, self.name, name, ...); end);
- hooked:set(self.host, self.name, name, true);
- end
- event_hooks:set(self.host, self.name, name, handler, true);
+function get_modules(host)
+ return modulemap[host];
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);
+function is_loaded(host, name)
+ return modulemap[host] and modulemap[host][name] and true;
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;
- end
- return api.hook(self, "stanza/"..(xmlns and (xmlns..":") or "")..name, function (data) return handler(data.origin, data.stanza, data); end, priority);
+function module_has_method(module, method)
+ return type(rawget(module.module, method)) == "function";
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 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
- if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message
- setfenv(f, setmetatable({ module = self }, { __index = _G }));
- return f();
end
-function api:get_option(name, default_value)
- return config.get(self.host, self.name, name) or config.get(self.host, "core", name) or default_value;
-end
-
---------------------------------------------------------------------
-
-local actions = {};
-
-function actions.load(params)
- --return true, "Module loaded ("..params.module.." on "..params.host..")";
- return load(params.host, params.module);
-end
-
-function actions.unload(params)
- return unload(params.host, params.module);
-end
-
-register_actions("/modules", actions);
-
return _M;
diff --git a/core/objectmanager.lua b/core/objectmanager.lua
deleted file mode 100644
index e96cbd90..00000000
--- a/core/objectmanager.lua
+++ /dev/null
@@ -1,68 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-local new_multitable = require "util.multitable".new;
-local t_insert = table.insert;
-local t_concat = table.concat;
-local tostring = tostring;
-local unpack = unpack;
-local pairs = pairs;
-local error = error;
-local type = type;
-local _G = _G;
-
-local data = new_multitable();
-
-module "objectmanager"
-
-function set(...)
- return data:set(...);
-end
-function remove(...)
- return data:remove(...);
-end
-function get(...)
- return data:get(...);
-end
-
-local function get_path(path)
- if type(path) == "table" then return path; end
- local s = {};
- for part in tostring(path):gmatch("[%w_]+") do
- t_insert(s, part);
- end
- return s;
-end
-
-function get_object(path)
- path = get_path(path)
- return data:get(unpack(path)), path;
-end
-function set_object(path, object)
- path = get_path(path);
- data:set(unpack(path), object);
-end
-
-data:set("ls", function(_dir)
- local obj, dir = get_object(_dir);
- if not obj then error("object not found: " .. t_concat(dir, '/')); end
- local r = {};
- if type(obj) == "table" then
- for key, val in pairs(obj) do
- r[key] = type(val);
- end
- end
- return r;
-end);
-data:set("get", get_object);
-data:set("set", set_object);
-data:set("echo", function(...) return {...}; end);
-data:set("_G", _G);
-
-return _M;
diff --git a/core/offlinemanager.lua b/core/offlinemanager.lua
deleted file mode 100644
index 37e93777..00000000
--- a/core/offlinemanager.lua
+++ /dev/null
@@ -1,41 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-
-local datamanager = require "util.datamanager";
-local st = require "util.stanza";
-local datetime = require "util.datetime";
-local ipairs = ipairs;
-
-module "offlinemanager"
-
-function store(node, host, stanza)
- stanza.attr.stamp = datetime.datetime();
- stanza.attr.stamp_legacy = datetime.legacy();
- return datamanager.list_append(node, host, "offline", st.preserialize(stanza));
-end
-
-function load(node, host)
- local data = datamanager.list_load(node, host, "offline");
- if not data then return; end
- for k, v in ipairs(data) do
- local stanza = st.deserialize(v);
- stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = stanza.attr.stamp}):up(); -- XEP-0203
- stanza:tag("x", {xmlns = "jabber:x:delay", from = host, stamp = stanza.attr.stamp_legacy}):up(); -- XEP-0091 (deprecated)
- stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
- data[k] = stanza;
- end
- return data;
-end
-
-function deleteAll(node, host)
- return datamanager.list_store(node, host, "offline", nil);
-end
-
-return _M;
diff --git a/core/portmanager.lua b/core/portmanager.lua
new file mode 100644
index 00000000..7a247452
--- /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 0163e343..5e06e3f7 100644
--- a/core/rostermanager.lua
+++ b/core/rostermanager.lua
@@ -1,6 +1,6 @@
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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,15 +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"
@@ -81,33 +80,56 @@ function roster_push(username, host, jid)
end
function load_roster(username, host)
- log("debug", "load_roster: asked for: "..username.."@"..host);
+ local jid = username.."@"..host;
+ log("debug", "load_roster: asked for: %s", jid);
+ local user = bare_sessions[jid];
local roster;
- if hosts[host] and hosts[host].sessions[username] then
- roster = hosts[host].sessions[username].roster;
- if not roster then
- log("debug", "load_roster: loading for new user: "..username.."@"..host);
- roster = datamanager.load(username, host, "roster") or {};
- if not roster[false] then roster[false] = { }; end
- hosts[host].sessions[username].roster = roster;
- hosts[host].events.fire_event("roster-load", username, host, roster);
- end
- return roster;
+ if user then
+ roster = user.roster;
+ if roster then return roster; end
+ 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: %s@%s", username, host);
+ end
+ local data, err = datamanager.load(username, host, "roster");
+ roster = data or {};
+ if user then user.roster = roster; end
+ if not roster[false] then roster[false] = { broken = err or nil }; end
+ if roster[jid] then
+ roster[jid] = nil;
+ log("warn", "roster for %s has a self-contact", jid);
end
-
- -- Attempt to load roster for non-loaded user
- log("debug", "load_roster: loading for offline user: "..username.."@"..host);
- roster = datamanager.load(username, host, "roster") or {};
- hosts[host].events.fire_event("roster-load", username, host, roster);
- return roster;
+ if not err then
+ hosts[host].events.fire_event("roster-load", username, host, roster);
+ end
+ return roster, err;
end
-function save_roster(username, host)
- log("debug", "save_roster: saving roster for "..username.."@"..host);
- if hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster then
- local roster = hosts[host].sessions[username].roster;
- roster[false].version = (roster[false].version or 1) + 1;
- return datamanager.store(username, host, "roster", hosts[host].sessions[username].roster);
+function save_roster(username, host, roster)
+ 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
+ -- --roster = load_roster(username, host);
+ -- return true; -- roster unchanged, no reason to save
+ --end
+ end
+ if roster then
+ local metadata = roster[false];
+ if not metadata then
+ metadata = {};
+ roster[false] = metadata;
+ end
+ if metadata.version ~= true then
+ metadata.version = (metadata.version or 0) + 1;
+ end
+ if roster[false].broken then return nil, "Not saving broken roster" end
+ return datamanager.store(username, host, "roster", roster);
end
log("warn", "save_roster: user had no roster to save");
return nil;
@@ -123,7 +145,7 @@ function process_inbound_subscription_approval(username, host, jid)
item.subscription = "both";
end
item.ask = nil;
- return datamanager.store(username, host, "roster", roster);
+ return save_roster(username, host, roster);
end
end
@@ -145,7 +167,7 @@ function process_inbound_subscription_cancellation(username, host, jid)
end
end
if changed then
- return datamanager.store(username, host, "roster", roster);
+ return save_roster(username, host, roster);
end
end
@@ -167,14 +189,26 @@ function process_inbound_unsubscribe(username, host, jid)
end
end
if changed then
- return datamanager.store(username, host, "roster", roster);
+ return save_roster(username, host, roster);
end
end
+local function _get_online_roster_subscription(jidA, jidB)
+ local user = bare_sessions[jidA];
+ local item = user and (user.roster[jidB] or { subscription = "none" });
+ return item and item.subscription;
+end
function is_contact_subscribed(username, host, jid)
- local roster = load_roster(username, host);
+ do
+ local selfjid = username.."@"..host;
+ local subscription = _get_online_roster_subscription(selfjid, jid);
+ if subscription then return (subscription == "both" or subscription == "from"); end
+ local subscription = _get_online_roster_subscription(jid, selfjid);
+ if subscription then return (subscription == "both" or subscription == "to"); end
+ end
+ local roster, err = load_roster(username, host);
local item = roster[jid];
- return item and (item.subscription == "from" or item.subscription == "both");
+ return item and (item.subscription == "from" or item.subscription == "both"), err;
end
function is_contact_pending_in(username, host, jid)
@@ -189,7 +223,7 @@ function set_contact_pending_in(username, host, jid, pending)
end
if not roster.pending then roster.pending = {}; end
roster.pending[jid] = true;
- return datamanager.store(username, host, "roster", roster);
+ return save_roster(username, host, roster);
end
function is_contact_pending_out(username, host, jid)
local roster = load_roster(username, host);
@@ -207,8 +241,8 @@ 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");
- return datamanager.store(username, host, "roster", roster);
+ 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)
local roster = load_roster(username, host);
@@ -223,7 +257,7 @@ function unsubscribe(username, host, jid)
elseif item.subscription == "to" then
item.subscription = "none";
end
- return datamanager.store(username, host, "roster", roster);
+ return save_roster(username, host, roster);
end
function subscribed(username, host, jid)
if is_contact_pending_in(username, host, jid) then
@@ -240,30 +274,28 @@ function subscribed(username, host, jid)
end
roster.pending[jid] = nil;
-- TODO maybe remove roster.pending if empty
- return datamanager.store(username, host, "roster", roster);
+ return save_roster(username, host, roster);
end -- TODO else implement optional feature pre-approval (ask = subscribed)
end
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 datamanager.store(username, host, "roster", 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)
@@ -271,7 +303,7 @@ function process_outbound_subscription_request(username, host, jid)
local item = roster[jid];
if item and (item.subscription == "none" or item.subscription == "from") then
item.ask = "subscribe";
- return datamanager.store(username, host, "roster", roster);
+ return save_roster(username, host, roster);
end
end
@@ -280,7 +312,7 @@ end
local item = roster[jid];
if item and (item.subscription == "none" or item.subscription == "from" then
item.ask = "subscribe";
- return datamanager.store(username, host, "roster", roster);
+ return save_roster(username, host, roster);
end
end]]
diff --git a/core/s2smanager.lua b/core/s2smanager.lua
index 0589e024..06d3f2c9 100644
--- a/core/s2smanager.lua
+++ b/core/s2smanager.lua
@@ -1,6 +1,6 @@
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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.
@@ -8,404 +8,91 @@
-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
- = tostring, pairs, ipairs, getmetatable, newproxy, error, tonumber;
-
-local idna_to_ascii = require "util.encodings".idna.to_ascii;
-local connlisteners_get = require "net.connlisteners".get;
-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 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 dialback_secret = uuid_gen();
-
-local adns = require "net.adns";
-
-local dns_timeout = config.get("*", "core", "dns_timeout") or 60;
-
+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"
-local function compare_srv_priorities(a,b) return a.priority < b.priority or a.weight < b.weight; end
-
-local function bounce_sendq(session)
- 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 or xmlns == "jabber:client" or xmlns == "jabber:server" then
- reply.attr.type = "error";
- reply:tag("error", {type = "cancel"})
- :tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
- core_process_stanza(dummy, reply);
- end
- sendq[i] = nil;
- end
- session.sendq = nil;
- end
-end
-
-function send_to_host(from_host, to_host, data)
- 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" and ((not data.xmlns) or data.xmlns == "jabber:client" or data.xmlns == "jabber:server") then
- (host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host);
- if not host.notopen and not host.dialback_key and host.sends2s then
- host.log("debug", "dialback had not been initiated");
- initiate_dialback(host);
- end
-
- -- 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));
- 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);
- destroy_session(host_session);
- end
- end
-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.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(tostring(t)); end
+ session.log = logger_init("s2sin"..tostring(session):match("[a-f0-9]+$"));
incoming_s2s[session] = true;
return session;
end
function new_outgoing(from_host, to_host)
- local host_session = { to_host = to_host, from_host = from_host, notopen = true, type = "s2sout_unauthed", direction = "outgoing" };
- hosts[from_host].s2sout[to_host] = host_session;
-
- local log;
- do
- local conn_name = "s2sout"..tostring(host_session):match("[a-f0-9]*$");
- log = logger_init(conn_name);
- host_session.log = log;
- end
-
- -- This is the first call, can't fail (the first step is DNS lookup)
- attempt_connection(host_session);
-
- 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;
+ 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
-
-function attempt_connection(host_session, err)
- local from_host, to_host = host_session.from_host, host_session.to_host;
- local connect_host, connect_port = idna_to_ascii(to_host), 5269;
-
- 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 answer, 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);
- end
- end
- end, "_xmpp-server._tcp."..connect_host..".", "SRV");
-
- -- Set handler for DNS timeout
- add_task(dns_timeout, function ()
- if handle then
- adns.cancel(handle, true);
- end
- end);
-
- log("debug", "DNS lookup for %s sent, waiting for response before we can connect", to_host);
- 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.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()
-
- 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, hosts[from_host].ssl_ctx, false );
- host_session.conn = conn;
-
- -- Register this outgoing connection so that xmppserver_listener knows about it
- -- otherwise it will assume it is a new incoming connection
- cl.register_outgoing(conn, host_session);
-
- local w = conn.write;
- host_session.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(tostring(t)); end
-
- conn.write(format([[<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' from='%s' to='%s' version='1.0' xml:lang='en'>]], from_host, to_host));
- log("debug", "Connection attempt in progress...");
- return true;
-end
-
-function streamopened(session, attr)
- local send = session.sends2s;
-
- -- TODO: #29: SASL/TLS on s2s streams
- session.version = 0; --tonumber(attr.version) or 0;
-
- if session.version >= 1.0 and not (attr.to and attr.from) then
- log("warn", (session.to_host or "(unknown)").." failed to specify 'to' or 'from' hostname as per RFC");
- 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>");
- 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 }):top_tag());
- if session.to_host and not hosts[session.to_host] then
- -- Attempting to connect to a host we don't serve
- session:close({ condition = "host-unknown"; text = "This host does not serve "..session.to_host });
- return;
- end
- if session.version >= 1.0 then
- send(st.stanza("stream:features")
- :tag("dialback", { xmlns='urn:xmpp:features:dialback' }):tag("optional"):up():up());
- 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;
-
- -- 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 not session.dialback_verifying then
- initiate_dialback(session);
- else
- mark_connected(session);
+local resting_session = { -- Resting, not dead
+ destroyed = true;
+ type = "s2s_destroyed";
+ open_stream = function (session)
+ session.log("debug", "Attempt to open stream on resting session");
+ end;
+ close = function (session)
+ session.log("debug", "Attempt to close already-closed session");
+ end;
+ filter = function (type, data) return data; end;
+ }; resting_session.__index = resting_session;
+
+function retire_session(session, reason)
+ local log = session.log or log;
+ for k in pairs(session) do
+ if k ~= "log" and k ~= "id" and k ~= "conn" then
+ session[k] = nil;
end
end
- session.notopen = nil;
-end
-
-function streamclosed(session)
- (session.log or log)("debug", "</stream:stream>");
- if session.sends2s then
- session.sends2s("</stream:stream>");
- end
- session.notopen = true;
-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..dialback_secret, true);
-end
+ session.destruction_reason = reason;
-function verify_dialback(id, to, from, key)
- return key == generate_dialback(id, to, from);
+ function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); end
+ function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end
+ return setmetatable(session, resting_session);
end
-function make_authenticated(session, host)
- if session.type == "s2sout_unauthed" then
- session.type = "s2sout";
- elseif session.type == "s2sin_unauthed" then
- session.type = "s2sin";
- if host then
- session.hosts[host].authed = true;
- end
- elseif session.type == "s2sin" and host then
- 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
-
-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) send_to_host(to, from, 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
-end
-
-function destroy_session(session)
- (session.log or log)("info", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host));
+function destroy_session(session, reason)
+ if session.destroyed then return; end
+ (session.log or log)("debug", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host)..(reason and (": "..reason) or ""));
if session.direction == "outgoing" then
hosts[session.from_host].s2sout[session.to_host] = nil;
- bounce_sendq(session);
+ session:bounce_sendq(reason);
elseif session.direction == "incoming" then
incoming_s2s[session] = nil;
end
- for k in pairs(session) do
- if k ~= "trace" then
- session[k] = nil;
+ local event_data = { session = session, reason = reason };
+ if session.type == "s2sout" then
+ fire_event("s2sout-destroyed", event_data);
+ if hosts[session.from_host] then
+ hosts[session.from_host].events.fire_event("s2sout-destroyed", event_data);
+ end
+ elseif session.type == "s2sin" then
+ fire_event("s2sin-destroyed", event_data);
+ if hosts[session.to_host] then
+ hosts[session.to_host].events.fire_event("s2sin-destroyed", event_data);
end
end
+
+ retire_session(session, reason); -- Clean session until it is GC'd
+ return true;
end
return _M;
diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua
index 1b1b36df..98ead07f 100644
--- a/core/sessionmanager.lua
+++ b/core/sessionmanager.lua
@@ -1,83 +1,106 @@
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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 = tonumber, tostring;
-local ipairs, pairs, print, next= ipairs, pairs, print, next;
-local collectgarbage = collectgarbage;
-local m_random = import("math", "random");
-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 log = require "util.logger".init("sessionmanager");
-local error = error;
-local uuid_generate = require "util.uuid".generate;
+local logger = require "util.logger";
+local log = logger.init("sessionmanager");
local rm_load_roster = require "core.rostermanager".load_roster;
local config_get = require "core.configmanager".get;
-local nameprep = require "util.encodings".stringprep.nameprep;
-
-local fire_event = require "core.eventmanager".fire_event;
+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 gettime = require "socket".gettime;
-local st = require "util.stanza";
-
-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) w(tostring(t)); end
- session.ip = conn.ip();
+ session.send = function (t)
+ if t.name then
+ t = filter("stanzas/out", t);
+ end
+ if t then
+ t = filter("bytes/out", tostring(t));
+ if t then
+ return w(conn, t);
+ end
+ end
+ end
+ session.ip = conn:ip();
+ local conn_name = "c2s"..tostring(session):match("[a-f0-9]+$");
+ session.log = logger.init(conn_name);
+
return session;
end
+local resting_session = { -- Resting, not dead
+ destroyed = true;
+ type = "c2s_destroyed";
+ close = function (session)
+ session.log("debug", "Attempt to close already-closed session");
+ end;
+ filter = function (type, data) return data; end;
+ }; resting_session.__index = resting_session;
+
+function retire_session(session)
+ local log = session.log or log;
+ for k in pairs(session) do
+ if k ~= "log" and k ~= "id" then
+ session[k] = nil;
+ end
+ end
+
+ function session.send(data) log("debug", "Discarding data sent to resting session: %s", 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].events.fire_event("resource-unbind", {session=session, error=err});
-
- 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
+
+ host_session.events.fire_event("resource-unbind", {session=session, error=err});
end
- for k in pairs(session) do
- if k ~= "trace" then
- session[k] = nil;
- end
- end
+ retire_session(session);
end
function make_authenticated(session, username)
+ username = nodeprep(username);
+ if not username or #username == 0 then return nil, "Invalid username"; end
session.username = username;
if session.type == "c2s_unauthed" then
session.type = "c2s";
@@ -93,7 +116,8 @@ function bind_resource(session, resource)
if session.resource then return nil, "cancel", "already-bound", "Cannot bind multiple resources on a single connection"; end
-- We don't support binding multiple resources
- resource = resource or uuid_generate();
+ 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
@@ -102,13 +126,9 @@ function bind_resource(session, resource)
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", "conflict", "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();
@@ -142,67 +162,53 @@ function bind_resource(session, resource)
hosts[session.host].sessions[session.username].sessions[resource] = session;
full_sessions[session.full_jid] = session;
- session.roster = rm_load_roster(session.username, session.host);
+ local err;
+ session.roster, err = rm_load_roster(session.username, session.host);
+ if err then
+ full_sessions[session.full_jid] = nil;
+ hosts[session.host].sessions[session.username].sessions[resource] = nil;
+ session.full_jid = nil;
+ session.resource = nil;
+ if next(bare_sessions[session.username..'@'..session.host].sessions) == nil then
+ bare_sessions[session.username..'@'..session.host] = nil;
+ hosts[session.host].sessions[session.username] = nil;
+ end
+ session.log("error", "Roster loading failed: %s", err);
+ return nil, "cancel", "internal-server-error", "Error loading roster";
+ end
hosts[session.host].events.fire_event("resource-bind", {session=session});
return true;
end
-function streamopened(session, attr)
- local send = session.send;
- session.host = attr.to or error("Client failed to specify destination hostname");
- session.host = nameprep(session.host);
- session.version = tonumber(attr.version) or 0;
- session.streamid = m_random(1000000, 99999999);
- (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host);
-
- 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));
-
- 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
-
- -- 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;
+function send_to_available_resources(user, host, stanza)
+ local jid = user.."@"..host;
+ local count = 0;
+ local user = bare_sessions[jid];
+ if user then
+ for k, session in pairs(user.sessions) do
+ if session.presence then
+ session.send(stanza);
+ count = count + 1;
+ end
+ end
end
-
- local features = st.stanza("stream:features");
- fire_event("stream-features", session, features);
-
- send(features);
-
- (session.log or log)("debug", "Sent reply <stream:stream> to client");
- session.notopen = nil;
-end
-
-function streamclosed(session)
- session.send("</stream:stream>");
- session.notopen = true;
+ return count;
end
-function send_to_available_resources(user, host, stanza)
+function send_to_interested_resources(user, host, stanza)
+ local jid = user.."@"..host;
local count = 0;
- local to = stanza.attr.to;
- stanza.attr.to = nil;
- local h = hosts[host];
- if h and h.type == "local" then
- local u = h.sessions[user];
- if u then
- for k, session in pairs(u.sessions) do
- if session.presence then
- session.send(stanza);
- count = count + 1;
- end
+ local user = bare_sessions[jid];
+ if user then
+ for k, session in pairs(user.sessions) do
+ if session.interested then
+ session.send(stanza);
+ count = count + 1;
end
end
end
- stanza.attr.to = to;
return count;
end
diff --git a/core/stanza_router.lua b/core/stanza_router.lua
index dac098bb..94753678 100644
--- a/core/stanza_router.lua
+++ b/core/stanza_router.lua
@@ -1,6 +1,6 @@
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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.
@@ -8,36 +8,71 @@
local log = require "util.logger".init("stanzarouter")
-local hosts = _G.hosts;
+local hosts = _G.prosody.hosts;
local tostring = tostring;
local st = require "util.stanza";
-local send_s2s = require "core.s2smanager".send_to_host;
-local modules_handle_stanza = require "core.modulemanager".handle_stanza;
-local component_handle_stanza = require "core.componentmanager".handle_stanza;
local jid_split = require "util.jid".split;
local jid_prepped_split = require "util.jid".prepped_split;
+local full_sessions = _G.prosody.full_sessions;
+local bare_sessions = _G.prosody.bare_sessions;
+
+local 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
+ if stanza.attr.type == "get" or stanza.attr.type == "set" then
+ xmlns = stanza.tags[1].attr.xmlns or "jabber:client";
+ log("debug", "Stanza of type %s from %s has xmlns: %s", name, origin_type, xmlns);
+ else
+ log("debug", "Discarding %s from %s of type: %s", name, origin_type, stanza.attr.type);
+ return true;
+ end
+ end
+ if stanza.attr.xmlns == nil 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 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())
- -- Currently we guarantee every stanza to have an xmlns, should we keep this rule?
- if not stanza.attr.xmlns then stanza.attr.xmlns = "jabber:client"; end
-
-- TODO verify validity of stanza (as well as JID validity)
if stanza.attr.type == "error" and #stanza.tags == 0 then return; end -- TODO invalid stanza, log
if stanza.name == "iq" then
- 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 stanza.attr.id then stanza.attr.id = ""; end -- COMPAT Jabiru doesn't send the id attribute on roster requests
+ if not iq_types[stanza.attr.type] or ((stanza.attr.type == "set" or stanza.attr.type == "get") and (#stanza.tags ~= 1)) then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid IQ type or incorrect number of children"));
return;
end
end
- if origin.type == "c2s" then
+ if origin.type == "c2s" and not stanza.attr.xmlns then
if not origin.full_jid
and not(stanza.name == "iq" and stanza.attr.type == "set" and stanza.tags[1] and stanza.tags[1].name == "bind"
and stanza.tags[1].attr.xmlns == "urn:ietf:params:xml:ns:xmpp-bind") then
-- authenticated client isn't bound and current stanza is not a bind request
- origin.send(st.error_reply(stanza, "auth", "not-authorized")); -- FIXME maybe allow stanzas to account or server
+ if stanza.attr.type ~= "result" and stanza.attr.type ~= "error" then
+ origin.send(st.error_reply(stanza, "auth", "not-authorized")); -- FIXME maybe allow stanzas to account or server
+ end
return;
end
@@ -81,46 +116,46 @@ 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 == "jabber:client" then
+ 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);
+ core_post_stanza(origin, stanza, origin.full_jid);
else
local h = hosts[stanza.attr.to or origin.host or origin.to_host];
if h then
local event;
- if stanza.attr.xmlns == "jabber:client" then
+ if xmlns == nil then
if stanza.name == "iq" and (stanza.attr.type == "set" or stanza.attr.type == "get") then
event = "stanza/iq/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name;
else
event = "stanza/"..stanza.name;
end
else
- event = "stanza/"..stanza.attr.xmlns..":"..stanza.name;
+ event = "stanza/"..xmlns..":"..stanza.name;
end
if h.events.fire_event(event, {origin = origin, stanza = stanza}) then return; end
end
- if host and not hosts[host] then host = nil; end -- workaround for a Pidgin bug which sets 'to' to the SRV result
- modules_handle_stanza(host or origin.host or origin.to_host, origin, stanza);
+ if host and not hosts[host] then host = nil; end -- COMPAT: workaround for a Pidgin bug which sets 'to' to the SRV result
+ handle_unhandled_stanza(host or origin.host or origin.to_host, origin, stanza);
end
end
-function core_post_stanza(origin, stanza)
+function core_post_stanza(origin, stanza, preevents)
local to = stanza.attr.to;
local node, host, resource = jid_split(to);
local to_bare = node and (node.."@"..host) or host; -- bare JID
- local to_type;
+ local to_type, to_self;
if node then
if resource then
to_type = '/full';
@@ -128,6 +163,7 @@ function core_post_stanza(origin, stanza)
to_type = '/bare';
if node == origin.username and host == origin.host then
stanza.attr.to = nil;
+ to_self = true;
end
end
else
@@ -135,22 +171,19 @@ function core_post_stanza(origin, stanza)
to_type = '/host';
else
to_type = '/bare';
+ to_self = true;
end
end
local event_data = {origin=origin, stanza=stanza};
- if origin.full_jid == stanza.attr.from then -- c2s connection
+ if preevents then -- c2s connection
if hosts[origin.host].events.fire_event('pre-'..stanza.name..to_type, event_data) then return; end -- do preprocessing
end
local h = hosts[to_bare] or hosts[host or origin.host];
if h then
if h.events.fire_event(stanza.name..to_type, event_data) then return; end -- do processing
-
- if h.type == "component" then
- component_handle_stanza(origin, stanza);
- return;
- end
- modules_handle_stanza(h.host, origin, stanza);
+ if to_self and h.events.fire_event(stanza.name..'/self', event_data) then return; end -- do processing
+ handle_unhandled_stanza(h.host, origin, stanza);
else
core_route_stanza(origin, stanza);
end
@@ -167,26 +200,24 @@ function core_route_stanza(origin, stanza)
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));
- 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 stanza from unhandled connection type: %s", 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
new file mode 100644
index 00000000..1c82af6d
--- /dev/null
+++ b/core/storagemanager.lua
@@ -0,0 +1,135 @@
+
+local error, type, pairs = error, type, pairs;
+local setmetatable = setmetatable;
+
+local config = require "core.configmanager";
+local datamanager = require "util.datamanager";
+local modulemanager = require "core.modulemanager";
+local multitable = require "util.multitable";
+local hosts = hosts;
+local log = require "util.logger".init("storagemanager");
+
+local prosody = prosody;
+
+module("storagemanager")
+
+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/storage-provider", function (event)
+ local item = event.item;
+ stores_available:set(host, item.name, item);
+ end);
+
+ host_session.events.add_handler("item-removed/storage-provider", function (event)
+ local item = event.item;
+ stores_available:set(host, item.name, nil);
+ end);
+end
+prosody.events.add_handler("host-activated", initialize_host, 101);
+
+function load_driver(host, driver_name)
+ if driver_name == "null" then
+ return null_storage_driver;
+ end
+ local driver = stores_available:get(host, driver_name);
+ if driver then return driver; end
+ local ok, err = modulemanager.load(host, "storage_"..driver_name);
+ if not ok then
+ log("error", "Failed to load storage driver plugin %s on %s: %s", driver_name, host, err);
+ end
+ return stores_available:get(host, driver_name);
+end
+
+function get_driver(host, store)
+ local storage = config.get(host, "storage");
+ local driver_name;
+ local option_type = type(storage);
+ if option_type == "string" then
+ driver_name = storage;
+ elseif option_type == "table" then
+ driver_name = storage[store];
+ end
+ if not driver_name then
+ driver_name = config.get(host, "default_storage") or "internal";
+ end
+
+ local driver = load_driver(host, driver_name);
+ if not driver then
+ log("warn", "Falling back to null driver for %s storage on %s", store, host);
+ driver_name = "null";
+ driver = null_storage_driver;
+ end
+ return driver, driver_name;
+end
+
+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 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 6c36fa29..08343bee 100644
--- a/core/usermanager.lua
+++ b/core/usermanager.lua
@@ -1,80 +1,155 @@
-- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
+-- 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.
--
-
-
-require "util.datamanager"
-local datamanager = datamanager;
+local modulemanager = require "core.modulemanager";
local log = require "util.logger".init("usermanager");
local type = type;
-local error = error;
local ipairs = ipairs;
-local hashes = require "util.hashes";
+local pairs = pairs;
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;
+
+local setmetatable = setmetatable;
+
+local default_provider = "internal_plain";
module "usermanager"
-function validate_credentials(host, username, password, method)
- log("debug", "User '%s' is being validated", username);
- local credentials = datamanager.load(username, host, "accounts") or {};
+function new_null_provider()
+ local function dummy() return nil, "method not implemented"; end;
+ local function dummy_get_sasl_handler() return sasl_new(nil, {}); end
+ return setmetatable({name = "null", get_sasl_handler = dummy_get_sasl_handler}, {
+ __index = function(self, method) return dummy; end
+ });
+end
+
+local provider_mt = { __index = new_null_provider() };
- if method == nil then method = "PLAIN"; end
- if method == "PLAIN" and credentials.password then -- PLAIN, do directly
- if password == credentials.password then
- return true;
- else
- return nil, "Auth failed. Invalid username or password.";
+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, "authentication") or default_provider;
+ if config.get(host, "anonymous_login") then
+ log("error", "Deprecated config option 'anonymous_login'. Use authentication = 'anonymous' instead.");
+ auth_provider = "anonymous";
+ end -- COMPAT 0.7
+ if provider.name == auth_provider then
+ host_session.users = setmetatable(provider, provider_mt);
end
- end
- -- must do md5
- -- make credentials md5
- local pwd = credentials.password;
- if not pwd then pwd = credentials.md5; else pwd = hashes.md5(pwd, true); end
- -- make password md5
- if method == "PLAIN" then
- password = hashes.md5(password or "", true);
- elseif method ~= "DIGEST-MD5" then
- return nil, "Unsupported auth method";
- end
- -- compare
- if password == pwd then
- return true;
- else
- return nil, "Auth failed. Invalid username or password.";
+ if host_session.users ~= nil and host_session.users.name ~= nil then
+ log("debug", "host '%s' now set to use user provider '%s'", host, host_session.users.name);
+ end
+ end);
+ host_session.events.add_handler("item-removed/auth-provider", function (event)
+ local provider = event.item;
+ if host_session.users == provider then
+ host_session.users = new_null_provider();
+ end
+ end);
+ host_session.users = new_null_provider(); -- Start with the default usermanager provider
+ local auth_provider = config.get(host, "authentication") or default_provider;
+ if config.get(host, "anonymous_login") then auth_provider = "anonymous"; end -- COMPAT 0.7
+ if auth_provider ~= "null" then
+ modulemanager.load(host, "auth_"..auth_provider);
end
+end;
+prosody.events.add_handler("host-activated", initialize_host, 100);
+
+function test_password(username, host, password)
+ return hosts[host].users.test_password(username, password);
end
function get_password(username, host)
- return (datamanager.load(username, host, "accounts") or {}).password
+ return hosts[host].users.get_password(username);
+end
+
+function set_password(username, password, host)
+ return hosts[host].users.set_password(username, password);
end
function user_exists(username, host)
- return datamanager.load(username, host, "accounts") ~= nil; -- FIXME also check for empty credentials
+ return hosts[host].users.user_exists(username);
end
function create_user(username, password, host)
- return datamanager.store(username, host, "accounts", {password = password});
+ return hosts[host].users.create_user(username, password);
+end
+
+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_supported_methods(host)
- return {["PLAIN"] = true, ["DIGEST-MD5"] = true}; -- TODO this should be taken from the config
+function get_provider(host)
+ return hosts[host].users;
end
-function is_admin(jid)
- local admins = config.get("*", "core", "admins");
- if type(admins) == "table" then
- jid = jid_bare(jid);
- for _,admin in ipairs(admins) do
- if admin == jid then return true; 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, "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 jid_prep(admin) == jid then
+ is_admin = true;
+ break;
+ end
+ end
+ elseif host_admins then
+ log("error", "Option 'admins' for host '%s' is not a list", host);
+ end
+ end
+
+ if not is_admin and global_admins then
+ if type(global_admins) == "table" then
+ for _,admin in ipairs(global_admins) do
+ if jid_prep(admin) == jid then
+ is_admin = true;
+ break;
+ end
+ end
+ elseif global_admins then
+ log("error", "Global option 'admins' is not a list");
end
- else log("debug", "Option core.admins is not a table"); end
- return nil;
+ end
+
+ -- Still not an admin, check with auth provider
+ if not is_admin and host ~= "*" and hosts[host].users and hosts[host].users.is_admin then
+ is_admin = hosts[host].users.is_admin(jid);
+ end
+ return is_admin or false;
end
return _M;
diff --git a/core/xmlhandlers.lua b/core/xmlhandlers.lua
deleted file mode 100644
index 7f47cf70..00000000
--- a/core/xmlhandlers.lua
+++ /dev/null
@@ -1,140 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-
-require "util.stanza"
-
-local st = stanza;
-local tostring = tostring;
-local pairs = pairs;
-local ipairs = ipairs;
-local t_insert = table.insert;
-local t_concat = table.concat;
-
-local default_log = require "util.logger".init("xmlhandlers");
-
-local error = error;
-
-module "xmlhandlers"
-
-local ns_prefixes = {
- ["http://www.w3.org/XML/1998/namespace"] = "xml";
- }
-
-function init_xmlhandlers(session, stream_callbacks)
- local ns_stack = { "" };
- local curr_ns, name = "";
- local curr_tag;
- local chardata = {};
- local xml_handlers = {};
- local log = session.log or default_log;
-
- local cb_streamopened = stream_callbacks.streamopened;
- local cb_streamclosed = stream_callbacks.streamclosed;
- local cb_error = stream_callbacks.error or function (session, e) error("XML stream error: "..tostring(e)); end;
- local cb_handlestanza = stream_callbacks.handlestanza;
-
- local stream_tag = stream_callbacks.stream_tag;
- local stream_default_ns = stream_callbacks.default_ns;
-
- local stanza
- function xml_handlers:StartElement(tagname, attr)
- if stanza and #chardata > 0 then
- -- We have some character data in the buffer
- stanza:text(t_concat(chardata));
- chardata = {};
- end
- local curr_ns,name = tagname:match("^(.-)|?([^%|]-)$");
- if not name then
- curr_ns, name = "", curr_ns;
- end
-
- if curr_ns ~= stream_default_ns then
- attr.xmlns = curr_ns;
- end
-
- -- FIXME !!!!!
- for i=1,#attr do
- local k = attr[i];
- attr[i] = nil;
- local ns, nm = k:match("^([^|]+)|?([^|]-)$")
- if ns and nm then
- ns = ns_prefixes[ns];
- if ns then
- attr[ns..":"..nm] = attr[k];
- attr[k] = nil;
- end
- end
- end
-
- if not stanza then --if we are not currently inside a stanza
- if session.notopen then
- if tagname == stream_tag then
- if cb_streamopened then
- cb_streamopened(session, attr);
- end
- else
- -- Garbage before stream?
- cb_error(session, "no-stream");
- end
- return;
- end
- if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then
- cb_error(session, "invalid-top-level-element");
- end
-
- stanza = st.stanza(name, attr);
- curr_tag = stanza;
- else -- we are inside a stanza, so add a tag
- attr.xmlns = nil;
- if curr_ns ~= stream_default_ns then
- attr.xmlns = curr_ns;
- end
- stanza:tag(name, attr);
- end
- end
- function xml_handlers:CharacterData(data)
- if stanza then
- t_insert(chardata, data);
- end
- end
- function xml_handlers:EndElement(tagname)
- curr_ns,name = tagname:match("^(.-)|?([^%|]-)$");
- if not name then
- curr_ns, name = "", curr_ns;
- end
- if (not stanza) or (#stanza.last_add > 0 and name ~= stanza.last_add[#stanza.last_add].name) then
- if tagname == stream_tag then
- if cb_streamclosed then
- cb_streamclosed(session);
- end
- return;
- elseif name == "error" then
- cb_error(session, "stream-error", stanza);
- else
- cb_error(session, "parse-error", "unexpected-element-close", name);
- end
- end
- if #chardata > 0 then
- -- We have some character data in the buffer
- stanza:text(t_concat(chardata));
- chardata = {};
- end
- -- Complete stanza
- if #stanza.last_add == 0 then
- cb_handlestanza(session, stanza);
- stanza = nil;
- else
- stanza:up();
- end
- end
- return xml_handlers;
-end
-
-return init_xmlhandlers;