diff options
Diffstat (limited to 'core')
-rw-r--r-- | core/certmanager.lua | 108 | ||||
-rw-r--r-- | core/configmanager.lua | 265 | ||||
-rw-r--r-- | core/hostmanager.lua | 158 | ||||
-rw-r--r-- | core/loggingmanager.lua | 276 | ||||
-rw-r--r-- | core/moduleapi.lua | 367 | ||||
-rw-r--r-- | core/modulemanager.lua | 370 | ||||
-rw-r--r-- | core/offlinemessage.lua | 13 | ||||
-rw-r--r-- | core/portmanager.lua | 239 | ||||
-rw-r--r-- | core/rostermanager.lua | 293 | ||||
-rw-r--r-- | core/s2smanager.lua | 216 | ||||
-rw-r--r-- | core/servermanager.lua | 22 | ||||
-rw-r--r-- | core/sessionmanager.lua | 233 | ||||
-rw-r--r-- | core/stanza_dispatch.lua | 149 | ||||
-rw-r--r-- | core/stanza_router.lua | 409 | ||||
-rw-r--r-- | core/storagemanager.lua | 135 | ||||
-rw-r--r-- | core/usermanager.lua | 152 | ||||
-rw-r--r-- | core/xmlhandlers.lua | 122 |
17 files changed, 2646 insertions, 881 deletions
diff --git a/core/certmanager.lua b/core/certmanager.lua new file mode 100644 index 00000000..b91f7110 --- /dev/null +++ b/core/certmanager.lua @@ -0,0 +1,108 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local configmanager = require "core.configmanager"; +local log = require "util.logger".init("certmanager"); +local ssl = ssl; +local ssl_newcontext = ssl and ssl.newcontext; + +local tostring = tostring; + +local prosody = prosody; +local resolve_path = configmanager.resolve_relative_path; +local config_path = prosody.paths.config; + +local luasec_has_noticket, luasec_has_verifyext; +if ssl then + local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)"); + luasec_has_noticket = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=4; + luasec_has_verifyext = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=5; +end + +module "certmanager" + +-- Global SSL options if not overridden per-host +local default_ssl_config = configmanager.get("*", "ssl"); +local default_capath = "/etc/ssl/certs"; +local default_verify = (ssl and ssl.x509 and { "peer", "client_once", }) or "none"; +local default_options = { "no_sslv2", luasec_has_noticket and "no_ticket" or nil }; +local default_verifyext = { "lsec_continue", "lsec_ignore_purpose" }; + +if ssl and not luasec_has_verifyext and ssl.x509 then + -- COMPAT mw/luasec-hg + for i=1,#default_verifyext do -- Remove lsec_ prefix + default_verify[#default_verify+1] = default_verifyext[i]:sub(6); + end +end + +function create_context(host, mode, user_ssl_config) + user_ssl_config = user_ssl_config or default_ssl_config; + + if not ssl then return nil, "LuaSec (required for encryption) was not found"; end + if not user_ssl_config then return nil, "No SSL/TLS configuration present for "..host; end + + local ssl_config = { + mode = mode; + protocol = user_ssl_config.protocol or "sslv23"; + key = resolve_path(config_path, user_ssl_config.key); + password = user_ssl_config.password or function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end; + certificate = resolve_path(config_path, user_ssl_config.certificate); + capath = resolve_path(config_path, user_ssl_config.capath or default_capath); + cafile = resolve_path(config_path, user_ssl_config.cafile); + verify = user_ssl_config.verify or default_verify; + verifyext = user_ssl_config.verifyext or default_verifyext; + options = user_ssl_config.options or default_options; + depth = user_ssl_config.depth; + }; + + local ctx, err = ssl_newcontext(ssl_config); + + -- LuaSec ignores the cipher list from the config, so we have to take care + -- of it ourselves (W/A for #x) + if ctx and user_ssl_config.ciphers then + local success; + success, err = ssl.context.setcipher(ctx, user_ssl_config.ciphers); + if not success then ctx = nil; end + end + + if not ctx then + err = err or "invalid ssl config" + local file = err:match("^error loading (.-) %("); + if file then + if file == "private key" then + file = ssl_config.key or "your private key"; + elseif file == "certificate" then + file = ssl_config.certificate or "your certificate file"; + end + local reason = err:match("%((.+)%)$") or "some reason"; + if reason == "Permission denied" then + reason = "Check that the permissions allow Prosody to read this file."; + elseif reason == "No such file or directory" then + reason = "Check that the path is correct, and the file exists."; + elseif reason == "system lib" then + reason = "Previous error (see logs), or other system error."; + elseif reason == "(null)" or not reason then + reason = "Check that the file exists and the permissions are correct"; + else + reason = "Reason: "..tostring(reason):lower(); + end + log("error", "SSL/TLS: Failed to load '%s': %s (for %s)", file, reason, host); + else + log("error", "SSL/TLS: Error initialising for %s: %s", host, err); + end + end + return ctx, err; +end + +function reload_ssl_config() + default_ssl_config = configmanager.get("*", "ssl"); +end + +prosody.events.add_handler("config-reloaded", reload_ssl_config); + +return _M; diff --git a/core/configmanager.lua b/core/configmanager.lua new file mode 100644 index 00000000..9720f48a --- /dev/null +++ b/core/configmanager.lua @@ -0,0 +1,265 @@ +-- 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, 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); + +module "configmanager" + +local parsers = {}; + +local config_mt = { __index = function (t, k) return rawget(t, "*"); end}; +local config = setmetatable({ ["*"] = { } }, config_mt); + +-- When host not found, use global +local host_mt = { __index = function(_, k) return config["*"][k] end } + +function getconfig() + return config; +end + +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 +end + +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 + hostconfig[key] = value; + return true; + end + return false; +end + +function _M.set(host, key, value, _oldvalue) + if key == "core" then + key, value = value, _oldvalue; --COMPAT with code that still uses "core" + end + return set(config, host, key, value); +end + +-- Helper function to resolve relative paths (needed by config) +do + function resolve_relative_path(parent_path, path) + if path then + -- Some normalization + parent_path = parent_path:gsub("%"..path_sep.."+$", ""); + path = path:gsub("^%.%"..path_sep.."+", ""); + + local is_relative; + if path_sep == "/" and path:sub(1,1) ~= "/" then + is_relative = true; + elseif path_sep == "\\" and (path:sub(1,1) ~= "/" and (path:sub(2,3) ~= ":\\" or path:sub(2,3) ~= ":/")) then + is_relative = true; + end + if is_relative then + return parent_path..path_sep..path; + end + end + return path; + end +end + +-- Helper function to convert a glob to a Lua pattern +local function glob_to_pattern(glob) + return "^"..glob:gsub("[%p*?]", function (c) + if c == "*" then + return ".*"; + elseif c == "?" then + return "."; + else + return "%"..c; + end + end).."$"; +end + +function load(filename, format) + format = format or filename:match("%w+$"); + + if parsers[format] and parsers[format].load then + local f, err = io.open(filename); + if f then + local new_config = setmetatable({ ["*"] = { } }, config_mt); + local ok, err = parsers[format].load(f:read("*a"), filename, new_config); + f:close(); + if ok then + config = new_config; + fire_event("config-reloaded", { + filename = filename, + format = format, + config = config + }); + end + return ok, "parser", err; + end + return f, "file", err; + end + + if not format then + return nil, "file", "no parser specified"; + else + return nil, "file", "no parser for "..(format); + end +end + +function save(filename, format) +end + +function addparser(format, parser) + if format and parser then + parsers[format] = parser; + end +end + +-- _M needed to avoid name clash with local 'parsers' +function _M.parsers() + local p = {}; + for format in pairs(parsers) do + table.insert(p, format); + end + return p; +end + +-- Built-in Lua parser +do + local pcall, setmetatable = _G.pcall, _G.setmetatable; + local rawget = _G.rawget; + parsers.lua = {}; + function parsers.lua.load(data, config_file, config) + local env; + -- The ' = true' are needed so as not to set off __newindex when we assign the functions below + env = setmetatable({ + Host = true, host = true, VirtualHost = true, + Component = true, component = true, + Include = true, include = true, RunScript = true }, { + __index = function (t, k) + return rawget(_G, k); + end, + __newindex = function (t, k, v) + set(config, env.__currenthost or "*", k, v); + end + }); + + rawset(env, "__currenthost", "*") -- Default is global + function env.VirtualHost(name) + if rawget(config, name) and rawget(config[name], "component_module") then + error(format("Host %q clashes with previously defined %s Component %q, for services use a sub-domain like conference.%s", + name, config[name].component_module:gsub("^%a+$", { component = "external", muc = "MUC"}), name, name), 0); + end + rawset(env, "__currenthost", name); + -- Needs at least one setting to logically exist :) + set(config, name or "*", "defined", true); + return function (config_options) + rawset(env, "__currenthost", "*"); -- Return to global scope + for option_name, option_value in pairs(config_options) do + set(config, name or "*", option_name, option_value); + end + end; + end + env.Host, env.host = env.VirtualHost, env.VirtualHost; + + function env.Component(name) + if rawget(config, name) and rawget(config[name], "defined") and not rawget(config[name], "component_module") then + error(format("Component %q clashes with previously defined Host %q, for services use a sub-domain like conference.%s", + name, name, name), 0); + end + set(config, name, "component_module", "component"); + -- Don't load the global modules by default + set(config, name, "load_global_modules", false); + rawset(env, "__currenthost", name); + local function handle_config_options(config_options) + rawset(env, "__currenthost", "*"); -- Return to global scope + for option_name, option_value in pairs(config_options) do + set(config, name or "*", option_name, option_value); + end + end + + return function (module) + if type(module) == "string" then + set(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) + if file:match("[*?]") then + local path_pos, glob = file:match("()([^"..path_sep.."]+)$"); + local path = file:sub(1, math_max(path_pos-2,0)); + local config_path = config_file:gsub("[^"..path_sep.."]+$", ""); + if #path > 0 then + path = resolve_relative_path(config_path, path); + else + path = config_path; + end + local patt = glob_to_pattern(glob); + for f in lfs.dir(path) do + if f:sub(1,1) ~= "." and f:match(patt) then + env.Include(path..path_sep..f); + end + end + else + local file = resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file); + local f, err = io.open(file); + if f then + local ret, err = parsers.lua.load(f:read("*a"), file, config); + if not ret then error(err:gsub("%[string.-%]", file), 0); end + end + if not f then error("Error loading included "..file..": "..err, 0); end + return f, err; + end + end + env.include = env.Include; + + function env.RunScript(file) + return dofile(resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file)); + end + + local chunk, err = envload(data, "@"..config_file, env); + + if not chunk then + return nil, err; + end + + 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 new file mode 100644 index 00000000..06ba72a1 --- /dev/null +++ b/core/hostmanager.lua @@ -0,0 +1,158 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local configmanager = require "core.configmanager"; +local modulemanager = require "core.modulemanager"; +local events_new = require "util.events".new; +local disco_items = require "util.multitable".new(); +local NULL = {}; + +local jid_split = require "util.jid".split; +local uuid_gen = require "util.uuid".generate; + +local log = require "util.logger".init("hostmanager"); + +local hosts = prosody.hosts; +local prosody_events = prosody.events; +if not _G.prosody.incoming_s2s then + require "core.s2smanager"; +end +local incoming_s2s = _G.prosody.incoming_s2s; +local core_route_stanza = _G.prosody.core_route_stanza; + +local pairs, select, rawget = pairs, select, rawget; +local tostring, type = tostring, type; + +module "hostmanager" + +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.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 rawget(hosts, host) then return nil, "The host "..host.." is already activated"; end + host_config = host_config or configmanager.getconfig()[host]; + if not host_config then return nil, "Couldn't find the host "..tostring(host).." defined in the current config"; end + local host_session = { + host = host; + s2sout = {}; + events = events_new(); + dialback_secret = configmanager.get(host, "dialback_secret") or uuid_gen(); + send = host_send; + modules = {}; + }; + if not host_config.component_module then -- host + host_session.type = "local"; + host_session.sessions = {}; + else -- component + host_session.type = "component"; + end + hosts[host] = host_session; + if not host:match("[@/]") then + disco_items:set(host:match("%.(.*)") or "*", host, host_config.name or true); + end + for option_name in pairs(host_config) do + if option_name:match("_ports$") or option_name:match("_interface$") then + log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in the server-wide section instead", host, option_name); + end + end + + log((hosts_loaded_once and "info") or "debug", "Activated host: %s", host); + prosody_events.fire_event("host-activated", host); + return true; +end + +function deactivate(host, reason) + local host_session = hosts[host]; + if not host_session then return nil, "The host "..tostring(host).." is not activated"; end + log("info", "Deactivating host: %s", host); + prosody_events.fire_event("host-deactivating", { host = host, 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 + log("debug", "Closing connection for %s@%s/%s", username, host, resource); + session:close(reason); + end + end + end + if host_session.s2sout then + for remotehost, session in pairs(host_session.s2sout) do + if session.close then + log("debug", "Closing outgoing connection to %s", remotehost); + if session.srv_hosts then session.srv_hosts = nil; end + session:close(reason); + end + end + end + for remote_session in pairs(incoming_s2s) do + if remote_session.to_host == host then + log("debug", "Closing incoming connection from %s", remote_session.from_host or "<unknown>"); + remote_session:close(reason); + end + end + + -- TODO: This should be done in modulemanager + if host_session.modules then + for module in pairs(host_session.modules) do + modulemanager.unload(host, module); + end + end + + hosts[host] = nil; + if not host:match("[@/]") then + disco_items:remove(host:match("%.(.*)") or "*", host); + end + prosody_events.fire_event("host-deactivated", host); + log("info", "Deactivated host: %s", host); + return true; +end + +function get_children(host) + return disco_items:get(host) or NULL; +end + +return _M; diff --git a/core/loggingmanager.lua b/core/loggingmanager.lua new file mode 100644 index 00000000..c69dede8 --- /dev/null +++ b/core/loggingmanager.lua @@ -0,0 +1,276 @@ +-- 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 = 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.date; +local getstyle, setstyle = require "util.termcolours".getstyle, require "util.termcolours".setstyle; + +if os.getenv("__FLUSH_LOG") then + local io_flush = io.flush; + local _io_write = io_write; + io_write = function(...) _io_write(...); io_flush(); end +end + +local config = require "core.configmanager"; +local logger = require "util.logger"; +local prosody = prosody; + +_G.log = logger.init("general"); + +module "loggingmanager" + +-- The log config used if none specified in the config file (see reload_logging for initialization) +local default_logging; +local default_file_logging; +local default_timestamp = "%b %d %H:%M:%S"; +-- The actual config loggingmanager is using +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" } + +-- 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 + -- 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 + end +end + +-- Search for all rules using a particular sink type, and apply +-- them. Called automatically when a new sink type is added to +-- the log_sink_types table. +function apply_sink_rules(sink_type) + if type(logging_config) == "table" then + + for _, level in ipairs(logging_levels) do + if type(logging_config[level]) == "string" then + local value = logging_config[level]; + if sink_type == "file" and not value:match("^%*") then + add_rule({ + to = sink_type; + filename = value; + timestamps = true; + levels = { min = level }; + }); + elseif value == "*"..sink_type then + add_rule({ + to = sink_type; + levels = { min = level }; + }); + end + end + end + + for _, sink_config in ipairs(logging_config) do + if (type(sink_config) == "table" and sink_config.to == sink_type) then + add_rule(sink_config); + elseif (type(sink_config) == "string" and sink_config:match("^%*(.+)") == sink_type) then + add_rule({ levels = { min = "debug" }, to = sink_type }); + end + end + elseif type(logging_config) == "string" and (not logging_config:match("^%*")) and sink_type == "file" then + -- User specified simply a filename, and the "file" sink type + -- was just added + for _, sink_config in pairs(default_file_logging) do + sink_config.filename = logging_config; + add_rule(sink_config); + sink_config.filename = nil; + end + elseif type(logging_config) == "string" and logging_config:match("^%*(.+)") == sink_type then + -- Log all levels (debug+) to this sink + add_rule({ levels = { min = "debug" }, to = sink_type }); + end +end + + + +--- Helper function to get a set of levels given a "criteria" table +function get_levels(criteria, set) + set = set or {}; + if type(criteria) == "string" then + set[criteria] = true; + return set; + end + local min, max = criteria.min, criteria.max; + if min or max then + local in_range; + for _, level in ipairs(logging_levels) do + if min == level then + set[level] = true; + in_range = true; + elseif max == level then + set[level] = true; + return set; + elseif in_range then + set[level] = true; + end + end + end + + for _, level in ipairs(criteria) do + set[level] = true; + end + return set; +end + +-- Initialize config, etc. -- +function reload_logging() + local old_sink_types = {}; + + for name, sink_maker in pairs(log_sink_types) do + old_sink_types[name] = sink_maker; + log_sink_types[name] = nil; + end + + logger.reset(); + + local debug_mode = config.get("*", "debug"); + + default_logging = { { to = "console" , levels = { min = (debug_mode and "debug") or "info" } } }; + default_file_logging = { + { to = "file", levels = { min = (debug_mode and "debug") or "info" }, timestamps = true } + }; + default_timestamp = "%b %d %H:%M:%S"; + + logging_config = config.get("*", "log") or default_logging; + + + for name, sink_maker in pairs(old_sink_types) do + log_sink_types[name] = sink_maker; + end + + prosody.events.fire_event("logging-reloaded"); +end + +reload_logging(); +prosody.events.add_handler("config-reloaded", reload_logging); + +--- Definition of built-in logging sinks --- + +-- Null sink, must enter log_sink_types *first* +function log_sink_types.nowhere() + return function () return false; end; +end + +-- Column width for "source" (used by stdout and console) +local sourcewidth = 20; + +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; + if timestamps then + io_write(os_date(timestamps), " "); + end + if ... then + io_write(name, rep(" ", sourcewidth-namelen), level, "\t", format(message, ...), "\n"); + else + io_write(name, rep(" ", sourcewidth-namelen), level, "\t", message, "\n"); + end + end +end + +do + local do_pretty_printing = true; + + local logstyles = {}; + if do_pretty_printing then + logstyles["info"] = getstyle("bold"); + logstyles["warn"] = getstyle("bold", "yellow"); + logstyles["error"] = getstyle("bold", "red"); + end + function log_sink_types.console(config) + -- Really if we don't want pretty colours then just use plain stdout + if not do_pretty_printing then + return log_sink_types.stdout(config); + end + + 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; + + if timestamps then + io_write(os_date(timestamps), " "); + end + io_write(name, rep(" ", sourcewidth-namelen)); + setstyle(logstyles[level]); + io_write(level); + setstyle(); + if ... then + io_write("\t", format(message, ...), "\n"); + else + io_write("\t", message, "\n"); + end + end + end +end + +local empty_function = function () end; +function log_sink_types.file(config) + local log = config.filename; + local logfile = io_open(log, "a+"); + if not logfile then + return empty_function; + end + local write, flush = logfile.write, logfile.flush; + + local timestamps = config.timestamps; + + if timestamps == nil or timestamps == true then + timestamps = default_timestamp; -- Default format + end + + return function (name, level, message, ...) + if timestamps then + write(logfile, os_date(timestamps), " "); + end + if ... then + write(logfile, name, "\t", level, "\t", format(message, ...), "\n"); + else + write(logfile, name, "\t" , level, "\t", message, "\n"); + end + flush(logfile); + end; +end + +function register_sink_type(name, sink_maker) + local old_sink_maker = log_sink_types[name]; + log_sink_types[name] = sink_maker; + return old_sink_maker; +end + +return _M; diff --git a/core/moduleapi.lua b/core/moduleapi.lua new file mode 100644 index 00000000..ed75669b --- /dev/null +++ b/core/moduleapi.lua @@ -0,0 +1,367 @@ +-- Prosody IM +-- Copyright (C) 2008-2012 Matthew Wild +-- Copyright (C) 2008-2012 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local config = require "core.configmanager"; +local modulemanager = require "modulemanager"; -- This is necessary to avoid require loops +local array = require "util.array"; +local set = require "util.set"; +local logger = require "util.logger"; +local pluginloader = require "util.pluginloader"; +local timer = require "util.timer"; + +local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; +local error, setmetatable, type = error, setmetatable, type; +local ipairs, pairs, select, unpack = ipairs, pairs, select, unpack; +local tonumber, tostring = tonumber, tostring; + +local prosody = prosody; +local hosts = prosody.hosts; + +-- FIXME: This assert() is to try and catch an obscure bug (2013-04-05) +local core_post_stanza = assert(prosody.core_post_stanza, + "prosody.core_post_stanza is nil, please report this as a bug"); + +-- Registry of shared module data +local shared_data = setmetatable({}, { __mode = "v" }); + +local NULL = {}; + +local api = {}; + +-- Returns the name of the current module +function api:get_name() + return self.name; +end + +-- Returns the host that the current module is serving +function api:get_host() + return self.host; +end + +function api:get_host_type() + return self.host ~= "*" and hosts[self.host].type or nil; +end + +function api:set_global() + self.host = "*"; + -- Update the logger + local _log = logger.init("mod_"..self.name); + self.log = function (self, ...) return _log(...); end; + self._log = _log; + self.global = true; +end + +function api:add_feature(xmlns) + self:add_item("feature", xmlns); +end +function api:add_identity(category, type, name) + self:add_item("identity", {category = category, type = type, name = name}); +end +function api:add_extension(data) + self:add_item("extension", data); +end +function api:has_feature(xmlns) + for _, feature in ipairs(self:get_host_items("feature")) do + if feature == xmlns then return true; end + end + return false; +end +function api:has_identity(category, type, name) + for _, id in ipairs(self:get_host_items("identity")) do + if id.category == category and id.type == type and id.name == name then + return true; + end + end + return false; +end + +function api:fire_event(...) + return (hosts[self.host] or prosody).events.fire_event(...); +end + +function api:hook_object_event(object, event, handler, priority) + self.event_handlers:set(object, event, handler, true); + return object.add_handler(event, handler, priority); +end + +function api:unhook_object_event(object, event, handler) + return object.remove_handler(event, handler); +end + +function api:hook(event, handler, priority) + return self:hook_object_event((hosts[self.host] or prosody).events, event, handler, priority); +end + +function api:hook_global(event, handler, priority) + return self:hook_object_event(prosody.events, event, handler, priority); +end + +function api:hook_tag(xmlns, name, handler, priority) + if not handler and type(name) == "function" then + -- If only 2 options then they specified no xmlns + xmlns, name, handler, priority = nil, xmlns, name, handler; + elseif not (handler and name) then + self:log("warn", "Error: Insufficient parameters to module:hook_stanza()"); + return; + end + return self:hook("stanza/"..(xmlns and (xmlns..":") or "")..name, function (data) return handler(data.origin, data.stanza, data); end, priority); +end +api.hook_stanza = api.hook_tag; -- COMPAT w/pre-0.9 + +function api:require(lib) + local f, n = pluginloader.load_code(self.name, lib..".lib.lua", self.environment); + if not f then + f, n = pluginloader.load_code(lib, lib..".lib.lua", self.environment); + end + if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message + return f(); +end + +function api:depends(name) + if not self.dependencies then + self.dependencies = {}; + self:hook("module-reloaded", function (event) + if self.dependencies[event.module] and not self.reloading then + self:log("info", "Auto-reloading due to reload of %s:%s", event.host, event.module); + modulemanager.reload(self.host, self.name); + return; + end + end); + self:hook("module-unloaded", function (event) + if self.dependencies[event.module] then + self:log("info", "Auto-unloading due to unload of %s:%s", event.host, event.module); + modulemanager.unload(self.host, self.name); + end + end); + end + local mod = modulemanager.get_module(self.host, name) or modulemanager.get_module("*", name); + if mod and mod.module.host == "*" and self.host ~= "*" + and modulemanager.module_has_method(mod, "add_host") then + mod = nil; -- Target is a shared module, so we still want to load it on our host + end + if not mod then + local err; + mod, err = modulemanager.load(self.host, name); + if not mod then + return error(("Unable to load required module, mod_%s: %s"):format(name, ((err or "unknown error"):gsub("%-", " ")) )); + end + end + self.dependencies[name] = true; + return mod; +end + +-- Returns one or more shared tables at the specified virtual paths +-- Intentionally does not allow the table at a path to be _set_, it +-- is auto-created if it does not exist. +function api:shared(...) + if not self.shared_data then self.shared_data = {}; end + local paths = { n = select("#", ...), ... }; + local data_array = {}; + local default_path_components = { self.host, self.name }; + for i = 1, paths.n do + local path = paths[i]; + if path:sub(1,1) ~= "/" then -- Prepend default components + local n_components = select(2, path:gsub("/", "%1")); + path = (n_components<#default_path_components and "/" or "")..t_concat(default_path_components, "/", 1, #default_path_components-n_components).."/"..path; + end + local shared = shared_data[path]; + if not shared then + shared = {}; + if path:match("%-cache$") then + setmetatable(shared, { __mode = "kv" }); + end + shared_data[path] = shared; + end + t_insert(data_array, shared); + self.shared_data[path] = shared; + end + return unpack(data_array); +end + +function api:get_option(name, default_value) + local value = config.get(self.host, name); + if value == nil then + value = default_value; + end + return value; +end + +function api:get_option_string(name, default_value) + local value = self:get_option(name, default_value); + if type(value) == "table" then + if #value > 1 then + self:log("error", "Config option '%s' does not take a list, using just the first item", name); + end + value = value[1]; + end + if value == nil then + return nil; + end + return tostring(value); +end + +function api:get_option_number(name, ...) + local value = self:get_option(name, ...); + if type(value) == "table" then + if #value > 1 then + self:log("error", "Config option '%s' does not take a list, using just the first item", name); + end + value = value[1]; + end + local ret = tonumber(value); + if value ~= nil and ret == nil then + self:log("error", "Config option '%s' not understood, expecting a number", name); + end + return ret; +end + +function api:get_option_boolean(name, ...) + local value = self:get_option(name, ...); + if type(value) == "table" then + if #value > 1 then + self:log("error", "Config option '%s' does not take a list, using just the first item", name); + end + value = value[1]; + end + if value == nil then + return nil; + end + local ret = value == true or value == "true" or value == 1 or nil; + if ret == nil then + ret = (value == false or value == "false" or value == 0); + if ret then + ret = false; + else + ret = nil; + end + end + if ret == nil then + self:log("error", "Config option '%s' not understood, expecting true/false", name); + end + return ret; +end + +function api:get_option_array(name, ...) + local value = self:get_option(name, ...); + + if value == nil then + return nil; + end + + if type(value) ~= "table" then + return array{ value }; -- Assume any non-list is a single-item list + end + + return array():append(value); -- Clone +end + +function api:get_option_set(name, ...) + local value = self:get_option_array(name, ...); + + if value == nil then + return nil; + end + + return set.new(value); +end + +function api:get_option_inherited_set(name, ...) + local value = self:get_option_set(name, ...); + local global_value = self:context("*"):get_option_set(name, ...); + if not value then + return global_value; + elseif not global_value then + return value; + end + value:include(global_value); + return value; +end + +function api:context(host) + return setmetatable({host=host or "*"}, {__index=self,__newindex=self}); +end + +function api:add_item(key, value) + self.items = self.items or {}; + self.items[key] = self.items[key] or {}; + t_insert(self.items[key], value); + self:fire_event("item-added/"..key, {source = self, item = value}); +end +function api:remove_item(key, value) + local t = self.items and self.items[key] or NULL; + for i = #t,1,-1 do + if t[i] == value then + t_remove(self.items[key], i); + self:fire_event("item-removed/"..key, {source = self, item = value}); + return value; + end + end +end + +function api:get_host_items(key) + local result = modulemanager.get_items(key, self.host) or {}; + return result; +end + +function api:handle_items(type, added_cb, removed_cb, existing) + self:hook("item-added/"..type, added_cb); + self:hook("item-removed/"..type, removed_cb); + if existing ~= false then + for _, item in ipairs(self:get_host_items(type)) do + added_cb({ item = item }); + end + end +end + +function api:provides(name, item) + -- if not item then item = setmetatable({}, { __index = function(t,k) return rawget(self.environment, k); end }); end + if not item then + item = {} + for k,v in pairs(self.environment) do + if k ~= "module" then item[k] = v; end + end + end + if not item.name then + local item_name = self.name; + -- Strip a provider prefix to find the item name + -- (e.g. "auth_foo" -> "foo" for an auth provider) + if item_name:find(name.."_", 1, true) == 1 then + item_name = item_name:sub(#name+2); + end + item.name = item_name; + end + item._provided_by = self.name; + self:add_item(name.."-provider", item); +end + +function api:send(stanza) + return core_post_stanza(hosts[self.host], stanza); +end + +function api:add_timer(delay, callback) + return timer.add_task(delay, function (t) + if self.loaded == false then return; end + return callback(t); + end); +end + +local path_sep = package.config:sub(1,1); +function api:get_directory() + return self.path and (self.path:gsub("%"..path_sep.."[^"..path_sep.."]*$", "")) or nil; +end + +function api:load_resource(path, mode) + path = config.resolve_relative_path(self:get_directory(), path); + return io.open(path, mode); +end + +function api:open_store(name, type) + return storagemanager.open(self.host, name or self.name, type); +end + +return api; diff --git a/core/modulemanager.lua b/core/modulemanager.lua index 24708232..535c227b 100644 --- a/core/modulemanager.lua +++ b/core/modulemanager.lua @@ -1,123 +1,319 @@ +-- 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 log = require "util.logger".init("modulemanager") +local logger = require "util.logger"; +local log = logger.init("modulemanager"); +local config = require "core.configmanager"; +local pluginloader = require "util.pluginloader"; +local set = require "util.set"; -local loadfile, pcall = loadfile, pcall; -local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv; -local pairs, ipairs = pairs, ipairs; -local t_insert = table.insert; -local type = type; +local new_multitable = require "util.multitable".new; -local tostring, print = tostring, print; +local hosts = hosts; +local prosody = prosody; +local pcall, xpcall = pcall, xpcall; +local setmetatable, rawget = setmetatable, rawget; +local ipairs, pairs, type, tostring, t_insert = ipairs, pairs, type, tostring, table.insert; + +local debug_traceback = debug.traceback; +local unpack, select = unpack, select; +pcall = function(f, ...) + local n = select("#", ...); + local params = {...}; + return xpcall(function() return f(unpack(params, 1, n)) end, function(e) return tostring(e).."\n"..debug_traceback(); end); +end + +local autoload_modules = {"presence", "message", "iq", "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" -local handler_info = {}; -local handlers = {}; - -local modulehelpers = setmetatable({}, { __index = _G }); - -function modulehelpers.add_iq_handler(origin_type, xmlns, handler) - if not (origin_type and handler and xmlns) then return false; end - handlers[origin_type] = handlers[origin_type] or {}; - handlers[origin_type].iq = handlers[origin_type].iq or {}; - if not handlers[origin_type].iq[xmlns] then - handlers[origin_type].iq[xmlns]= handler; - handler_info[handler] = getfenv(2).module; - log("debug", "mod_%s now handles tag 'iq' with query namespace '%s'", getfenv(2).module.name, xmlns); - else - log("warning", "mod_%s wants to handle tag 'iq' with query namespace '%s' but mod_%s already handles that", getfenv(2).module.name, xmlns, handler_info[handlers[origin_type].iq[xmlns]].module.name); - end -end +local api = _G.require "core.moduleapi"; -- Module API container + +-- [host] = { [module] = module_env } +local modulemap = { ["*"] = {} }; -function modulehelpers.add_handler(origin_type, tag, xmlns, handler) - if not (origin_type and tag and xmlns and handler) then return false; end - handlers[origin_type] = handlers[origin_type] or {}; - if not handlers[origin_type][tag] then - handlers[origin_type][tag] = handlers[origin_type][tag] or {}; - handlers[origin_type][tag][xmlns]= handler; - handler_info[handler] = getfenv(2).module; - log("debug", "mod_%s now handles tag '%s'", getfenv(2).module.name, tag); - elseif handler_info[handlers[origin_type][tag]] then - log("warning", "mod_%s wants to handle tag '%s' but mod_%s already handles that", getfenv(2).module.name, tag, handler_info[handlers[origin_type][tag]].module.name); +-- Load modules when a host is activated +function load_modules_for_host(host) + local component = config.get(host, "component_module"); + + local global_modules_enabled = config.get("*", "modules_enabled"); + local global_modules_disabled = config.get("*", "modules_disabled"); + local host_modules_enabled = config.get(host, "modules_enabled"); + local host_modules_disabled = config.get(host, "modules_disabled"); + + if host_modules_enabled == global_modules_enabled then host_modules_enabled = nil; end + if host_modules_disabled == global_modules_disabled then host_modules_disabled = nil; end + + local global_modules = set.new(autoload_modules) + set.new(global_modules_enabled) - set.new(global_modules_disabled); + if component then + global_modules = set.intersection(set.new(component_inheritable_modules), global_modules); + end + local modules = (global_modules + set.new(host_modules_enabled)) - set.new(host_modules_disabled); + + -- COMPAT w/ pre 0.8 + if modules:contains("console") then + log("error", "The mod_console plugin has been renamed to mod_admin_telnet. Please update your config."); + modules:remove("console"); + modules:add("admin_telnet"); + end + + if component then + load(host, component); + end + for module in modules do + load(host, module); end end +prosody.events.add_handler("host-activated", load_modules_for_host); +prosody.events.add_handler("host-deactivated", function (host) + modulemap[host] = nil; +end); -function loadall() - load("saslauth"); - load("legacyauth"); - load("roster"); - load("register"); - load("tls"); - load("vcard"); -end +--- Private helpers --- -function load(name) - local mod, err = loadfile("plugins/mod_"..name..".lua"); - if not mod then - log("error", "Unable to load module '%s': %s", name or "nil", err or "nil"); - return; +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 - local pluginenv = setmetatable({ module = { name = name } }, { __index = modulehelpers }); + for object, event, handler in mod.module.event_handlers:iter(nil, nil, nil) do + object.remove_handler(event, handler); + end - setfenv(mod, pluginenv); - local success, ret = pcall(mod); - if not success then - log("error", "Error initialising module '%s': %s", name or "nil", ret or "nil"); - return; + 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 handle_stanza(origin, stanza) - local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns, origin.type; +local function do_load_module(host, module_name, state) + if not (host and module_name) then + return nil, "insufficient-parameters"; + elseif not hosts[host] and host ~= "*"then + return nil, "unknown-host"; + end - if name == "iq" and xmlns == "jabber:client" and handlers[origin_type] then - log("debug", "Stanza is an <iq/>"); - local child = stanza.tags[1]; - if child then - local xmlns = child.attr.xmlns; - log("debug", "Stanza has xmlns: %s", xmlns); - local handler = handlers[origin_type][name][xmlns]; - if handler then - log("debug", "Passing stanza to mod_%s", handler_info[handler].name); - return handler(origin, stanza) or true; + if not modulemap[host] then + 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 _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 + + api_instance.path = err; + + modulemap[host][module_name] = pluginenv; + local ok, err = pcall(mod); + if ok then + -- Call module's "load" + if module_has_method(pluginenv, "load") then + ok, err = call_module_method(pluginenv, "load"); + if not ok then + log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err or "nil"); + end end - elseif handlers[origin_type] then - local handler = handlers[origin_type][name]; - if handler then - handler = handler[xmlns]; - if handler then - log("debug", "Passing stanza to mod_%s", handler_info[handler].name); - return handler(origin, stanza) or true; + api_instance.reloading, api_instance.saved_state = nil, nil; + + if api_instance.host == "*" then + if not api_instance.global then -- COMPAT w/pre-0.9 + if host ~= "*" then + log("warn", "mod_%s: Setting module.host = '*' deprecated, call module:set_global() instead", module_name); + end + api_instance:set_global(); + end + modulemap[host][module_name] = nil; + modulemap[api_instance.host][module_name] = pluginenv; + if host ~= api_instance.host and module_has_method(pluginenv, "add_host") then + -- Now load the module again onto the host it was originally being loaded on + ok, err = do_load_module(host, module_name); end end end - log("debug", "Stanza unhandled by any modules, xmlns: %s", stanza.attr.xmlns); - return false; -- we didn't handle it + 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 -do - local event_handlers = {}; - - function modulehelpers.add_event_hook(name, handler) - if not event_handlers[name] then - event_handlers[name] = {}; +local function do_reload_module(host, name) + local mod = get_module(host, name); + if not mod then return nil, "module-not-loaded"; end + + local _mod, err = pluginloader.load_code(name); -- checking for syntax errors + if not _mod then + log("error", "Unable to load module '%s': %s", name or "nil", err or "nil"); + return nil, err; + 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, 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 + log("warn", "Continuing with reload (using the force)"); + end end - t_insert(event_handlers[name] , handler); end - - function fire_event(name, ...) - local event_handlers = event_handlers[name]; - if event_handlers then - for name, handler in ipairs(event_handlers) do - handler(...); + + 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 + local ok, err = call_module_method(mod, "restore", saved or {}) + if (not ok) and err then + log("warn", "Error restoring module '%s' from '%s': %s", name, host, err); end end end + return ok and mod, err; +end + +--- Public API --- + +-- 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 + +-- Unload a module and fire module-unloaded +function unload(host, name) + local ok, err = do_unload_module(host, name); + if ok then + (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host }); + end + return ok, err; +end + +function reload(host, name) + local mod, err = do_reload_module(host, name); + if mod then + modulemap[host][name].module.reloading = true; + (hosts[host] or prosody).events.fire_event("module-reloaded", { module = name, host = host }); + mod.module.reloading = nil; + elseif not is_loaded(host, name) then + (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host }); + end + return mod, err; +end + +function get_module(host, name) + return modulemap[host] and modulemap[host][name]; +end + +function get_items(key, host) + local result = {}; + local modules = modulemap[host]; + if not key or not host or not modules then return nil; end + + for _, module in pairs(modules) do + local mod = module.module; + if mod.items and mod.items[key] then + for _, value in ipairs(mod.items[key]) do + t_insert(result, value); + end + end + end + + return result; +end + +function get_modules(host) + return modulemap[host]; +end + +function is_loaded(host, name) + return modulemap[host] and modulemap[host][name] and true; +end + +function module_has_method(module, method) + return type(rawget(module.module, method)) == "function"; +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 end return _M; diff --git a/core/offlinemessage.lua b/core/offlinemessage.lua deleted file mode 100644 index dda9b7d8..00000000 --- a/core/offlinemessage.lua +++ /dev/null @@ -1,13 +0,0 @@ - -require "util.datamanager" - -local datamanager = datamanager; -local t_insert = table.insert; - -module "offlinemessage" - -function new(user, host, stanza) - local offlinedata = datamanager.load(user, host, "offlinemsg") or {}; - t_insert(offlinedata, stanza); - return datamanager.store(user, host, "offlinemsg", offlinedata); -end diff --git a/core/portmanager.lua b/core/portmanager.lua new file mode 100644 index 00000000..7a247452 --- /dev/null +++ b/core/portmanager.lua @@ -0,0 +1,239 @@ +local config = require "core.configmanager"; +local certmanager = require "core.certmanager"; +local server = require "net.server"; +local socket = require "socket"; + +local log = require "util.logger".init("portmanager"); +local multitable = require "util.multitable"; +local set = require "util.set"; + +local table = table; +local setmetatable, rawset, rawget = setmetatable, rawset, rawget; +local type, tonumber, tostring, ipairs, pairs = type, tonumber, tostring, ipairs, pairs; + +local prosody = prosody; +local fire_event = prosody.events.fire_event; + +module "portmanager"; + +--- Config + +local default_interfaces = { }; +local default_local_interfaces = { }; +if config.get("*", "use_ipv4") ~= false then + table.insert(default_interfaces, "*"); + table.insert(default_local_interfaces, "127.0.0.1"); +end +if socket.tcp6 and config.get("*", "use_ipv6") ~= false then + table.insert(default_interfaces, "::"); + table.insert(default_local_interfaces, "::1"); +end + +--- Private state + +-- service_name -> { service_info, ... } +local services = setmetatable({}, { __index = function (t, k) rawset(t, k, {}); return rawget(t, k); end }); + +-- service_name, interface (string), port (number) +local active_services = multitable.new(); + +--- Private helpers + +local function error_to_friendly_message(service_name, port, err) + local friendly_message = err; + if err:match(" in use") then + -- FIXME: Use service_name here + if port == 5222 or port == 5223 or port == 5269 then + friendly_message = "check that Prosody or another XMPP server is " + .."not already running and using this port"; + elseif port == 80 or port == 81 then + friendly_message = "check that a HTTP server is not already using " + .."this port"; + elseif port == 5280 then + friendly_message = "check that Prosody or a BOSH connection manager " + .."is not already running"; + else + friendly_message = "this port is in use by another application"; + end + elseif err:match("permission") then + friendly_message = "Prosody does not have sufficient privileges to use this port"; + end + return friendly_message; +end + +prosody.events.add_handler("item-added/net-provider", function (event) + local item = event.item; + register_service(item.name, item); +end); +prosody.events.add_handler("item-removed/net-provider", function (event) + local item = event.item; + unregister_service(item.name, item); +end); + +local function duplicate_ssl_config(ssl_config) + local ssl_config = type(ssl_config) == "table" and ssl_config or {}; + + local _config = {}; + for k, v in pairs(ssl_config) do + _config[k] = v; + end + return _config; +end + +--- Public API + +function activate(service_name) + local service_info = services[service_name][1]; + if not service_info then + return nil, "Unknown service: "..service_name; + end + + local listener = service_info.listener; + + local config_prefix = (service_info.config_prefix or service_name).."_"; + if config_prefix == "_" then + config_prefix = ""; + end + + local bind_interfaces = config.get("*", config_prefix.."interfaces") + or config.get("*", config_prefix.."interface") -- COMPAT w/pre-0.9 + or (service_info.private and (config.get("*", "local_interfaces") or default_local_interfaces)) + or config.get("*", "interfaces") + or config.get("*", "interface") -- COMPAT w/pre-0.9 + or listener.default_interface -- COMPAT w/pre0.9 + or default_interfaces + bind_interfaces = set.new(type(bind_interfaces)~="table" and {bind_interfaces} or bind_interfaces); + + local bind_ports = config.get("*", config_prefix.."ports") + or service_info.default_ports + or {service_info.default_port + or listener.default_port -- COMPAT w/pre-0.9 + } + bind_ports = set.new(type(bind_ports) ~= "table" and { bind_ports } or bind_ports ); + + local mode, ssl = listener.default_mode or "*a"; + local hooked_ports = {}; + + for interface in bind_interfaces do + for port in bind_ports do + local port_number = tonumber(port); + if not port_number then + log("error", "Invalid port number specified for service '%s': %s", service_info.name, tostring(port)); + elseif #active_services:search(nil, interface, port_number) > 0 then + log("error", "Multiple services configured to listen on the same port ([%s]:%d): %s, %s", interface, port, active_services:search(nil, interface, port)[1][1].service.name or "<unnamed>", service_name or "<unnamed>"); + else + local err; + -- Create SSL context for this service/port + if service_info.encryption == "ssl" then + local ssl_config = duplicate_ssl_config((config.get("*", config_prefix.."ssl") and config.get("*", config_prefix.."ssl")[interface]) + or (config.get("*", config_prefix.."ssl") and config.get("*", config_prefix.."ssl")[port]) + or config.get("*", config_prefix.."ssl") + or (config.get("*", "ssl") and config.get("*", "ssl")[interface]) + or (config.get("*", "ssl") and config.get("*", "ssl")[port]) + or config.get("*", "ssl")); + -- add default entries for, or override ssl configuration + if ssl_config and service_info.ssl_config then + for key, value in pairs(service_info.ssl_config) do + if not service_info.ssl_config_override and not ssl_config[key] then + ssl_config[key] = value; + elseif service_info.ssl_config_override then + ssl_config[key] = value; + end + end + end + + ssl, err = certmanager.create_context(service_info.name.." port "..port, "server", ssl_config); + if not ssl then + log("error", "Error binding encrypted port for %s: %s", service_info.name, error_to_friendly_message(service_name, port_number, err) or "unknown error"); + end + end + if not err then + -- Start listening on interface+port + local handler, err = server.addserver(interface, port_number, listener, mode, ssl); + if not handler then + log("error", "Failed to open server port %d on %s, %s", port_number, interface, error_to_friendly_message(service_name, port_number, err)); + else + table.insert(hooked_ports, "["..interface.."]:"..port_number); + log("debug", "Added listening service %s to [%s]:%d", service_name, interface, port_number); + active_services:add(service_name, interface, port_number, { + server = handler; + service = service_info; + }); + end + end + end + end + end + log("info", "Activated service '%s' on %s", service_name, #hooked_ports == 0 and "no ports" or table.concat(hooked_ports, ", ")); + return true; +end + +function deactivate(service_name, service_info) + for name, interface, port, n, active_service + in active_services:iter(service_name or service_info and service_info.name, nil, nil, nil) do + if service_info == nil or active_service.service == service_info then + close(interface, port); + end + end + log("info", "Deactivated service '%s'", service_name or service_info.name); +end + +function register_service(service_name, service_info) + table.insert(services[service_name], service_info); + + if not active_services:get(service_name) then + log("debug", "No active service for %s, activating...", service_name); + local ok, err = activate(service_name); + if not ok then + log("error", "Failed to activate service '%s': %s", service_name, err or "unknown error"); + end + end + + fire_event("service-added", { name = service_name, service = service_info }); + return true; +end + +function unregister_service(service_name, service_info) + log("debug", "Unregistering service: %s", service_name); + local service_info_list = services[service_name]; + for i, service in ipairs(service_info_list) do + if service == service_info then + table.remove(service_info_list, i); + end + end + deactivate(nil, service_info); + if #service_info_list > 0 then -- Other services registered with this name + activate(service_name); -- Re-activate with the next available one + end + fire_event("service-removed", { name = service_name, service = service_info }); +end + +function close(interface, port) + local service, server = get_service_at(interface, port); + if not service then + return false, "port-not-open"; + end + server:close(); + active_services:remove(service.name, interface, port); + log("debug", "Removed listening service %s from [%s]:%d", service.name, interface, port); + return true; +end + +function get_service_at(interface, port) + local data = active_services:search(nil, interface, port)[1][1]; + return data.service, data.server; +end + +function get_service(service_name) + return (services[service_name] or {})[1]; +end + +function get_active_services(...) + return active_services; +end + +function get_registered_services() + return services; +end + +return _M; diff --git a/core/rostermanager.lua b/core/rostermanager.lua index 26a14256..5e06e3f7 100644 --- a/core/rostermanager.lua +++ b/core/rostermanager.lua @@ -1,34 +1,28 @@ +-- 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 mainlog = log; -local function log(type, message) - mainlog(type, "rostermanager", message); -end -local setmetatable = setmetatable; -local format = string.format; -local loadfile, setfenv, pcall = loadfile, setfenv, pcall; -local pairs, ipairs = pairs, ipairs; -local hosts = hosts; -require "util.datamanager" +local log = require "util.logger".init("rostermanager"); + +local pairs = pairs; +local tostring = tostring; + +local hosts = hosts; +local bare_sessions = bare_sessions; -local datamanager = datamanager; +local datamanager = require "util.datamanager" +local um_user_exists = require "core.usermanager".user_exists; local st = require "util.stanza"; module "rostermanager" ---[[function getroster(username, host) - return { - ["mattj@localhost"] = true, - ["tobias@getjabber.ath.cx"] = true, - ["waqas@getjabber.ath.cx"] = true, - ["thorns@getjabber.ath.cx"] = true, - ["idw@getjabber.ath.cx"] = true, - } - --return datamanager.load(username, host, "roster") or {}; -end]] - function add_to_roster(session, jid, item) if session.roster then local old_item = session.roster[jid]; @@ -60,20 +54,21 @@ function remove_from_roster(session, jid) end function roster_push(username, host, jid) - if hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster then + local roster = jid and jid ~= "pending" and hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster; + if roster then local item = hosts[host].sessions[username].roster[jid]; local stanza = st.iq({type="set"}); - stanza:tag("query", {xmlns = "jabber:iq:roster"}); + stanza:tag("query", {xmlns = "jabber:iq:roster", ver = tostring(roster[false].version or "1") }); if item then - stanza:tag("item", {jid = jid, subscription = item.subscription, name = item.name}); + stanza:tag("item", {jid = jid, subscription = item.subscription, name = item.name, ask = item.ask}); for group in pairs(item.groups) do stanza:tag("group"):text(group):up(); end else stanza:tag("item", {jid = jid, subscription = "remove"}); end - stanza:up(); - stanza:up(); + stanza:up(); -- move out from item + stanza:up(); -- move out from stanza -- stanza ready for _, session in pairs(hosts[host].sessions[username].sessions) do if session.interested then @@ -85,22 +80,242 @@ function roster_push(username, host, jid) end function load_roster(username, host) - if hosts[host] and hosts[host].sessions[username] then - local roster = hosts[host].sessions[username].roster; - if not roster then - roster = datamanager.load(username, host, "roster") or {}; - hosts[host].sessions[username].roster = roster; - end - return roster; + local jid = username.."@"..host; + 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: %s@%s", username, host); + else -- Attempt to load roster for non-loaded user + log("debug", "load_roster: loading for offline user: %s@%s", username, host); end - -- Attempt to load roster for non-loaded user + local data, err = datamanager.load(username, host, "roster"); + roster = data or {}; + if user then user.roster = roster; end + if not roster[false] then roster[false] = { broken = err or nil }; end + if roster[jid] then + roster[jid] = nil; + log("warn", "roster for %s has a self-contact", jid); + end + if not err then + hosts[host].events.fire_event("roster-load", username, host, roster); + end + return roster, err; end -function save_roster(username, host) - if hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster then - return datamanager.store(username, host, "roster", hosts[host].sessions[username].roster); +function save_roster(username, host, roster) + if not um_user_exists(username, host) then + log("debug", "not saving roster for %s@%s: the user doesn't exist", username, host); + return nil; + end + + log("debug", "save_roster: saving roster for %s@%s", username, host); + if not roster then + roster = hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster; + --if not roster then + -- --roster = load_roster(username, host); + -- return true; -- roster unchanged, no reason to save + --end end + if roster then + local metadata = roster[false]; + if not metadata then + metadata = {}; + roster[false] = metadata; + end + if metadata.version ~= true then + metadata.version = (metadata.version or 0) + 1; + end + if roster[false].broken then return nil, "Not saving broken roster" end + return datamanager.store(username, host, "roster", roster); + end + log("warn", "save_roster: user had no roster to save"); return nil; end -return _M;
\ No newline at end of file +function process_inbound_subscription_approval(username, host, jid) + local roster = load_roster(username, host); + local item = roster[jid]; + if item and item.ask then + if item.subscription == "none" then + item.subscription = "to"; + else -- subscription == from + item.subscription = "both"; + end + item.ask = nil; + return save_roster(username, host, roster); + end +end + +function process_inbound_subscription_cancellation(username, host, jid) + local roster = load_roster(username, host); + local item = roster[jid]; + local changed = nil; + if is_contact_pending_out(username, host, jid) then + item.ask = nil; + changed = true; + end + if item then + if item.subscription == "to" then + item.subscription = "none"; + changed = true; + elseif item.subscription == "both" then + item.subscription = "from"; + changed = true; + end + end + if changed then + return save_roster(username, host, roster); + end +end + +function process_inbound_unsubscribe(username, host, jid) + local roster = load_roster(username, host); + local item = roster[jid]; + local changed = nil; + if is_contact_pending_in(username, host, jid) then + roster.pending[jid] = nil; -- TODO maybe delete roster.pending if empty? + changed = true; + end + if item then + if item.subscription == "from" then + item.subscription = "none"; + changed = true; + elseif item.subscription == "both" then + item.subscription = "to"; + changed = true; + end + end + if changed then + return save_roster(username, host, roster); + end +end + +local function _get_online_roster_subscription(jidA, jidB) + local user = bare_sessions[jidA]; + local item = user and (user.roster[jidB] or { subscription = "none" }); + return item and item.subscription; +end +function is_contact_subscribed(username, host, jid) + do + local selfjid = username.."@"..host; + local subscription = _get_online_roster_subscription(selfjid, jid); + if subscription then return (subscription == "both" or subscription == "from"); end + local subscription = _get_online_roster_subscription(jid, selfjid); + if subscription then return (subscription == "both" or subscription == "to"); end + end + local roster, err = load_roster(username, host); + local item = roster[jid]; + return item and (item.subscription == "from" or item.subscription == "both"), err; +end + +function is_contact_pending_in(username, host, jid) + local roster = load_roster(username, host); + return roster.pending and roster.pending[jid]; +end +function set_contact_pending_in(username, host, jid, pending) + local roster = load_roster(username, host); + local item = roster[jid]; + if item and (item.subscription == "from" or item.subscription == "both") then + return; -- false + end + if not roster.pending then roster.pending = {}; end + roster.pending[jid] = true; + return save_roster(username, host, roster); +end +function is_contact_pending_out(username, host, jid) + local roster = load_roster(username, host); + local item = roster[jid]; + return item and item.ask; +end +function set_contact_pending_out(username, host, jid) -- subscribe + local roster = load_roster(username, host); + local item = roster[jid]; + if item and (item.ask or item.subscription == "to" or item.subscription == "both") then + return true; + end + if not item then + item = {subscription = "none", groups = {}}; + roster[jid] = item; + end + item.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) + local roster = load_roster(username, host); + local item = roster[jid]; + if not item then return false; end + if (item.subscription == "from" or item.subscription == "none") and not item.ask then + return true; + end + item.ask = nil; + if item.subscription == "both" then + item.subscription = "from"; + elseif item.subscription == "to" then + item.subscription = "none"; + end + return save_roster(username, host, roster); +end +function subscribed(username, host, jid) + if is_contact_pending_in(username, host, jid) then + local roster = load_roster(username, host); + local item = roster[jid]; + if not item then -- FIXME should roster item be auto-created? + item = {subscription = "none", groups = {}}; + roster[jid] = item; + end + if item.subscription == "none" then + item.subscription = "from"; + else -- subscription == to + item.subscription = "both"; + end + roster.pending[jid] = nil; + -- TODO maybe remove roster.pending if empty + return save_roster(username, host, roster); + end -- TODO else implement optional feature pre-approval (ask = subscribed) +end +function unsubscribed(username, host, jid) + local roster = load_roster(username, host); + local item = roster[jid]; + local pending = is_contact_pending_in(username, host, jid); + if pending then + roster.pending[jid] = nil; -- TODO maybe delete roster.pending if empty? + end + local subscribed; + if item then + if item.subscription == "from" then + item.subscription = "none"; + subscribed = true; + elseif item.subscription == "both" then + item.subscription = "to"; + subscribed = true; + end + 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) + local roster = load_roster(username, host); + local item = roster[jid]; + if item and (item.subscription == "none" or item.subscription == "from") then + item.ask = "subscribe"; + return save_roster(username, host, roster); + end +end + +--[[function process_outbound_subscription_approval(username, host, jid) + local roster = load_roster(username, host); + local item = roster[jid]; + if item and (item.subscription == "none" or item.subscription == "from" then + item.ask = "subscribe"; + return save_roster(username, host, roster); + end +end]] + + + +return _M; diff --git a/core/s2smanager.lua b/core/s2smanager.lua index 1d718305..06d3f2c9 100644 --- a/core/s2smanager.lua +++ b/core/s2smanager.lua @@ -1,178 +1,98 @@ +-- 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 socket = require "socket"; -local format = string.format; -local tostring, pairs, ipairs, getmetatable, print, newproxy, error, tonumber - = tostring, pairs, ipairs, getmetatable, print, newproxy, error, tonumber; -local connlisteners_get = require "net.connlisteners".get; -local wraptlsclient = require "net.server".wraptlsclient; -local modulemanager = require "core.modulemanager"; -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 md5_hash = require "util.hashes".md5; - -local dialback_secret = "This is very secret!!! Ha!"; +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 connect_host(from_host, to_host) -end - -function send_to_host(from_host, to_host, data) - if hosts[to_host] then - -- Write to connection - hosts[to_host].send(data); - log("debug", "stanza sent over s2s"); - 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 = { data }; - end -end - -function disconnect_host(host) - -end - -local open_sessions = 0; - function new_incoming(conn) - local session = { conn = conn, priority = 0, type = "s2sin_unauthed", direction = "incoming" }; - if true then - session.trace = newproxy(true); - getmetatable(session.trace).__gc = function () open_sessions = open_sessions - 1; print("s2s session got collected, now "..open_sessions.." s2s sessions are allocated") end; - end - open_sessions = open_sessions + 1; - local w = conn.write; - session.send = function (t) w(tostring(t)); end + local session = { conn = conn, type = "s2sin_unauthed", direction = "incoming", hosts = {} }; + session.log = logger_init("s2sin"..tostring(session):match("[a-f0-9]+$")); + incoming_s2s[session] = true; return session; end function new_outgoing(from_host, to_host) - local host_session = { to_host = to_host, from_host = from_host, notopen = true, type = "s2sout_unauthed", direction = "outgoing" }; - hosts[to_host] = host_session; - - local cl = connlisteners_get("xmppserver"); - - local conn, handler = socket.tcp() - --FIXME: Below parameters (ports/ip) are incorrect (use SRV) - conn:connect(to_host, 5269); - conn = wraptlsclient(cl, conn, to_host, 5269, 0, 1, hosts[from_host].ssl_ctx ); - host_session.conn = conn; - - -- Register this outgoing connection so that xmppserver_listener knows about it - -- otherwise it will assume it is a new incoming connection - cl.register_outgoing(conn, host_session); - - do - local conn_name = "s2sout"..tostring(conn):match("[a-f0-9]*$"); - host_session.log = logger_init(conn_name); - end - - local w = conn.write; - host_session.send = function (t) w(tostring(t)); end - - conn.write(format([[<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' from='%s' to='%s' version='1.0'>]], from_host, to_host)); - - return host_session; + local host_session = { to_host = to_host, from_host = from_host, host = from_host, + notopen = true, type = "s2sout_unauthed", direction = "outgoing" }; + hosts[from_host].s2sout[to_host] = host_session; + local conn_name = "s2sout"..tostring(host_session):match("[a-f0-9]*$"); + host_session.log = logger_init(conn_name); + return host_session; end -function streamopened(session, attr) - session.log("debug", "s2s stream opened"); - local send = session.send; - - session.version = tonumber(attr.version) or 0; - if session.version >= 1.0 and not (attr.to and attr.from) then - print("to: "..tostring(attr.to).." from: "..tostring(attr.from)); - --error(session.to_host.." failed to specify 'to' or 'from' hostname as per RFC"); - log("warn", (session.to_host or "(unknown)").." failed to specify 'to' or 'from' hostname as per RFC"); - end - - if session.direction == "incoming" then - -- Send a reply stream header - - for k,v in pairs(attr) do print("", tostring(k), ":::", tostring(v)); end - - session.to_host = attr.to; - session.from_host = attr.from; - - session.streamid = uuid_gen(); - print(session, session.from_host, "incoming s2s stream opened"); - send("<?xml version='1.0'?>"); - send(format("<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' id='%s' from='%s'>", session.streamid, session.to_host)); - if session.from_host then - -- Need to perform dialback to check identity - print("to: "..tostring(attr.to).." from: "..tostring(attr.from)); - print("Need to do dialback here you know!!"); - 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 session.dialback_verifying then - -- generate dialback key - if not attr.id then error("stream response did not give us a streamid!!!"); end - session.streamid = attr.id; - session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host); - session.send(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"); - else - mark_connected(session); +local resting_session = { -- Resting, not dead + destroyed = true; + type = "s2s_destroyed"; + open_stream = function (session) + session.log("debug", "Attempt to open stream on resting session"); + end; + close = function (session) + session.log("debug", "Attempt to close already-closed session"); + end; + filter = function (type, data) return data; end; + }; resting_session.__index = resting_session; + +function retire_session(session, reason) + local log = session.log or log; + for k in pairs(session) do + if k ~= "log" and k ~= "id" and k ~= "conn" then + session[k] = nil; end end - --[[ - local features = {}; - modulemanager.fire_event("stream-features-s2s", session, features); - - send("<stream:features>"); - - for _, feature in ipairs(features) do - send(tostring(feature)); - end - - send("</stream:features>");]] - log("info", "s2s stream opened successfully"); - session.notopen = nil; -end -function generate_dialback(id, to, from) - return md5_hash(id..to..from..dialback_secret); -- FIXME: See XEP-185 and XEP-220 -end + session.destruction_reason = reason; -function verify_dialback(id, to, from, key) - return key == generate_dialback(id, to, from); + function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); end + function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end + return setmetatable(session, resting_session); end -function make_authenticated(session) - if session.type == "s2sout_unauthed" then - session.type = "s2sout"; - elseif session.type == "s2sin_unauthed" then - session.type = "s2sin"; - else - return false; - end - session.log("info", "connection is now authenticated"); +function destroy_session(session, reason) + if session.destroyed then return; end + (session.log or log)("debug", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host)..(reason and (": "..reason) or "")); - mark_connected(session); + if session.direction == "outgoing" then + hosts[session.from_host].s2sout[session.to_host] = nil; + session:bounce_sendq(reason); + elseif session.direction == "incoming" then + incoming_s2s[session] = nil; + end - return true; -end - -function mark_connected(session) - local sendq, send = session.sendq, session.send; - if sendq then - session.log("debug", "sending queued stanzas across new connection"); - for _, data in ipairs(sendq) do - session.log("debug", "sending: %s", tostring(data)); - send(data); + local event_data = { session = session, reason = reason }; + if session.type == "s2sout" then + fire_event("s2sout-destroyed", event_data); + if hosts[session.from_host] then + hosts[session.from_host].events.fire_event("s2sout-destroyed", event_data); + end + elseif session.type == "s2sin" then + fire_event("s2sin-destroyed", event_data); + if hosts[session.to_host] then + hosts[session.to_host].events.fire_event("s2sin-destroyed", event_data); end end + + retire_session(session, reason); -- Clean session until it is GC'd + return true; end -return _M;
\ No newline at end of file +return _M; diff --git a/core/servermanager.lua b/core/servermanager.lua deleted file mode 100644 index aba3f5d5..00000000 --- a/core/servermanager.lua +++ /dev/null @@ -1,22 +0,0 @@ - -local st = require "util.stanza"; -local send = require "core.sessionmanager".send_to_session; -local xmlns_stanzas ='urn:ietf:params:xml:ns:xmpp-stanzas'; - -require "modulemanager" - --- Handle stanzas that were addressed to the server (whether they came from c2s, s2s, etc.) -function handle_stanza(origin, stanza) - -- Use plugins - if not modulemanager.handle_stanza(origin, stanza) then - if stanza.name == "iq" then - if stanza.attr.type ~= "result" and stanza.attr.type ~= "error" then - send(origin, st.error_reply(stanza, "cancel", "service-unavailable")); - end - elseif stanza.name == "message" then - send(origin, st.error_reply(stanza, "cancel", "service-unavailable")); - elseif stanza.name ~= "presence" then - error("Unknown stanza"); - end - end -end diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 2b7659d2..98ead07f 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -1,118 +1,215 @@ +-- 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 = tonumber, tostring; -local ipairs, pairs, print, next= ipairs, pairs, print, next; -local collectgarbage = collectgarbage; -local m_random = import("math", "random"); -local format = import("string", "format"); +local tostring, setmetatable = tostring, setmetatable; +local pairs, next= pairs, next; local hosts = hosts; -local sessions = sessions; +local full_sessions = full_sessions; +local bare_sessions = bare_sessions; -local modulemanager = require "core.modulemanager"; -local log = require "util.logger".init("sessionmanager"); -local error = error; -local uuid_generate = require "util.uuid".generate; +local logger = require "util.logger"; +local log = logger.init("sessionmanager"); local rm_load_roster = require "core.rostermanager".load_roster; +local config_get = require "core.configmanager".get; +local resourceprep = require "util.encodings".stringprep.resourceprep; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local uuid_generate = require "util.uuid".generate; -local newproxy = newproxy; -local getmetatable = getmetatable; +local initialize_filters = require "util.filters".initialize; +local gettime = require "socket".gettime; module "sessionmanager" -local open_sessions = 0; - function new_session(conn) - local session = { conn = conn, priority = 0, type = "c2s_unauthed" }; - if true then - session.trace = newproxy(true); - getmetatable(session.trace).__gc = function () open_sessions = open_sessions - 1; print("Session got collected, now "..open_sessions.." sessions are allocated") end; - end - open_sessions = open_sessions + 1; + local session = { conn = conn, type = "c2s_unauthed", conntime = gettime() }; + local filter = initialize_filters(session); local w = conn.write; - session.send = function (t) w(tostring(t)); end - return session; -end - -function destroy_session(session) - session.log("info", "Destroying session"); - if session.host and session.username then - if session.resource then - hosts[session.host].sessions[session.username].sessions[session.resource] = nil; + session.send = function (t) + if t.name then + t = filter("stanzas/out", t); end - if hosts[session.host] and hosts[session.host].sessions[session.username] then - if not next(hosts[session.host].sessions[session.username].sessions) then - log("debug", "All resources of %s are now offline", session.username); - hosts[session.host].sessions[session.username] = nil; + if t then + t = filter("bytes/out", tostring(t)); + if t then + return w(conn, t); end end end - session.conn = nil; - session.disconnect = nil; + session.ip = conn:ip(); + local conn_name = "c2s"..tostring(session):match("[a-f0-9]+$"); + session.log = logger.init(conn_name); + + return session; +end + +local resting_session = { -- Resting, not dead + destroyed = true; + type = "c2s_destroyed"; + close = function (session) + session.log("debug", "Attempt to close already-closed session"); + end; + filter = function (type, data) return data; end; + }; resting_session.__index = resting_session; + +function retire_session(session) + local log = session.log or log; for k in pairs(session) do - if k ~= "trace" 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)); 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 send_to_session(session, data) - log("debug", "Sending: %s", tostring(data)); - session.conn.write(tostring(data)); +function destroy_session(session, err) + (session.log or log)("debug", "Destroying session for %s (%s@%s)%s", session.full_jid or "(unknown)", session.username or "(unknown)", 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 + 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(host_session.sessions[session.username].sessions) then + log("debug", "All resources of %s are now offline", session.username); + host_session.sessions[session.username] = nil; + bare_sessions[session.username..'@'..session.host] = nil; + end + + host_session.events.fire_event("resource-unbind", {session=session, error=err}); + end + + retire_session(session); end function make_authenticated(session, username) + username = nodeprep(username); + if not username or #username == 0 then return nil, "Invalid username"; end session.username = username; if session.type == "c2s_unauthed" then session.type = "c2s"; end + session.log("info", "Authenticated as %s@%s", username or "(unknown)", session.host or "(unknown)"); return true; end +-- returns true, nil on success +-- returns nil, err_type, err, err_message on failure function bind_resource(session, resource) - if not session.username then return false, "auth"; end - if session.resource then return false, "constraint"; end -- We don't support binding multiple resources - resource = resource or uuid_generate(); + if not session.username then return nil, "auth", "not-authorized", "Cannot bind resource before authentication"; end + if session.resource then return nil, "cancel", "already-bound", "Cannot bind multiple resources on a single connection"; end + -- We don't support binding multiple resources + + resource = 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 - hosts[session.host].sessions[session.username] = { sessions = {} }; + local sessions = { sessions = {} }; + hosts[session.host].sessions[session.username] = sessions; + bare_sessions[session.username..'@'..session.host] = sessions; else - if hosts[session.host].sessions[session.username].sessions[resource] then + local sessions = hosts[session.host].sessions[session.username].sessions; + if sessions[resource] then -- Resource conflict - return false, "conflict"; -- TODO kick old resource + local policy = config_get(session.host, "conflict_resolve"); + local increment; + if policy == "random" then + resource = uuid_generate(); + increment = true; + elseif policy == "increment" then + increment = true; -- TODO ping old resource + elseif policy == "kick_new" then + return nil, "cancel", "conflict", "Resource already exists"; + else -- if policy == "kick_old" then + sessions[resource]:close { + condition = "conflict"; + text = "Replaced by new connection"; + }; + if not next(sessions) then + hosts[session.host].sessions[session.username] = { sessions = sessions }; + bare_sessions[session.username.."@"..session.host] = hosts[session.host].sessions[session.username]; + end + end + if increment and sessions[resource] then + local count = 1; + while sessions[resource.."#"..count] do + count = count + 1; + end + resource = resource.."#"..count; + 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 + full_sessions[session.full_jid] = nil; + hosts[session.host].sessions[session.username].sessions[resource] = nil; + session.full_jid = nil; + session.resource = nil; + if next(bare_sessions[session.username..'@'..session.host].sessions) == nil then + bare_sessions[session.username..'@'..session.host] = nil; + hosts[session.host].sessions[session.username] = nil; + end + session.log("error", "Roster loading failed: %s", err); + return nil, "cancel", "internal-server-error", "Error loading roster"; + end - session.roster = rm_load_roster(session.username, session.host); + hosts[session.host].events.fire_event("resource-bind", {session=session}); return true; end -function streamopened(session, attr) - local send = session.send; - session.host = attr.to or error("Client failed to specify destination hostname"); - session.version = tonumber(attr.version) or 0; - session.streamid = m_random(1000000, 99999999); - print(session, session.host, "Client opened stream"); - 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'>", session.streamid, session.host)); - - local features = {}; - modulemanager.fire_event("stream-features", session, features); - - send("<stream:features>"); - - for _, feature in ipairs(features) do - send_to_session(session, tostring(feature)); - end - - send("</stream:features>"); - log("info", "Stream opened successfully"); - session.notopen = nil; +function send_to_available_resources(user, host, stanza) + local jid = user.."@"..host; + local count = 0; + local user = bare_sessions[jid]; + if user then + for k, session in pairs(user.sessions) do + if session.presence then + session.send(stanza); + count = count + 1; + end + end + end + return count; +end + +function send_to_interested_resources(user, host, stanza) + local jid = user.."@"..host; + local count = 0; + local user = bare_sessions[jid]; + if user then + for k, session in pairs(user.sessions) do + if session.interested then + session.send(stanza); + count = count + 1; + end + end + end + return count; end -return _M;
\ No newline at end of file +return _M; diff --git a/core/stanza_dispatch.lua b/core/stanza_dispatch.lua deleted file mode 100644 index e76d6ddd..00000000 --- a/core/stanza_dispatch.lua +++ /dev/null @@ -1,149 +0,0 @@ - -require "util.stanza" - -local st = stanza; - -local t_concat = table.concat; -local format = string.format; - -function init_stanza_dispatcher(session) - local iq_handlers = {}; - - local session_log = session.log; - local log = function (type, msg) session_log(type, "stanza_dispatcher", msg); end - local send = session.send; - local send_to; - do - local _send_to = session.send_to; - send_to = function (...) _send_to(session, ...); end - end - - iq_handlers["jabber:iq:auth"] = - function (stanza) - local username = stanza.tags[1]:child_with_name("username"); - local password = stanza.tags[1]:child_with_name("password"); - local resource = stanza.tags[1]:child_with_name("resource"); - if not (username and password and resource) then - local reply = st.reply(stanza); - send(reply:query("jabber:iq:auth") - :tag("username"):up() - :tag("password"):up() - :tag("resource"):up()); - return true; - else - username, password, resource = t_concat(username), t_concat(password), t_concat(resource); - local reply = st.reply(stanza); - require "core.usermanager" - if usermanager.validate_credentials(session.host, username, password) then - -- Authentication successful! - session.username = username; - session.resource = resource; - session.full_jid = username.."@"..session.host.."/"..session.resource; - if not hosts[session.host].sessions[username] then - hosts[session.host].sessions[username] = { sessions = {} }; - end - hosts[session.host].sessions[username].sessions[resource] = session; - send(st.reply(stanza)); - return true; - else - local reply = st.reply(stanza); - reply.attr.type = "error"; - reply:tag("error", { code = "401", type = "auth" }) - :tag("not-authorized", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" }); - send(reply); - return true; - end - end - - end - - iq_handlers["jabber:iq:roster"] = - function (stanza) - if stanza.attr.type == "get" then - session.roster = session.roster or rostermanager.getroster(session.username, session.host); - if session.roster == false then - send(st.reply(stanza) - :tag("error", { type = "wait" }) - :tag("internal-server-error", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})); - return true; - else session.roster = session.roster or {}; - end - local roster = st.reply(stanza) - :query("jabber:iq:roster"); - for jid in pairs(session.roster) do - roster:tag("item", { jid = jid, subscription = "none" }):up(); - end - send(roster); - return true; - end - end - - - return function (stanza) - log("info", "--> "..tostring(stanza)); - if (not stanza.attr.to) or (hosts[stanza.attr.to] and hosts[stanza.attr.to].type == "local") then - if stanza.name == "iq" then - if not stanza.tags[1] then log("warn", "<iq> without child is invalid"); return; end - if not stanza.attr.id then log("warn", "<iq> without id attribute is invalid"); end - local xmlns = (stanza.tags[1].attr and stanza.tags[1].attr.xmlns) or nil; - if not xmlns then log("warn", "Child of <iq> has no xmlns - invalid"); return; end - if (((not stanza.attr.to) or stanza.attr.to == session.host or stanza.attr.to:match("@[^/]+$")) and (stanza.attr.type == "get" or stanza.attr.type == "set")) then -- Stanza sent to us - if iq_handlers[xmlns] then - if iq_handlers[xmlns](stanza) then return; end; - end - log("warn", "Unhandled namespace: "..xmlns); - send(format("<iq type='error' id='%s'><error type='cancel'><service-unavailable/></error></iq>", stanza.attr.id)); - return; - end - end - if not session.username then log("warn", "Attempt to use an unauthed stream!"); return; end - if stanza.name == "presence" then - if session.roster then - local initial_presence = not session.last_presence; - session.last_presence = stanza; - - -- Broadcast presence and probes - local broadcast = st.presence({ from = session.full_jid, type = stanza.attr.type }); - --local probe = st.presence { from = broadcast.attr.from, type = "probe" }; - - for child in stanza:childtags() do - broadcast:add_child(child); - end - for contact_jid in pairs(session.roster) do - broadcast.attr.to = contact_jid; - send_to(contact_jid, broadcast); - if initial_presence then - local node, host = jid.split(contact_jid); - if hosts[host] and hosts[host].type == "local" then - local contact = hosts[host].sessions[node] - if contact then - local pres = st.presence { to = session.full_jid }; - for resource, contact_session in pairs(contact.sessions) do - if contact_session.last_presence then - pres.tags = contact_session.last_presence.tags; - pres.attr.from = contact_session.full_jid; - send(pres); - end - end - end - --FIXME: Do we send unavailable if they are offline? - else - probe.attr.to = contact; - send_to(contact, probe); - end - end - end - - -- Probe for our contacts' presence - end - end - elseif session.username then - --end - --if stanza.attr.to and ((not hosts[stanza.attr.to]) or hosts[stanza.attr.to].type ~= "local") then - -- Need to route stanza - stanza.attr.from = session.username.."@"..session.host; - session:send_to(stanza.attr.to, stanza); - end - end - -end diff --git a/core/stanza_router.lua b/core/stanza_router.lua index 905de6e3..94753678 100644 --- a/core/stanza_router.lua +++ b/core/stanza_router.lua @@ -1,260 +1,223 @@ - --- The code in this file should be self-explanatory, though the logic is horrible --- for more info on that, see doc/stanza_routing.txt, which attempts to condense --- the rules from the RFCs (mainly 3921) - -require "core.servermanager" +-- 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 log = require "util.logger".init("stanzarouter") +local hosts = _G.prosody.hosts; +local tostring = tostring; local st = require "util.stanza"; -local send = require "core.sessionmanager".send_to_session; -local send_s2s = require "core.s2smanager".send_to_host; -local user_exists = require "core.usermanager".user_exists; +local jid_split = require "util.jid".split; +local jid_prepped_split = require "util.jid".prepped_split; -local s2s_verify_dialback = require "core.s2smanager".verify_dialback; -local s2s_make_authenticated = require "core.s2smanager".make_authenticated; -local format = string.format; -local tostring = tostring; +local full_sessions = _G.prosody.full_sessions; +local bare_sessions = _G.prosody.bare_sessions; -local jid_split = require "util.jid".split; -local print = print; +local core_post_stanza, core_process_stanza, core_route_stanza; + +function deprecated_warning(f) + _G[f] = function(...) + log("warn", "Using the global %s() is deprecated, use module:send() or prosody.%s(). %s", f, f, debug.traceback()); + return prosody[f](...); + end +end +deprecated_warning"core_post_stanza"; +deprecated_warning"core_process_stanza"; +deprecated_warning"core_route_stanza"; +local function handle_unhandled_stanza(host, origin, stanza) + local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns or "jabber:client", origin.type; + if name == "iq" and xmlns == "jabber:client" then + if stanza.attr.type == "get" or stanza.attr.type == "set" then + xmlns = stanza.tags[1].attr.xmlns or "jabber:client"; + log("debug", "Stanza of type %s from %s has xmlns: %s", name, origin_type, xmlns); + else + log("debug", "Discarding %s from %s of type: %s", name, origin_type, stanza.attr.type); + return true; + end + end + if stanza.attr.xmlns == nil and origin.send then + log("debug", "Unhandled %s stanza: %s; xmlns=%s", origin.type, stanza.name, xmlns); -- we didn't handle it + if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + end + elseif not((name == "features" or name == "error") and xmlns == "http://etherx.jabber.org/streams") then -- FIXME remove check once we handle S2S features + log("warn", "Unhandled %s stream element or stanza: %s; xmlns=%s: %s", origin.type, stanza.name, xmlns, tostring(stanza)); -- we didn't handle it + origin:close("unsupported-stanza-type"); + end +end + +local iq_types = { set=true, get=true, result=true, error=true }; function core_process_stanza(origin, stanza) - log("debug", "Received: "..tostring(stanza)) + (origin.log or log)("debug", "Received[%s]: %s", origin.type, stanza:top_tag()) + -- TODO verify validity of stanza (as well as JID validity) - if stanza.name == "iq" and not(#stanza.tags == 1 and stanza.tags[1].attr.xmlns) then - if stanza.attr.type == "set" or stanza.attr.type == "get" then - error("Invalid IQ"); - elseif #stanza.tags > 1 and not(stanza.attr.type == "error" or stanza.attr.type == "result") then - error("Invalid IQ"); + if stanza.attr.type == "error" and #stanza.tags == 0 then return; end -- TODO invalid stanza, log + if stanza.name == "iq" then + if not stanza.attr.id then stanza.attr.id = ""; end -- COMPAT Jabiru doesn't send the id attribute on roster requests + if not iq_types[stanza.attr.type] or ((stanza.attr.type == "set" or stanza.attr.type == "get") and (#stanza.tags ~= 1)) then + origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid IQ type or incorrect number of children")); + return; end end - if origin.type == "c2s" and not origin.full_jid - and not(stanza.name == "iq" and stanza.tags[1].name == "bind" - and stanza.tags[1].attr.xmlns == "urn:ietf:params:xml:ns:xmpp-bind") then - error("Client MUST bind resource after auth"); - end + if origin.type == "c2s" and not stanza.attr.xmlns then + if not origin.full_jid + and not(stanza.name == "iq" and stanza.attr.type == "set" and stanza.tags[1] and stanza.tags[1].name == "bind" + and stanza.tags[1].attr.xmlns == "urn:ietf:params:xml:ns:xmpp-bind") then + -- authenticated client isn't bound and current stanza is not a bind request + if stanza.attr.type ~= "result" and stanza.attr.type ~= "error" then + origin.send(st.error_reply(stanza, "auth", "not-authorized")); -- FIXME maybe allow stanzas to account or server + end + return; + end - local to = stanza.attr.to; - -- TODO also, stazas should be returned to their original state before the function ends - if origin.type == "c2s" then - stanza.attr.from = origin.full_jid; -- quick fix to prevent impersonation (FIXME this would be incorrect when the origin is not c2s) + -- TODO also, stanzas should be returned to their original state before the function ends + stanza.attr.from = origin.full_jid; end - - if not to then - core_handle_stanza(origin, stanza); - elseif hosts[to] and hosts[to].type == "local" then - core_handle_stanza(origin, stanza); - elseif stanza.name == "iq" and not select(3, jid_split(to)) then - core_handle_stanza(origin, stanza); - elseif origin.type == "c2s" or origin.type == "s2sin" then - core_route_stanza(origin, stanza); - end -end - --- This function handles stanzas which are not routed any further, --- that is, they are handled by this server -function core_handle_stanza(origin, stanza) - -- Handlers - if origin.type == "c2s" or origin.type == "c2s_unauthed" then - local session = origin; - - if stanza.name == "presence" and origin.roster then - if stanza.attr.type == nil or stanza.attr.type == "available" or stanza.attr.type == "unavailable" then - for jid in pairs(origin.roster) do -- broadcast to all interested contacts - local subscription = origin.roster[jid].subscription; - if subscription == "both" or subscription == "from" then - stanza.attr.to = jid; - core_route_stanza(origin, stanza); - end - end - --[[local node, host = jid_split(stanza.attr.from); - for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast to all resources - if res.full_jid then - res = user.sessions[k]; - break; - end - end]] - if not origin.presence then -- presence probes on initial presence - local probe = st.presence({from = origin.full_jid, type = "probe"}); - for jid in pairs(origin.roster) do - local subscription = origin.roster[jid].subscription; - if subscription == "both" or subscription == "to" then - probe.attr.to = jid; - core_route_stanza(origin, probe); - end - end + local to, xmlns = stanza.attr.to, stanza.attr.xmlns; + local from = stanza.attr.from; + local node, host, resource; + local from_node, from_host, from_resource; + local to_bare, from_bare; + if to then + if full_sessions[to] or bare_sessions[to] or hosts[to] then + node, host = jid_split(to); -- TODO only the host is needed, optimize + else + node, host, resource = jid_prepped_split(to); + if not host then + log("warn", "Received stanza with invalid destination JID: %s", to); + if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then + origin.send(st.error_reply(stanza, "modify", "jid-malformed", "The destination address is invalid: "..to)); end - origin.presence = stanza; - stanza.attr.to = nil; -- reset it - else - -- TODO error, bad type + return; end - else - log("debug", "Routing stanza to local"); - handle_stanza(session, stanza); + to_bare = node and (node.."@"..host) or host; -- bare JID + if resource then to = to_bare.."/"..resource; else to = to_bare; end + stanza.attr.to = to; end - elseif origin.type == "s2sin_unauthed" or origin.type == "s2sin" then - if stanza.attr.xmlns == "jabber:server:dialback" then - if stanza.name == "verify" then - -- We are being asked to verify the key, to ensure it was generated by us - log("debug", "verifying dialback key..."); - local attr = stanza.attr; - print(tostring(attr.to), tostring(attr.from)) - print(tostring(origin.to_host), tostring(origin.from_host)) - -- FIXME: Grr, ejabberd breaks this one too?? it is black and white in XEP-220 example 34 - --if attr.from ~= origin.to_host then error("invalid-from"); end - local type = "invalid"; - if s2s_verify_dialback(attr.id, attr.from, attr.to, stanza[1]) then - type = "valid" - end - origin.send(format("<db:verify from='%s' to='%s' id='%s' type='%s'>%s</db:verify>", attr.to, attr.from, attr.id, type, stanza[1])); - elseif stanza.name == "result" and origin.type == "s2sin_unauthed" then - -- he wants to be identified through dialback - -- We need to check the key with the Authoritative server - local attr = stanza.attr; - origin.from_host = attr.from; - origin.to_host = attr.to; - origin.dialback_key = stanza[1]; - log("debug", "asking %s if key %s belongs to them", attr.from, stanza[1]); - send_s2s(attr.to, attr.from, format("<db:verify from='%s' to='%s' id='%s'>%s</db:verify>", attr.to, attr.from, origin.streamid, stanza[1])); - hosts[attr.from].dialback_verifying = origin; + end + if from and not origin.full_jid then + -- We only stamp the 'from' on c2s stanzas, so we still need to check validity + from_node, from_host, from_resource = jid_prepped_split(from); + if not from_host then + log("warn", "Received stanza with invalid source JID: %s", from); + if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then + origin.send(st.error_reply(stanza, "modify", "jid-malformed", "The source address is invalid: "..from)); end + return; end - elseif origin.type == "s2sout_unauthed" or origin.type == "s2sout" then - if stanza.attr.xmlns == "jabber:server:dialback" then - if stanza.name == "result" then - if stanza.attr.type == "valid" then - s2s_make_authenticated(origin); - else - -- FIXME - error("dialback failed!"); - end - elseif stanza.name == "verify" and origin.dialback_verifying then - local valid; - local attr = stanza.attr; - if attr.type == "valid" then - s2s_make_authenticated(origin.dialback_verifying); - valid = "valid"; + from_bare = from_node and (from_node.."@"..from_host) or from_host; -- bare JID + if from_resource then from = from_bare.."/"..from_resource; else from = from_bare; end + stanza.attr.from = from; + end + + 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); + 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); + else + local h = hosts[stanza.attr.to or origin.host or origin.to_host]; + if h then + local event; + if xmlns == nil then + if stanza.name == "iq" and (stanza.attr.type == "set" or stanza.attr.type == "get") then + event = "stanza/iq/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name; else - -- Warn the original connection that is was not verified successfully - log("warn", "dialback for "..(origin.dialback_verifying.from_host or "(unknown)").." failed"); - valid = "invalid"; + event = "stanza/"..stanza.name; end - origin.dialback_verifying.send(format("<db:result from='%s' to='%s' id='%s' type='%s'>%s</db:result>", attr.from, attr.to, attr.id, valid, origin.dialback_verifying.dialback_key)); + else + event = "stanza/"..xmlns..":"..stanza.name; end + if h.events.fire_event(event, {origin = origin, stanza = stanza}) then return; end end - else - log("warn", "Unhandled origin: %s", origin.type); + if host and not hosts[host] then host = nil; end -- COMPAT: workaround for a Pidgin bug which sets 'to' to the SRV result + handle_unhandled_stanza(host or origin.host or origin.to_host, origin, stanza); end end --- TODO: Does this function belong here? -function is_authorized_to_see_presence(origin, username, host) - local roster = datamanager.load(username, host, "roster") or {}; - local item = roster[origin.username.."@"..origin.host]; - return item and (item.subscription == "both" or item.subscription == "from"); -end - -function core_route_stanza(origin, stanza) - -- Hooks - --- ...later - - -- Deliver +function core_post_stanza(origin, stanza, preevents) local to = stanza.attr.to; local node, host, resource = jid_split(to); + local to_bare = node and (node.."@"..host) or host; -- bare JID - if stanza.name == "presence" and stanza.attr.type == "probe" then resource = nil; end - - local host_session = hosts[host] - if host_session and host_session.type == "local" then - -- Local host - local user = host_session.sessions[node]; - if user then - local res = user.sessions[resource]; - if not res then - -- if we get here, resource was not specified or was unavailable - if stanza.name == "presence" then - if stanza.attr.type == "probe" then - if is_authorized_to_see_presence(origin, node, host) then - for k in pairs(user.sessions) do -- return presence for all resources - if user.sessions[k].presence then - local pres = user.sessions[k].presence; - pres.attr.to = origin.full_jid; - pres.attr.from = user.sessions[k].full_jid; - send(origin, pres); - pres.attr.to = nil; - pres.attr.from = nil; - end - end - else - send(origin, st.presence({from = user.."@"..host, to = origin.username.."@"..origin.host, type = "unsubscribed"})); - end - else - for k in pairs(user.sessions) do -- presence broadcast to all user resources - if user.sessions[k].full_jid then - stanza.attr.to = user.sessions[k].full_jid; - send(user.sessions[k], stanza); - end - end - end - elseif stanza.name == "message" then -- select a resource to recieve message - for k in pairs(user.sessions) do - if user.sessions[k].full_jid then - res = user.sessions[k]; - break; - end - end - -- TODO find resource with greatest priority - send(res, stanza); - else - -- TODO send IQ error - end - else - -- User + resource is online... - stanza.attr.to = res.full_jid; - send(res, stanza); -- Yay \o/ - end + local to_type, to_self; + if node then + if resource then + to_type = '/full'; else - -- user not online - if user_exists(node, host) then - if stanza.name == "presence" then - if stanza.attr.type == "probe" and is_authorized_to_see_presence(origin, node, host) then -- FIXME what to do for not c2s? - -- TODO send last recieved unavailable presence - else - -- TODO send unavailable presence - end - elseif stanza.name == "message" then - -- TODO send message error, or store offline messages - elseif stanza.name == "iq" then - -- TODO send IQ error - end - else -- user does not exist - -- TODO we would get here for nodeless JIDs too. Do something fun maybe? Echo service? Let plugins use xmpp:server/resource addresses? - if stanza.name == "presence" then - if stanza.attr.type == "probe" then - send(origin, st.presence({from = user.."@"..host, to = origin.username.."@"..origin.host, type = "unsubscribed"})); - end - -- else ignore - else - send(origin, st.error_reply(stanza, "cancel", "service-unavailable")); - end + to_type = '/bare'; + if node == origin.username and host == origin.host then + stanza.attr.to = nil; + to_self = true; end end - elseif origin.type == "c2s" then - -- Remote host - --stanza.attr.xmlns = "jabber:server"; - stanza.attr.xmlns = nil; - log("debug", "sending s2s stanza: %s", tostring(stanza)); - send_s2s(origin.host, host, stanza); else - log("warn", "received stanza from unhandled connection type: %s", origin.type); + if host then + to_type = '/host'; + else + to_type = '/bare'; + to_self = true; + end + end + + local event_data = {origin=origin, stanza=stanza}; + if preevents then -- c2s connection + if hosts[origin.host].events.fire_event('pre-'..stanza.name..to_type, event_data) then return; end -- do preprocessing + end + local h = hosts[to_bare] or hosts[host or origin.host]; + if h then + if h.events.fire_event(stanza.name..to_type, event_data) then return; end -- do processing + if to_self and h.events.fire_event(stanza.name..'/self', event_data) then return; end -- do processing + handle_unhandled_stanza(h.host, origin, stanza); + else + core_route_stanza(origin, stanza); end - stanza.attr.to = to; -- reset end -function handle_stanza_toremote(stanza) - log("error", "Stanza bound for remote host, but s2s is not implemented"); +function core_route_stanza(origin, stanza) + local node, host, resource = jid_split(stanza.attr.to); + local from_node, from_host, from_resource = jid_split(stanza.attr.from); + + -- 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); + 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)); + else + local xmlns = stanza.attr.xmlns; + stanza.attr.xmlns = nil; + 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 + 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 + end end +prosody.core_process_stanza = core_process_stanza; +prosody.core_post_stanza = core_post_stanza; +prosody.core_route_stanza = core_route_stanza; diff --git a/core/storagemanager.lua b/core/storagemanager.lua new file mode 100644 index 00000000..1c82af6d --- /dev/null +++ b/core/storagemanager.lua @@ -0,0 +1,135 @@ + +local error, type, pairs = error, type, pairs; +local setmetatable = setmetatable; + +local config = require "core.configmanager"; +local datamanager = require "util.datamanager"; +local modulemanager = require "core.modulemanager"; +local multitable = require "util.multitable"; +local hosts = hosts; +local log = require "util.logger".init("storagemanager"); + +local prosody = prosody; + +module("storagemanager") + +local olddm = {}; -- maintain old datamanager, for backwards compatibility +for k,v in pairs(datamanager) do olddm[k] = v; end +_M.olddm = olddm; + +local null_storage_method = function () return false, "no data storage active"; end +local null_storage_driver = setmetatable( + { + name = "null", + open = function (self) return self; end + }, { + __index = function (self, method) + return null_storage_method; + end + } +); + +local stores_available = multitable.new(); + +function initialize_host(host) + local host_session = hosts[host]; + host_session.events.add_handler("item-added/storage-provider", function (event) + local item = event.item; + stores_available:set(host, item.name, item); + end); + + host_session.events.add_handler("item-removed/storage-provider", function (event) + local item = event.item; + stores_available:set(host, item.name, nil); + end); +end +prosody.events.add_handler("host-activated", initialize_host, 101); + +function load_driver(host, driver_name) + if driver_name == "null" then + return null_storage_driver; + end + local driver = stores_available:get(host, driver_name); + if driver then return driver; end + local ok, err = modulemanager.load(host, "storage_"..driver_name); + if not ok then + log("error", "Failed to load storage driver plugin %s on %s: %s", driver_name, host, err); + end + return stores_available:get(host, driver_name); +end + +function get_driver(host, store) + local storage = config.get(host, "storage"); + local driver_name; + local option_type = type(storage); + if option_type == "string" then + driver_name = storage; + elseif option_type == "table" then + driver_name = storage[store]; + end + if not driver_name then + driver_name = config.get(host, "default_storage") or "internal"; + end + + local driver = load_driver(host, driver_name); + if not driver then + log("warn", "Falling back to null driver for %s storage on %s", store, host); + driver_name = "null"; + driver = null_storage_driver; + end + return driver, driver_name; +end + +function open(host, store, typ) + local driver, driver_name = get_driver(host, store); + local ret, err = driver:open(store, typ); + if not ret then + if err == "unsupported-store" then + log("debug", "Storage driver %s does not support store %s (%s), falling back to null driver", + driver_name, store, typ or "<nil>"); + ret = null_storage_driver; + err = nil; + end + end + return ret, err; +end + +function purge(user, host) + local storage = config.get(host, "storage"); + if type(storage) == "table" then + -- multiple storage backends in use that we need to purge + local purged = {}; + for store, driver in pairs(storage) do + if not purged[driver] then + purged[driver] = get_driver(host, store):purge(user); + end + end + end + get_driver(host):purge(user); -- and the default driver + + olddm.purge(user, host); -- COMPAT list stores, like offline messages end up in the old datamanager + + return true; +end + +function datamanager.load(username, host, datastore) + return open(host, datastore):get(username); +end +function datamanager.store(username, host, datastore, data) + return open(host, datastore):set(username, data); +end +function datamanager.users(host, datastore, typ) + local driver = open(host, datastore, typ); + if not driver.users then + return function() log("warn", "storage driver %s does not support listing users", driver.name) end + end + return driver:users(); +end +function datamanager.stores(username, host, typ) + return get_driver(host):stores(username, typ); +end +function datamanager.purge(username, host) + return purge(username, host); +end + +return _M; diff --git a/core/usermanager.lua b/core/usermanager.lua index e38acc87..08343bee 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -1,23 +1,155 @@ +-- 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. +-- -require "util.datamanager" -local datamanager = datamanager; +local modulemanager = require "core.modulemanager"; local log = require "util.logger".init("usermanager"); +local type = type; +local ipairs = ipairs; +local pairs = pairs; +local jid_bare = require "util.jid".bare; +local jid_prep = require "util.jid".prep; +local config = require "core.configmanager"; +local hosts = hosts; +local sasl_new = require "util.sasl".new; +local storagemanager = require "core.storagemanager"; + +local prosody = _G.prosody; + +local setmetatable = setmetatable; + +local default_provider = "internal_plain"; module "usermanager" -function validate_credentials(host, username, password) - log("debug", "User '%s' is being validated", username); - local credentials = datamanager.load(username, host, "accounts") or {}; - if password == credentials.password then return true; end - return false; +function new_null_provider() + local function dummy() return nil, "method not implemented"; end; + local function dummy_get_sasl_handler() return sasl_new(nil, {}); end + return setmetatable({name = "null", get_sasl_handler = dummy_get_sasl_handler}, { + __index = function(self, method) return dummy; end + }); +end + +local provider_mt = { __index = new_null_provider() }; + +function initialize_host(host) + local host_session = hosts[host]; + if host_session.type ~= "local" then return; end + + host_session.events.add_handler("item-added/auth-provider", function (event) + local provider = event.item; + local auth_provider = config.get(host, "authentication") or default_provider; + if config.get(host, "anonymous_login") then + log("error", "Deprecated config option 'anonymous_login'. Use authentication = 'anonymous' instead."); + auth_provider = "anonymous"; + end -- COMPAT 0.7 + if provider.name == auth_provider then + host_session.users = setmetatable(provider, provider_mt); + end + if host_session.users ~= nil and host_session.users.name ~= nil then + log("debug", "host '%s' now set to use user provider '%s'", host, host_session.users.name); + end + end); + host_session.events.add_handler("item-removed/auth-provider", function (event) + local provider = event.item; + if host_session.users == provider then + host_session.users = new_null_provider(); + end + end); + host_session.users = new_null_provider(); -- Start with the default usermanager provider + local auth_provider = config.get(host, "authentication") or default_provider; + if config.get(host, "anonymous_login") then auth_provider = "anonymous"; end -- COMPAT 0.7 + if auth_provider ~= "null" then + modulemanager.load(host, "auth_"..auth_provider); + end +end; +prosody.events.add_handler("host-activated", initialize_host, 100); + +function test_password(username, host, password) + return hosts[host].users.test_password(username, password); +end + +function get_password(username, host) + return hosts[host].users.get_password(username); +end + +function set_password(username, password, host) + return hosts[host].users.set_password(username, password); end function user_exists(username, host) - return datamanager.load(username, host, "accounts") ~= nil; + return hosts[host].users.user_exists(username); end function create_user(username, password, host) - return datamanager.store(username, host, "accounts", {password = password}); + return hosts[host].users.create_user(username, password); +end + +function delete_user(username, host) + local ok, err = hosts[host].users.delete_user(username); + if not ok then return nil, err; end + prosody.events.fire_event("user-deleted", { username = username, host = host }); + return storagemanager.purge(username, host); +end + +function users(host) + return hosts[host].users.users(); +end + +function get_sasl_handler(host, session) + return hosts[host].users.get_sasl_handler(session); +end + +function get_provider(host) + return hosts[host].users; +end + +function is_admin(jid, host) + if host and not hosts[host] then return false; end + if type(jid) ~= "string" then return false; end + + local is_admin; + jid = jid_bare(jid); + host = host or "*"; + + local host_admins = config.get(host, "admins"); + local global_admins = config.get("*", "admins"); + + if host_admins and host_admins ~= global_admins then + if type(host_admins) == "table" then + for _,admin in ipairs(host_admins) do + if jid_prep(admin) == jid then + is_admin = true; + break; + end + end + elseif host_admins then + log("error", "Option 'admins' for host '%s' is not a list", host); + end + end + + if not is_admin and global_admins then + if type(global_admins) == "table" then + for _,admin in ipairs(global_admins) do + if jid_prep(admin) == jid then + is_admin = true; + break; + end + end + elseif global_admins then + log("error", "Global option 'admins' is not a list"); + end + end + + -- Still not an admin, check with auth provider + if not is_admin and host ~= "*" and hosts[host].users and hosts[host].users.is_admin then + is_admin = hosts[host].users.is_admin(jid); + end + return is_admin or false; end -return _M;
\ No newline at end of file +return _M; diff --git a/core/xmlhandlers.lua b/core/xmlhandlers.lua deleted file mode 100644 index 3037a848..00000000 --- a/core/xmlhandlers.lua +++ /dev/null @@ -1,122 +0,0 @@ - -require "util.stanza" - -local st = stanza; -local tostring = tostring; -local pairs = pairs; -local ipairs = ipairs; -local type = type; -local print = print; -local format = string.format; -local m_random = math.random; -local t_insert = table.insert; -local t_remove = table.remove; -local t_concat = table.concat; -local t_concatall = function (t, sep) local tt = {}; for _, s in ipairs(t) do t_insert(tt, tostring(s)); end return t_concat(tt, sep); end -local sm_destroy_session = import("core.sessionmanager", "destroy_session"); - -local default_log = require "util.logger".init("xmlhandlers"); - -local error = error; - -module "xmlhandlers" - -local ns_prefixes = { - ["http://www.w3.org/XML/1998/namespace"] = "xml"; - } - -function init_xmlhandlers(session, streamopened) - local ns_stack = { "" }; - local curr_ns = ""; - local curr_tag; - local chardata = {}; - local xml_handlers = {}; - local log = session.log or default_log; - --local print = function (...) log("info", "xmlhandlers", t_concatall({...}, "\t")); end - - local send = session.send; - - local stanza - function xml_handlers:StartElement(name, attr) - if stanza and #chardata > 0 then - -- We have some character data in the buffer - stanza:text(t_concat(chardata)); - chardata = {}; - end - curr_ns,name = name:match("^(.+)|([%w%-]+)$"); - if curr_ns ~= "jabber:server" then - attr.xmlns = curr_ns; - end - - -- FIXME !!!!! - for i, k in ipairs(attr) do - if type(k) == "string" then - local ns, nm = k:match("^([^|]+)|?([^|]-)$") - if ns and nm then - ns = ns_prefixes[ns]; - if ns then - attr[ns..":"..nm] = attr[k]; - attr[i] = ns..":"..nm; - attr[k] = nil; - end - end - end - end - - if not stanza then --if we are not currently inside a stanza - if session.notopen then - if name == "stream" then - streamopened(session, attr); - return; - end - error("Client failed to open stream successfully"); - end - if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then - error("Client sent invalid top-level stanza"); - end - - stanza = st.stanza(name, attr); --{ to = attr.to, type = attr.type, id = attr.id, xmlns = curr_ns }); - curr_tag = stanza; - else -- we are inside a stanza, so add a tag - attr.xmlns = nil; - if curr_ns ~= "jabber:server" and curr_ns ~= "jabber:client" then - attr.xmlns = curr_ns; - end - stanza:tag(name, attr); - end - end - function xml_handlers:CharacterData(data) - if stanza then - t_insert(chardata, data); - end - end - function xml_handlers:EndElement(name) - curr_ns,name = name:match("^(.+)|([%w%-]+)$"); - if (not stanza) or #stanza.last_add < 0 or (#stanza.last_add > 0 and name ~= stanza.last_add[#stanza.last_add].name) then - if name == "stream" then - log("debug", "Stream closed"); - sm_destroy_session(session); - return; - elseif name == "error" then - error("Stream error: "..tostring(name)..": "..tostring(stanza)); - else - error("XML parse error in client stream"); - end - end - if stanza and #chardata > 0 then - -- We have some character data in the buffer - stanza:text(t_concat(chardata)); - chardata = {}; - end - -- Complete stanza - if #stanza.last_add == 0 then - session.stanza_dispatch(stanza); - stanza = nil; - else - stanza:up(); - end - end - return xml_handlers; -end - -return init_xmlhandlers; |