aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authorMatthew Wild <mwild1@gmail.com>2012-04-24 21:59:20 +0100
committerMatthew Wild <mwild1@gmail.com>2012-04-24 21:59:20 +0100
commit15175bb828ab14cf0f35b221227beeeb46e4dd8f (patch)
treefe8c013599dd847f7365cb1adab80ab8fe846de5 /core
parenta5efef5aab5624e3a1cb4f1966e719738b859eda (diff)
parent7cb22f77a78741a1bd085c6b3dc3b31f51c1fa09 (diff)
downloadprosody-15175bb828ab14cf0f35b221227beeeb46e4dd8f.tar.gz
prosody-15175bb828ab14cf0f35b221227beeeb46e4dd8f.zip
Merge timber->trunk - thanks everyone!
Diffstat (limited to 'core')
-rw-r--r--core/certmanager.lua2
-rw-r--r--core/configmanager.lua3
-rw-r--r--core/loggingmanager.lua34
-rw-r--r--core/moduleapi.lua326
-rw-r--r--core/modulemanager.lua403
-rw-r--r--core/portmanager.lua194
-rw-r--r--core/s2smanager.lua593
-rw-r--r--core/sessionmanager.lua60
-rw-r--r--core/stanza_router.lua28
9 files changed, 667 insertions, 976 deletions
diff --git a/core/certmanager.lua b/core/certmanager.lua
index 8b82ac47..cccf3098 100644
--- a/core/certmanager.lua
+++ b/core/certmanager.lua
@@ -35,7 +35,7 @@ function create_context(host, mode, user_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;
+ 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);
diff --git a/core/configmanager.lua b/core/configmanager.lua
index 85919492..e2253171 100644
--- a/core/configmanager.lua
+++ b/core/configmanager.lua
@@ -41,6 +41,9 @@ function getconfig()
end
function get(host, section, key)
+ if not key then
+ section, key = "core", section;
+ end
local sec = config[host][section];
if sec then
return sec[key];
diff --git a/core/loggingmanager.lua b/core/loggingmanager.lua
index 426425c1..56a3ee2c 100644
--- a/core/loggingmanager.lua
+++ b/core/loggingmanager.lua
@@ -48,34 +48,12 @@ local logging_levels = { "debug", "info", "warn", "error" }
local function add_rule(sink_config)
local sink_maker = log_sink_types[sink_config.to];
if sink_maker then
- if sink_config.levels and not sink_config.source then
- -- Create sink
- local sink = sink_maker(sink_config);
-
- -- Set sink for all chosen levels
- for level in pairs(get_levels(sink_config.levels)) do
- logger.add_level_sink(level, sink);
- end
- elseif sink_config.source and not sink_config.levels then
- logger.add_name_sink(sink_config.source, sink_maker(sink_config));
- elseif sink_config.source and sink_config.levels then
- local levels = get_levels(sink_config.levels);
- local sink = sink_maker(sink_config);
- logger.add_name_sink(sink_config.source,
- function (name, level, ...)
- if levels[level] then
- return sink(name, level, ...);
- end
- end);
- else
- -- All sources
- -- Create sink
- local sink = sink_maker(sink_config);
-
- -- Set sink for all levels
- for _, level in pairs(logging_levels) do
- logger.add_level_sink(level, sink);
- end
+ -- Create sink
+ local sink = sink_maker(sink_config);
+
+ -- Set sink for all chosen levels
+ for level in pairs(get_levels(sink_config.levels or logging_levels)) do
+ logger.add_level_sink(level, sink);
end
else
-- No such sink type
diff --git a/core/moduleapi.lua b/core/moduleapi.lua
new file mode 100644
index 00000000..d16ee410
--- /dev/null
+++ b/core/moduleapi.lua
@@ -0,0 +1,326 @@
+-- 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";
+local array = require "util.array";
+local set = require "util.set";
+local logger = require "util.logger";
+local pluginloader = require "util.pluginloader";
+
+local multitable_new = require "util.multitable".new;
+
+local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
+local error, setmetatable, setfenv, type = error, setmetatable, setfenv, type;
+local ipairs, pairs, select, unpack = ipairs, pairs, select, unpack;
+local tonumber, tostring = tonumber, tostring;
+
+local prosody = prosody;
+local hosts = prosody.hosts;
+local core_post_stanza = prosody.core_post_stanza;
+
+-- 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 hosts[self.host].type;
+end
+
+function api:set_global()
+ self.host = "*";
+ -- Update the logger
+ local _log = logger.init("mod_"..self.name);
+ self.log = function (self, ...) return _log(...); end;
+ self._log = _log;
+ 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:fire_event(...)
+ return (hosts[self.host] or prosody).events.fire_event(...);
+end
+
+function api:hook_object_event(object, event, handler, priority)
+ self.event_handlers[handler] = { name = event, priority = priority, object = object };
+ return object.add_handler(event, handler, priority);
+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_stanza(xmlns, name, handler, priority)
+ if not handler and type(name) == "function" then
+ -- If only 2 options then they specified no xmlns
+ xmlns, name, handler, priority = nil, xmlns, name, handler;
+ elseif not (handler and name) then
+ self:log("warn", "Error: Insufficient parameters to module:hook_stanza()");
+ return;
+ end
+ return self:hook("stanza/"..(xmlns and (xmlns..":") or "")..name, function (data) return handler(data.origin, data.stanza, data); end, priority);
+end
+
+function api:require(lib)
+ local f, n = pluginloader.load_code(self.name, lib..".lib.lua");
+ if not f then
+ f, n = pluginloader.load_code(lib, lib..".lib.lua");
+ end
+ if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message
+ setfenv(f, self.environment);
+ return f();
+end
+
+function api:depends(name)
+ if not self.dependencies then
+ self.dependencies = {};
+ self:hook("module-reloaded", function (event)
+ if self.dependencies[event.module] 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 modulemanager.module_has_method(mod, "add_host") then
+ mod = nil; -- This 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 = {};
+ 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, self.name, name);
+ if value == nil then
+ value = config.get(self.host, "core", name);
+ if value == nil then
+ value = default_value;
+ end
+ end
+ return value;
+end
+
+function api:get_option_string(name, default_value)
+ local value = self:get_option(name, default_value);
+ if type(value) == "table" then
+ if #value > 1 then
+ self:log("error", "Config option '%s' does not take a list, using just the first item", name);
+ end
+ value = value[1];
+ end
+ if value == nil then
+ return nil;
+ end
+ return tostring(value);
+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
+
+local module_items = multitable_new();
+function api:add_item(key, value)
+ self.items = self.items or {};
+ self.items[key] = self.items[key] or {};
+ t_insert(self.items[key], value);
+ self:fire_event("item-added/"..key, {source = self, item = value});
+end
+function api:remove_item(key, value)
+ local t = self.items and self.items[key] or NULL;
+ for i = #t,1,-1 do
+ if t[i] == value then
+ t_remove(self.items[key], i);
+ self:fire_event("item-removed/"..key, {source = self, item = value});
+ return value;
+ end
+ end
+end
+
+function api:get_host_items(key)
+ local result = {};
+ for mod_name, module in pairs(modulemanager.get_modules(self.host)) do
+ module = module.module;
+ if module.items then
+ for _, item in ipairs(module.items[key] or NULL) do
+ t_insert(result, item);
+ end
+ end
+ end
+ for mod_name, module in pairs(modulemanager.get_modules("*")) do
+ module = module.module;
+ if module.items then
+ for _, item in ipairs(module.items[key] or NULL) do
+ t_insert(result, item);
+ end
+ end
+ end
+ 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 = self.environment; 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
+ 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
+
+return api;
diff --git a/core/modulemanager.lua b/core/modulemanager.lua
index c4d95695..bf1e1924 100644
--- a/core/modulemanager.lua
+++ b/core/modulemanager.lua
@@ -9,13 +9,10 @@
local logger = require "util.logger";
local log = logger.init("modulemanager");
local config = require "core.configmanager";
-local multitable_new = require "util.multitable".new;
-local st = require "util.stanza";
local pluginloader = require "util.pluginloader";
local hosts = hosts;
local prosody = prosody;
-local prosody_events = prosody.events;
local loadfile, pcall, xpcall = loadfile, pcall, xpcall;
local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv;
@@ -37,7 +34,7 @@ end
local array, set = require "util.array", require "util.set";
-local autoload_modules = {"presence", "message", "iq", "offline"};
+local autoload_modules = {"presence", "message", "iq", "offline", "c2s", "s2s"};
local component_inheritable_modules = {"tls", "dialback", "iq"};
-- We need this to let modules access the real global namespace
@@ -45,15 +42,11 @@ local _G = _G;
module "modulemanager"
-api = {};
-local api = api; -- Module API container
+local api = _G.require "core.moduleapi"; -- Module API container
+-- [host] = { [module] = module_env }
local modulemap = { ["*"] = {} };
-local modulehelpers = setmetatable({}, { __index = _G });
-
-local hooks = multitable_new();
-
local NULL = {};
-- Load modules when a host is activated
@@ -88,24 +81,74 @@ function load_modules_for_host(host)
load(host, module);
end
end
-prosody_events.add_handler("host-activated", load_modules_for_host);
---
+prosody.events.add_handler("host-activated", load_modules_for_host);
+
+--- Private helpers ---
+
+local function do_unload_module(host, name)
+ local mod = get_module(host, name);
+ if not mod then return nil, "module-not-loaded"; end
+
+ if module_has_method(mod, "unload") then
+ local ok, err = call_module_method(mod, "unload");
+ if (not ok) and err then
+ log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
+ end
+ end
+
+ for handler, event in pairs(mod.module.event_handlers) do
+ event.object.remove_handler(event.name, handler);
+ end
+
+ if mod.module.items then -- remove items
+ local events = (host == "*" and prosody.events) or hosts[host].events;
+ for key,t in pairs(mod.module.items) do
+ for i = #t,1,-1 do
+ local value = t[i];
+ t[i] = nil;
+ events.fire_event("item-removed/"..key, {source = mod.module, item = value});
+ end
+ end
+ end
+ mod.module.loaded = false;
+ modulemap[host][name] = nil;
+ return true;
+end
-function load(host, module_name, config)
+local function do_load_module(host, module_name)
if not (host and module_name) then
return nil, "insufficient-parameters";
- elseif not hosts[host] then
+ elseif not hosts[host] and host ~= "*"then
return nil, "unknown-host";
end
if not modulemap[host] then
modulemap[host] = {};
+ if host ~= "*" then
+ hosts[host].modules = modulemap[host];
+ end
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 = {}, items = {};
+ _log = _log, log = function (self, ...) return _log(...); end;
+ },{
+ __index = modulemap["*"][module_name].module;
+ });
+ local ok, result, module_err = call_module_method(mod, "add_host", host_module_api);
+ if not ok or result == false then return nil, ok and module_err or result; end
+ local host_module = setmetatable({ module = host_module_api }, { __index = mod });
+ host_module.module.environment = host_module;
+ modulemap[host][module_name] = host_module;
+ return host_module;
+ end
return nil, "global-module-already-loaded";
end
@@ -117,88 +160,44 @@ function load(host, module_name, config)
end
local _log = logger.init(host..":"..module_name);
- local api_instance = setmetatable({ name = module_name, host = host, path = err, _log = _log, log = function (self, ...) return _log(...); end }, { __index = api });
+ local api_instance = setmetatable({ name = module_name, host = host, path = err,
+ _log = _log, log = function (self, ...) return _log(...); end, event_handlers = {} }
+ , { __index = api });
local pluginenv = setmetatable({ module = api_instance }, { __index = _G });
api_instance.environment = pluginenv;
setfenv(mod, pluginenv);
- hosts[host].modules = modulemap[host];
- modulemap[host][module_name] = pluginenv;
- local success, err = pcall(mod);
- if success then
+ local ok, err = pcall(mod);
+ if ok then
+ -- Call module's "load"
if module_has_method(pluginenv, "load") then
- success, err = call_module_method(pluginenv, "load");
- if not success then
+ ok, err = call_module_method(pluginenv, "load");
+ if not ok then
log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err or "nil");
end
end
- -- Use modified host, if the module set one
- if api_instance.host == "*" and host ~= "*" then
- modulemap[host][module_name] = nil;
- modulemap["*"][module_name] = pluginenv;
- api_instance:set_global();
- end
- else
- log("error", "Error initializing module '%s' on '%s': %s", module_name, host, err or "nil");
- end
- if success then
- (hosts[api_instance.host] or prosody).events.fire_event("module-loaded", { module = module_name, host = host });
- return true;
- else -- load failed, unloading
- unload(api_instance.host, module_name);
- return nil, err;
- end
-end
-
-function get_module(host, name)
- return modulemap[host] and modulemap[host][name];
-end
-
-function is_loaded(host, name)
- return modulemap[host] and modulemap[host][name] and true;
-end
-
-function unload(host, name, ...)
- local mod = get_module(host, name);
- if not mod then return nil, "module-not-loaded"; end
-
- if module_has_method(mod, "unload") then
- local ok, err = call_module_method(mod, "unload");
- if (not ok) and err then
- log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
- end
- end
- -- unhook event handlers hooked by module:hook
- for event, handlers in pairs(hooks:get(host, name) or NULL) do
- for handler in pairs(handlers or NULL) do
- (hosts[host] or prosody).events.remove_handler(event, handler);
- end
- end
- -- unhook event handlers hooked by module:hook_global
- for event, handlers in pairs(hooks:get("*", name) or NULL) do
- for handler in pairs(handlers or NULL) do
- prosody.events.remove_handler(event, handler);
- end
- end
- hooks:remove(host, name);
- if mod.module.items then -- remove items
- for key,t in pairs(mod.module.items) do
- for i = #t,1,-1 do
- local value = t[i];
- t[i] = nil;
- hosts[host].events.fire_event("item-removed/"..key, {source = mod.module, item = value});
+ modulemap[api_instance.host][module_name] = pluginenv;
+ if api_instance.host == "*" then
+ if not api_instance.global then -- COMPAT w/pre-0.9
+ log("warn", "mod_%s: Setting module.host = '*' deprecated, call module:set_global() instead", module_name);
+ api_instance:set_global();
+ end
+ if host ~= api_instance.host and module_has_method(pluginenv, "add_host") then
+ -- Now load the module again onto the host it was originally being loaded on
+ ok, err = do_load_module(host, module_name);
end
end
end
- modulemap[host][name] = nil;
- (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host });
- return true;
+ if not ok then
+ log("error", "Error initializing module '%s' on '%s': %s", module_name, host, err or "nil");
+ end
+ return ok and pluginenv, err;
end
-function reload(host, name, ...)
+local function do_reload_module(host, name)
local mod = get_module(host, name);
if not mod then return nil, "module-not-loaded"; end
@@ -209,7 +208,6 @@ function reload(host, name, ...)
end
local saved;
-
if module_has_method(mod, "save") then
local ok, ret, err = call_module_method(mod, "save");
if ok then
@@ -225,8 +223,8 @@ function reload(host, name, ...)
end
end
- unload(host, name, ...);
- local ok, err = load(host, name, ...);
+ do_unload_module(host, name);
+ local ok, err = do_load_module(host, name);
if ok then
mod = get_module(host, name);
if module_has_method(mod, "restore") then
@@ -235,231 +233,62 @@ function reload(host, name, ...)
log("warn", "Error restoring module '%s' from '%s': %s", name, host, err);
end
end
- return true;
end
- return ok, err;
+ return ok and mod, err;
end
-function module_has_method(module, method)
- return type(module.module[method]) == "function";
-end
+--- Public API ---
-function call_module_method(module, method, ...)
- if module_has_method(module, method) then
- local f = module.module[method];
- return pcall(f, ...);
- else
- return false, "no-such-method";
- end
-end
-
------ API functions exposed to modules -----------
--- Must all be in api.*
-
--- Returns the name of the current module
-function api:get_name()
- return self.name;
-end
-
--- Returns the host that the current module is serving
-function api:get_host()
- return self.host;
-end
-
-function api:get_host_type()
- return hosts[self.host].type;
-end
-
-function api:set_global()
- self.host = "*";
- -- Update the logger
- local _log = logger.init("mod_"..self.name);
- self.log = function (self, ...) return _log(...); end;
- self._log = _log;
-end
-
-function api:add_feature(xmlns)
- self:add_item("feature", xmlns);
-end
-function api:add_identity(category, type, name)
- self:add_item("identity", {category = category, type = type, name = name});
-end
-function api:add_extension(data)
- self:add_item("extension", data);
-end
-
-function api:fire_event(...)
- return (hosts[self.host] or prosody).events.fire_event(...);
-end
-
-function api:hook(event, handler, priority)
- hooks:set(self.host, self.name, event, handler, true);
- (hosts[self.host] or prosody).events.add_handler(event, handler, priority);
-end
-
-function api:hook_global(event, handler, priority)
- hooks:set("*", self.name, event, handler, true);
- prosody.events.add_handler(event, handler, priority);
-end
-
-function api:hook_stanza(xmlns, name, handler, priority)
- if not handler and type(name) == "function" then
- -- If only 2 options then they specified no xmlns
- xmlns, name, handler, priority = nil, xmlns, name, handler;
- elseif not (handler and name) then
- self:log("warn", "Error: Insufficient parameters to module:hook_stanza()");
- return;
+-- 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 = host });
end
- return api.hook(self, "stanza/"..(xmlns and (xmlns..":") or "")..name, function (data) return handler(data.origin, data.stanza, data); end, priority);
+ return mod, err;
end
-function api:require(lib)
- local f, n = pluginloader.load_code(self.name, lib..".lib.lua");
- if not f then
- f, n = pluginloader.load_code(lib, lib..".lib.lua");
- end
- if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message
- setfenv(f, self.environment);
- return f();
-end
-
-function api:get_option(name, default_value)
- local value = config.get(self.host, self.name, name);
- if value == nil then
- value = config.get(self.host, "core", name);
- if value == nil then
- value = default_value;
- end
- end
- return value;
-end
-
-function api:get_option_string(name, default_value)
- local value = self:get_option(name, default_value);
- if type(value) == "table" then
- if #value > 1 then
- self:log("error", "Config option '%s' does not take a list, using just the first item", name);
- end
- value = value[1];
- end
- if value == nil then
- return nil;
- end
- return tostring(value);
-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);
+-- 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 ret;
+ return ok, err;
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);
+function reload(host, name)
+ local ok, err = do_reload_module(host, name);
+ if ok then
+ (hosts[host] or prosody).events.fire_event("module-reloaded", { module = name, host = host });
+ elseif not is_loaded(host, name) then
+ (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host });
end
- return ret;
+ return ok, err;
end
-function api:get_option_array(name, ...)
- local value = self:get_option(name, ...);
-
- if value == nil then
- return nil;
- end
-
- if type(value) ~= "table" then
- return array{ value }; -- Assume any non-list is a single-item list
- end
-
- return array():append(value); -- Clone
+function get_module(host, name)
+ return modulemap[host] and modulemap[host][name];
end
-function api:get_option_set(name, ...)
- local value = self:get_option_array(name, ...);
-
- if value == nil then
- return nil;
- end
-
- return set.new(value);
+function get_modules(host)
+ return modulemap[host];
end
-local t_remove = _G.table.remove;
-local module_items = multitable_new();
-function api:add_item(key, value)
- self.items = self.items or {};
- self.items[key] = self.items[key] or {};
- t_insert(self.items[key], value);
- self:fire_event("item-added/"..key, {source = self, item = value});
-end
-function api:remove_item(key, value)
- local t = self.items and self.items[key] or NULL;
- for i = #t,1,-1 do
- if t[i] == value then
- t_remove(self.items[key], i);
- self:fire_event("item-removed/"..key, {source = self, item = value});
- return value;
- end
- end
+function is_loaded(host, name)
+ return modulemap[host] and modulemap[host][name] and true;
end
-function api:get_host_items(key)
- local result = {};
- for mod_name, module in pairs(modulemap[self.host]) do
- module = module.module;
- if module.items then
- for _, item in ipairs(module.items[key] or NULL) do
- t_insert(result, item);
- end
- end
- end
- for mod_name, module in pairs(modulemap["*"]) do
- module = module.module;
- if module.items then
- for _, item in ipairs(module.items[key] or NULL) do
- t_insert(result, item);
- end
- end
- end
- return result;
+function module_has_method(module, method)
+ return type(rawget(module.module, method)) == "function";
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
+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
diff --git a/core/portmanager.lua b/core/portmanager.lua
new file mode 100644
index 00000000..da238dba
--- /dev/null
+++ b/core/portmanager.lua
@@ -0,0 +1,194 @@
+
+local multitable = require "util.multitable";
+local fire_event = prosody.events.fire_event;
+
+--- Config
+
+local default_interfaces = { "*" };
+local default_local_interfaces = { "127.0.0.1" };
+if config.get("*", "use_ipv6") 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";
+ elseif err == "no ssl context" then
+ if not config.get("*", "core", "ssl") then
+ friendly_message = "there is no 'ssl' config under Host \"*\" which is "
+ .."require for legacy SSL ports";
+ else
+ friendly_message = "initializing SSL support failed, see previous log entries";
+ end
+ end
+ return friendly_message;
+end
+
+module("portmanager", package.seeall);
+
+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);
+
+--- Public API
+
+function activate_service(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 = set.new(config.get("*", config_prefix.."interfaces")
+ or config.get("*", config_prefix.."interface") -- COMPAT w/pre-0.9
+ or (service_info.private and 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);
+
+ local bind_ports = set.new(config.get("*", config_prefix.."ports")
+ or service_info.default_ports
+ or {service_info.default_port
+ or listener.default_port -- COMPAT w/pre-0.9
+ });
+
+ local mode = listener.default_mode or "*a";
+ local ssl;
+ if service_info.encryption == "ssl" then
+ ssl = prosody.global_ssl_ctx;
+ if not ssl then
+ return nil, "global-ssl-context-required";
+ end
+ end
+
+ for interface in bind_interfaces do
+ for port in bind_ports do
+ if #active_services:search(nil, interface, port) > 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 handler, err = server.addserver(interface, port, listener, mode, ssl);
+ if not handler then
+ log("error", "Failed to open server port %d on %s, %s", port, interface, error_to_friendly_message(service_name, port, err));
+ else
+ log("debug", "Added listening service %s to [%s]:%d", service_name, interface, port);
+ active_services:add(service_name, interface, port, {
+ server = handler;
+ service = service_info;
+ });
+ end
+ end
+ end
+ end
+ log("info", "Activated service '%s'", service_name);
+ return true;
+end
+
+function deactivate(service_name)
+ local active = active_services:search(service_name)[1];
+ if not active then return; end
+ for interface, ports in pairs(active) do
+ for port, active_service in pairs(ports) do
+ close(interface, port);
+ end
+ end
+ log("info", "Deactivated service '%s'", service_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(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)
+ 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
+ if active_services[service_name] == service_info then
+ deactivate(service_name);
+ if #service_info_list > 0 then -- Other services registered with this name
+ activate(service_name); -- Re-activate with the next available one
+ end
+ 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];
+end
+
+function get_active_services(...)
+ return active_services;
+end
+
+function get_registered_services()
+ return services;
+end
+
+return _M;
diff --git a/core/s2smanager.lua b/core/s2smanager.lua
index acb0baeb..5907ff2f 100644
--- a/core/s2smanager.lua
+++ b/core/s2smanager.lua
@@ -9,18 +9,13 @@
local hosts = hosts;
-local sessions = sessions;
local core_process_stanza = function(a, b) core_process_stanza(a, b); end
-local add_task = require "util.timer".add_task;
-local socket = require "socket";
local format = string.format;
local t_insert, t_sort = table.insert, table.sort;
local get_traceback = debug.traceback;
local tostring, pairs, ipairs, getmetatable, newproxy, type, error, tonumber, setmetatable
= tostring, pairs, ipairs, getmetatable, newproxy, type, error, tonumber, setmetatable;
-local idna_to_ascii = require "util.encodings".idna.to_ascii;
-local connlisteners_get = require "net.connlisteners".get;
local initialize_filters = require "util.filters".initialize;
local wrapclient = require "net.server".wrapclient;
local st = require "stanza";
@@ -41,11 +36,12 @@ local sha256_hash = require "util.hashes".sha256;
local adns, dns = require "net.adns", require "net.dns";
local config = require "core.configmanager";
-local connect_timeout = config.get("*", "core", "s2s_timeout") or 60;
local dns_timeout = config.get("*", "core", "dns_timeout") or 15;
-local max_dns_depth = config.get("*", "core", "dns_max_depth") or 3;
+local cfg_sources = config.get("*", "core", "s2s_interface")
+ or config.get("*", "core", "interface");
local sources;
+--FIXME: s2sout should create its own resolver w/ timeout
dns.settimeout(dns_timeout);
local prosody = _G.prosody;
@@ -55,89 +51,6 @@ local incoming_s2s = incoming_s2s;
module "s2smanager"
-function compare_srv_priorities(a,b)
- return a.priority < b.priority or (a.priority == b.priority and a.weight > b.weight);
-end
-
-local bouncy_stanzas = { message = true, presence = true, iq = true };
-local function bounce_sendq(session, reason)
- local sendq = session.sendq;
- if sendq then
- session.log("info", "sending error replies for "..#sendq.." queued stanzas because of failed outgoing connection to "..tostring(session.to_host));
- local dummy = {
- type = "s2sin";
- send = function(s)
- (session.log or log)("error", "Replying to to an s2s error reply, please report this! Traceback: %s", get_traceback());
- end;
- dummy = true;
- };
- for i, data in ipairs(sendq) do
- local reply = data[2];
- if reply and not(reply.attr.xmlns) and bouncy_stanzas[reply.name] then
- reply.attr.type = "error";
- reply:tag("error", {type = "cancel"})
- :tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
- if reason then
- reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})
- :text("Server-to-server connection failed: "..reason):up();
- end
- core_process_stanza(dummy, reply);
- end
- sendq[i] = nil;
- end
- session.sendq = nil;
- end
-end
-
-function send_to_host(from_host, to_host, data)
- if not hosts[from_host] then
- log("warn", "Attempt to send stanza from %s - a host we don't serve", from_host);
- return false;
- end
- local host = hosts[from_host].s2sout[to_host];
- if host then
- -- We have a connection to this host already
- if host.type == "s2sout_unauthed" and (data.name ~= "db:verify" or not host.dialback_key) then
- (host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host);
-
- -- Queue stanza until we are able to send it
- if host.sendq then t_insert(host.sendq, {tostring(data), data.attr.type ~= "error" and data.attr.type ~= "result" and st.reply(data)});
- else host.sendq = { {tostring(data), data.attr.type ~= "error" and data.attr.type ~= "result" and st.reply(data)} }; end
- host.log("debug", "stanza [%s] queued ", data.name);
- elseif host.type == "local" or host.type == "component" then
- log("error", "Trying to send a stanza to ourselves??")
- log("error", "Traceback: %s", get_traceback());
- log("error", "Stanza: %s", tostring(data));
- return false;
- else
- (host.log or log)("debug", "going to send stanza to "..to_host.." from "..from_host);
- -- FIXME
- if host.from_host ~= from_host then
- log("error", "WARNING! This might, possibly, be a bug, but it might not...");
- log("error", "We are going to send from %s instead of %s", tostring(host.from_host), tostring(from_host));
- end
- host.sends2s(data);
- host.log("debug", "stanza sent over "..host.type);
- end
- else
- log("debug", "opening a new outgoing connection for this stanza");
- local host_session = new_outgoing(from_host, to_host);
-
- -- Store in buffer
- host_session.sendq = { {tostring(data), data.attr.type ~= "error" and data.attr.type ~= "result" and st.reply(data)} };
- log("debug", "stanza [%s] queued until connection complete", tostring(data.name));
- if (not host_session.connecting) and (not host_session.conn) then
- log("warn", "Connection to %s failed already, destroying session...", to_host);
- if not destroy_session(host_session, "Connection failed") then
- -- Already destroyed, we need to bounce our stanza
- bounce_sendq(host_session, host_session.destruction_reason);
- end
- return false;
- end
- end
- return true;
-end
-
local open_sessions = 0;
function new_incoming(conn)
@@ -147,496 +60,18 @@ function new_incoming(conn)
getmetatable(session.trace).__gc = function () open_sessions = open_sessions - 1; end;
end
open_sessions = open_sessions + 1;
- local w, log = conn.write, logger_init("s2sin"..tostring(conn):match("[a-f0-9]+$"));
- session.log = log;
- local filter = initialize_filters(session);
- session.sends2s = function (t)
- log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)"));
- if t.name then
- t = filter("stanzas/out", t);
- end
- if t then
- t = filter("bytes/out", tostring(t));
- if t then
- return w(conn, t);
- end
- end
- end
+ session.log = logger_init("s2sin"..tostring(conn):match("[a-f0-9]+$"));
incoming_s2s[session] = true;
- add_task(connect_timeout, function ()
- if session.conn ~= conn or
- session.type == "s2sin" then
- return; -- Ok, we're connect[ed|ing]
- end
- -- Not connected, need to close session and clean up
- (session.log or log)("debug", "Destroying incomplete session %s->%s due to inactivity",
- session.from_host or "(unknown)", session.to_host or "(unknown)");
- session:close("connection-timeout");
- end);
return session;
end
function new_outgoing(from_host, to_host, connect)
- local host_session = { to_host = to_host, from_host = from_host, host = from_host,
- notopen = true, type = "s2sout_unauthed", direction = "outgoing",
- open_stream = session_open_stream };
-
- hosts[from_host].s2sout[to_host] = host_session;
-
- host_session.close = destroy_session; -- This gets replaced by xmppserver_listener later
-
- local log;
- do
- local conn_name = "s2sout"..tostring(host_session):match("[a-f0-9]*$");
- log = logger_init(conn_name);
- host_session.log = log;
- end
-
- initialize_filters(host_session);
-
- if connect ~= false then
- -- Kick the connection attempting machine into life
- if not attempt_connection(host_session) then
- -- Intentionally not returning here, the
- -- session is needed, connected or not
- destroy_session(host_session);
- end
- end
-
- if not host_session.sends2s then
- -- A sends2s which buffers data (until the stream is opened)
- -- note that data in this buffer will be sent before the stream is authed
- -- and will not be ack'd in any way, successful or otherwise
- local buffer;
- function host_session.sends2s(data)
- if not buffer then
- buffer = {};
- host_session.send_buffer = buffer;
- end
- log("debug", "Buffering data on unconnected s2sout to %s", to_host);
- buffer[#buffer+1] = data;
- log("debug", "Buffered item %d: %s", #buffer, tostring(data));
- end
- end
-
- return host_session;
-end
-
-
-function attempt_connection(host_session, err)
- local from_host, to_host = host_session.from_host, host_session.to_host;
- local connect_host, connect_port = to_host and idna_to_ascii(to_host), 5269;
-
- if not connect_host then
- return false;
- end
-
- if not err then -- This is our first attempt
- log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host);
- host_session.connecting = true;
- local handle;
- handle = adns.lookup(function (answer)
- handle = nil;
- host_session.connecting = nil;
- if answer then
- log("debug", to_host.." has SRV records, handling...");
- local srv_hosts = {};
- host_session.srv_hosts = srv_hosts;
- for _, record in ipairs(answer) do
- t_insert(srv_hosts, record.srv);
- end
- if #srv_hosts == 1 and srv_hosts[1].target == "." then
- log("debug", to_host.." does not provide a XMPP service");
- destroy_session(host_session, err); -- Nothing to see here
- return;
- end
- t_sort(srv_hosts, compare_srv_priorities);
-
- local srv_choice = srv_hosts[1];
- host_session.srv_choice = 1;
- if srv_choice then
- connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
- log("debug", "Best record found, will connect to %s:%d", connect_host, connect_port);
- end
- else
- log("debug", to_host.." has no SRV records, falling back to A");
- end
- -- Try with SRV, or just the plain hostname if no SRV
- local ok, err = try_connect(host_session, connect_host, connect_port);
- if not ok then
- if not attempt_connection(host_session, err) then
- -- No more attempts will be made
- destroy_session(host_session, err);
- end
- end
- end, "_xmpp-server._tcp."..connect_host..".", "SRV");
-
- return true; -- Attempt in progress
- elseif host_session.ip_hosts then
- return try_connect(host_session, connect_host, connect_port, err);
- elseif host_session.srv_hosts and #host_session.srv_hosts > host_session.srv_choice then -- Not our first attempt, and we also have SRV
- host_session.srv_choice = host_session.srv_choice + 1;
- local srv_choice = host_session.srv_hosts[host_session.srv_choice];
- connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
- host_session.log("info", "Connection failed (%s). Attempt #%d: This time to %s:%d", tostring(err), host_session.srv_choice, connect_host, connect_port);
- else
- host_session.log("info", "Out of connection options, can't connect to %s", tostring(host_session.to_host));
- -- We're out of options
- return false;
- end
-
- if not (connect_host and connect_port) then
- -- Likely we couldn't resolve DNS
- log("warn", "Hmm, we're without a host (%s) and port (%s) to connect to for %s, giving up :(", tostring(connect_host), tostring(connect_port), tostring(to_host));
- return false;
- end
-
- return try_connect(host_session, connect_host, connect_port);
-end
-
-function try_next_ip(host_session)
- host_session.connecting = nil;
- host_session.ip_choice = host_session.ip_choice + 1;
- local ip = host_session.ip_hosts[host_session.ip_choice];
- local ok, err= make_connect(host_session, ip.ip, ip.port);
- if not ok then
- if not attempt_connection(host_session, err or "closed") then
- err = err and (": "..err) or "";
- destroy_session(host_session, "Connection failed"..err);
- end
- end
-end
-
-function try_connect(host_session, connect_host, connect_port, err)
- host_session.connecting = true;
-
- if not err then
- local IPs = {};
- host_session.ip_hosts = IPs;
- local handle4, handle6;
- local has_other = false;
-
- if not sources then
- sources = {};
- local cfg_sources = config.get("*", "core", "interface") or connlisteners_get("xmppserver").default_interface;
- if type(cfg_sources) == "string" then
- cfg_sources = { cfg_sources };
- end
- for i, source in ipairs(cfg_sources) do
- if source == "*" then
- sources[i] = new_ip("0.0.0.0", "IPv4");
- else
- sources[i] = new_ip(source, (source:find(":") and "IPv6") or "IPv4");
- end
- end
- end
-
- handle4 = adns.lookup(function (reply, err)
- handle4 = nil;
-
- -- COMPAT: This is a compromise for all you CNAME-(ab)users :)
- if not (reply and reply[#reply] and reply[#reply].a) then
- local count = max_dns_depth;
- reply = dns.peek(connect_host, "CNAME", "IN");
- while count > 0 and reply and reply[#reply] and not reply[#reply].a and reply[#reply].cname do
- log("debug", "Looking up %s (DNS depth is %d)", tostring(reply[#reply].cname), count);
- reply = dns.peek(reply[#reply].cname, "A", "IN") or dns.peek(reply[#reply].cname, "CNAME", "IN");
- count = count - 1;
- end
- end
- -- end of CNAME resolving
-
- if reply and reply[#reply] and reply[#reply].a then
- for _, ip in ipairs(reply) do
- log("debug", "DNS reply for %s gives us %s", connect_host, ip.a);
- IPs[#IPs+1] = new_ip(ip.a, "IPv4");
- end
- end
-
- if has_other then
- if #IPs > 0 then
- rfc3484_dest(host_session.ip_hosts, sources);
- for i = 1, #IPs do
- IPs[i] = {ip = IPs[i], port = connect_port};
- end
- host_session.ip_choice = 0;
- try_next_ip(host_session);
- else
- log("debug", "DNS lookup failed to get a response for %s", connect_host);
- host_session.ip_hosts = nil;
- if not attempt_connection(host_session, "name resolution failed") then -- Retry if we can
- log("debug", "No other records to try for %s - destroying", host_session.to_host);
- err = err and (": "..err) or "";
- destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't
- end
- end
- else
- has_other = true;
- end
- end, connect_host, "A", "IN");
-
- handle6 = adns.lookup(function (reply, err)
- handle6 = nil;
-
- if reply and reply[#reply] and reply[#reply].aaaa then
- for _, ip in ipairs(reply) do
- log("debug", "DNS reply for %s gives us %s", connect_host, ip.aaaa);
- IPs[#IPs+1] = new_ip(ip.aaaa, "IPv6");
- end
- end
-
- if has_other then
- if #IPs > 0 then
- rfc3484_dest(host_session.ip_hosts, sources);
- for i = 1, #IPs do
- IPs[i] = {ip = IPs[i], port = connect_port};
- end
- host_session.ip_choice = 0;
- try_next_ip(host_session);
- else
- log("debug", "DNS lookup failed to get a response for %s", connect_host);
- host_session.ip_hosts = nil;
- if not attempt_connection(host_session, "name resolution failed") then -- Retry if we can
- log("debug", "No other records to try for %s - destroying", host_session.to_host);
- err = err and (": "..err) or "";
- destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't
- end
- end
- else
- has_other = true;
- end
- end, connect_host, "AAAA", "IN");
-
- return true;
- elseif host_session.ip_hosts and #host_session.ip_hosts > host_session.ip_choice then -- Not our first attempt, and we also have IPs left to try
- try_next_ip(host_session);
- else
- host_session.ip_hosts = nil;
- if not attempt_connection(host_session, "out of IP addresses") then -- Retry if we can
- log("debug", "No other records to try for %s - destroying", host_session.to_host);
- err = err and (": "..err) or "";
- destroy_session(host_session, "Connecting failed"..err); -- End of the line, we can't
- return false;
- end
- end
-
- return true;
-end
-
-function make_connect(host_session, connect_host, connect_port)
- (host_session.log or log)("info", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port);
- -- Ok, we're going to try to connect
-
- local from_host, to_host = host_session.from_host, host_session.to_host;
-
- local conn, handler;
- if connect_host.proto == "IPv4" then
- conn, handler = socket.tcp();
- elseif socket.tcp6 then
- conn, handler = socket.tcp6();
- end
-
- if not conn then
- log("warn", "Failed to create outgoing connection, system error: %s", handler);
- return false, handler;
- end
-
- conn:settimeout(0);
- local success, err = conn:connect(connect_host.addr, connect_port);
- if not success and err ~= "timeout" then
- log("warn", "s2s connect() to %s (%s:%d) failed: %s", host_session.to_host, connect_host.addr, connect_port, err);
- return false, err;
- end
-
- local cl = connlisteners_get("xmppserver");
- conn = wrapclient(conn, connect_host.addr, connect_port, cl, cl.default_mode or 1 );
- host_session.conn = conn;
-
- local filter = initialize_filters(host_session);
- local w, log = conn.write, host_session.log;
- host_session.sends2s = function (t)
- log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
- if t.name then
- t = filter("stanzas/out", t);
- end
- if t then
- t = filter("bytes/out", tostring(t));
- if t then
- return w(conn, tostring(t));
- end
- end
- end
-
- -- Register this outgoing connection so that xmppserver_listener knows about it
- -- otherwise it will assume it is a new incoming connection
- cl.register_outgoing(conn, host_session);
-
- host_session:open_stream(from_host, to_host);
-
- log("debug", "Connection attempt in progress...");
- add_task(connect_timeout, function ()
- if host_session.conn ~= conn or
- host_session.type == "s2sout" or
- host_session.connecting then
- return; -- Ok, we're connect[ed|ing]
- end
- -- Not connected, need to close session and clean up
- (host_session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity",
- host_session.from_host or "(unknown)", host_session.to_host or "(unknown)");
- host_session:close("connection-timeout");
- end);
- return true;
-end
-
-function session_open_stream(session, from, to)
- session.sends2s(st.stanza("stream:stream", {
- xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
- ["xmlns:stream"]='http://etherx.jabber.org/streams',
- from=from, to=to, version='1.0', ["xml:lang"]='en'}):top_tag());
-end
-
-local function check_cert_status(session)
- local conn = session.conn:socket()
- local cert
- if conn.getpeercertificate then
- cert = conn:getpeercertificate()
- end
-
- if cert then
- local chain_valid, errors = conn:getpeerverification()
- -- Is there any interest in printing out all/the number of errors here?
- if not chain_valid then
- (session.log or log)("debug", "certificate chain validation result: invalid");
- session.cert_chain_status = "invalid";
- else
- (session.log or log)("debug", "certificate chain validation result: valid");
- session.cert_chain_status = "valid";
-
- local host;
- if session.direction == "incoming" then
- host = session.from_host;
- else
- host = session.to_host;
- end
-
- -- We'll go ahead and verify the asserted identity if the
- -- connecting server specified one.
- if host then
- if cert_verify_identity(host, "xmpp-server", cert) then
- session.cert_identity_status = "valid"
- else
- session.cert_identity_status = "invalid"
- end
- end
- end
- end
-end
-
-function streamopened(session, attr)
- local send = session.sends2s;
-
- -- TODO: #29: SASL/TLS on s2s streams
- session.version = tonumber(attr.version) or 0;
-
- -- TODO: Rename session.secure to session.encrypted
- if session.secure == false then
- session.secure = true;
- end
-
- if session.direction == "incoming" then
- -- Send a reply stream header
- session.to_host = attr.to and nameprep(attr.to);
- session.from_host = attr.from and nameprep(attr.from);
-
- session.streamid = uuid_gen();
- (session.log or log)("debug", "Incoming s2s received %s", st.stanza("stream:stream", attr):top_tag());
- if session.to_host then
- if not hosts[session.to_host] then
- -- Attempting to connect to a host we don't serve
- session:close({
- condition = "host-unknown";
- text = "This host does not serve "..session.to_host
- });
- return;
- elseif hosts[session.to_host].disallow_s2s then
- -- Attempting to connect to a host that disallows s2s
- session:close({
- condition = "policy-violation";
- text = "Server-to-server communication is not allowed to this host";
- });
- return;
- end
- end
-
- if session.secure and not session.cert_chain_status then check_cert_status(session); end
-
- send("<?xml version='1.0'?>");
- send(stanza("stream:stream", { xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
- ["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.to_host, to=session.from_host, version=(session.version > 0 and "1.0" or nil) }):top_tag());
- if session.version >= 1.0 then
- local features = st.stanza("stream:features");
-
- if session.to_host then
- hosts[session.to_host].events.fire_event("s2s-stream-features", { origin = session, features = features });
- else
- (session.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", session.from_host or "unknown host");
- end
-
- log("debug", "Sending stream features: %s", tostring(features));
- send(features);
- end
- elseif session.direction == "outgoing" then
- -- If we are just using the connection for verifying dialback keys, we won't try and auth it
- if not attr.id then error("stream response did not give us a streamid!!!"); end
- session.streamid = attr.id;
-
- if session.secure and not session.cert_chain_status then check_cert_status(session); end
-
- -- Send unauthed buffer
- -- (stanzas which are fine to send before dialback)
- -- Note that this is *not* the stanza queue (which
- -- we can only send if auth succeeds) :)
- local send_buffer = session.send_buffer;
- if send_buffer and #send_buffer > 0 then
- log("debug", "Sending s2s send_buffer now...");
- for i, data in ipairs(send_buffer) do
- session.sends2s(tostring(data));
- send_buffer[i] = nil;
- end
- end
- session.send_buffer = nil;
-
- -- If server is pre-1.0, don't wait for features, just do dialback
- if session.version < 1.0 then
- if not session.dialback_verifying then
- log("debug", "Initiating dialback...");
- initiate_dialback(session);
- else
- mark_connected(session);
- end
- end
- end
- session.notopen = nil;
-end
-
-function streamclosed(session)
- (session.log or log)("debug", "Received </stream:stream>");
- session:close();
-end
-
-function initiate_dialback(session)
- -- generate dialback key
- session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host);
- session.sends2s(format("<db:result from='%s' to='%s'>%s</db:result>", session.from_host, session.to_host, session.dialback_key));
- session.log("info", "sent dialback key on outgoing s2s stream");
-end
-
-function generate_dialback(id, to, from)
- return sha256_hash(id..to..from..hosts[from].dialback_secret, true);
-end
-
-function verify_dialback(id, to, from, key)
- return key == generate_dialback(id, to, from);
+ 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 make_authenticated(session, host)
@@ -678,10 +113,7 @@ function mark_connected(session)
local from, to = session.from_host, session.to_host;
session.log("info", session.direction.." s2s connection "..from.."->"..to.." complete");
-
- local send_to_host = send_to_host;
- function session.send(data) return send_to_host(to, from, data); end
-
+
local event_data = { session = session };
if session.type == "s2sout" then
prosody.events.fire_event("s2sout-established", event_data);
@@ -701,6 +133,7 @@ function mark_connected(session)
session.sendq = nil;
end
+ session.ip_hosts = nil;
session.srv_hosts = nil;
end
end
@@ -738,7 +171,7 @@ function destroy_session(session, reason)
if session.direction == "outgoing" then
hosts[session.from_host].s2sout[session.to_host] = nil;
- bounce_sendq(session, reason);
+ session:bounce_sendq(reason);
elseif session.direction == "incoming" then
incoming_s2s[session] = nil;
end
diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua
index b1ec819f..c101bf4e 100644
--- a/core/sessionmanager.lua
+++ b/core/sessionmanager.lua
@@ -6,11 +6,8 @@
-- COPYING file in the source package for more information.
--
-
-
local tonumber, tostring, setmetatable = tonumber, tostring, setmetatable;
local ipairs, pairs, print, next= ipairs, pairs, print, next;
-local format = string.format;
local hosts = hosts;
local full_sessions = full_sessions;
@@ -19,12 +16,11 @@ local bare_sessions = bare_sessions;
local logger = require "util.logger";
local log = logger.init("sessionmanager");
local error = error;
-local uuid_generate = require "util.uuid".generate;
local rm_load_roster = require "core.rostermanager".load_roster;
local config_get = require "core.configmanager".get;
-local nameprep = require "util.encodings".stringprep.nameprep;
local resourceprep = require "util.encodings".stringprep.resourceprep;
local nodeprep = require "util.encodings".stringprep.nodeprep;
+local uuid_generate = require "util.uuid".generate;
local initialize_filters = require "util.filters".initialize;
local fire_event = prosody.events.fire_event;
@@ -33,8 +29,6 @@ local gettime = require "socket".gettime;
local st = require "util.stanza";
-local c2s_timeout = config_get("*", "core", "c2s_timeout");
-
local newproxy = newproxy;
local getmetatable = getmetatable;
@@ -67,14 +61,6 @@ function new_session(conn)
session.ip = conn:ip();
local conn_name = "c2s"..tostring(conn):match("[a-f0-9]+$");
session.log = logger.init(conn_name);
-
- if c2s_timeout then
- add_task(c2s_timeout, function ()
- if session.type == "c2s_unauthed" then
- session:close("connection-timeout");
- end
- end);
- end
return session;
end
@@ -216,50 +202,6 @@ function bind_resource(session, resource)
return true;
end
-function streamopened(session, attr)
- local send = session.send;
- session.host = attr.to;
- if not session.host then
- session:close{ condition = "improper-addressing",
- text = "A 'to' attribute is required on stream headers" };
- return;
- end
- session.host = nameprep(session.host);
- session.version = tonumber(attr.version) or 0;
- session.streamid = uuid_generate();
- (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host);
-
- if not hosts[session.host] then
- -- We don't serve this host...
- session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)};
- return;
- end
-
- send("<?xml version='1.0'?>");
- send(format("<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='%s' from='%s' version='1.0' xml:lang='en'>", session.streamid, session.host));
-
- (session.log or log)("debug", "Sent reply <stream:stream> to client");
- session.notopen = nil;
-
- -- If session.secure is *false* (not nil) then it means we /were/ encrypting
- -- since we now have a new stream header, session is secured
- if session.secure == false then
- session.secure = true;
- end
-
- local features = st.stanza("stream:features");
- hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
- fire_event("stream-features", session, features);
-
- send(features);
-
-end
-
-function streamclosed(session)
- session.log("debug", "Received </stream:stream>");
- session:close();
-end
-
function send_to_available_resources(user, host, stanza)
local jid = user.."@"..host;
local count = 0;
diff --git a/core/stanza_router.lua b/core/stanza_router.lua
index 406ad2f0..54c5a1a6 100644
--- a/core/stanza_router.lua
+++ b/core/stanza_router.lua
@@ -11,7 +11,6 @@ local log = require "util.logger".init("stanzarouter")
local hosts = _G.prosody.hosts;
local tostring = tostring;
local st = require "util.stanza";
-local send_s2s = require "core.s2smanager".send_to_host;
local jid_split = require "util.jid".split;
local jid_prepped_split = require "util.jid".prepped_split;
@@ -105,11 +104,6 @@ function core_process_stanza(origin, stanza)
stanza.attr.from = from;
end
- --[[if to and not(hosts[to]) and not(hosts[to_bare]) and (hosts[host] and hosts[host].type ~= "local") then -- not for us?
- log("warn", "stanza recieved for a non-local server");
- return; -- FIXME what should we do here?
- end]] -- FIXME
-
if (origin.type == "s2sin" or origin.type == "c2s" or origin.type == "component") and xmlns == nil then
if origin.type == "s2sin" and not origin.dummy then
local host_status = origin.hosts[from_host];
@@ -189,26 +183,18 @@ function core_route_stanza(origin, stanza)
if hosts[host] then
-- old stanza routing code removed
core_post_stanza(origin, stanza);
- elseif origin.type == "c2s" then
- -- Remote host
+ else
+ log("debug", "Routing to remote...");
if not hosts[from_host] then
log("error", "No hosts[from_host] (please report): %s", tostring(stanza));
- end
- if (not hosts[from_host]) or (not hosts[from_host].disallow_s2s) then
+ else
local xmlns = stanza.attr.xmlns;
- --stanza.attr.xmlns = "jabber:server";
stanza.attr.xmlns = nil;
- log("debug", "sending s2s stanza: %s", tostring(stanza.top_tag and stanza:top_tag()) or stanza);
- send_s2s(origin.host, host, stanza); -- TODO handle remote routing errors
+ local routed = prosody.events.fire_event("route/remote", { origin = origin, stanza = stanza, from_host = from_host, to_host = host }); --FIXME: Should be per-host (shared modules!)
stanza.attr.xmlns = xmlns; -- reset
- else
- core_route_stanza(hosts[from_host], st.error_reply(stanza, "cancel", "not-allowed", "Communication with remote servers is not allowed"));
+ if routed == nil then
+ core_route_stanza(hosts[from_host], st.error_reply(stanza, "cancel", "not-allowed", "Communication with remote domains is not enabled"));
+ end
end
- elseif origin.type == "component" or origin.type == "local" then
- -- Route via s2s for components and modules
- log("debug", "Routing outgoing stanza for %s to %s", from_host, host);
- send_s2s(from_host, host, stanza);
- else
- log("warn", "received %s stanza from unhandled connection type: %s", tostring(stanza.name), tostring(origin.type));
end
end