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