diff options
Diffstat (limited to 'core')
-rw-r--r-- | core/actions.lua | 27 | ||||
-rw-r--r-- | core/certmanager.lua | 62 | ||||
-rw-r--r-- | core/componentmanager.lua | 28 | ||||
-rw-r--r-- | core/configmanager.lua | 24 | ||||
-rw-r--r-- | core/hostmanager.lua | 38 | ||||
-rw-r--r-- | core/loggingmanager.lua | 12 | ||||
-rw-r--r-- | core/modulemanager.lua | 119 | ||||
-rw-r--r-- | core/objectmanager.lua | 68 | ||||
-rw-r--r-- | core/s2smanager.lua | 84 | ||||
-rw-r--r-- | core/sessionmanager.lua | 19 | ||||
-rw-r--r-- | core/stanza_router.lua | 15 | ||||
-rw-r--r-- | core/xmlhandlers.lua | 181 |
12 files changed, 395 insertions, 282 deletions
diff --git a/core/actions.lua b/core/actions.lua deleted file mode 100644 index 5c2525e0..00000000 --- a/core/actions.lua +++ /dev/null @@ -1,27 +0,0 @@ --- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -
-local actions = {};
-
-function register(path, t)
- local curr = actions;
- for comp in path:gmatch("([^/]+)/") do
- if curr[comp] == nil then
- curr[comp] = {};
- end
- curr = curr[comp];
- if type(curr) ~= "table" then
- return nil, "path-taken";
- end
- end
- curr[path:match("/([^/]+)$")] = t;
- return true;
-end
-
-return { actions = actions, register= register };
\ No newline at end of file diff --git a/core/certmanager.lua b/core/certmanager.lua new file mode 100644 index 00000000..5794ba6e --- /dev/null +++ b/core/certmanager.lua @@ -0,0 +1,62 @@ +local configmanager = require "core.configmanager"; +local log = require "util.logger".init("certmanager"); +local ssl = ssl; +local ssl_newcontext = ssl and ssl.newcontext; + +local setmetatable = setmetatable; + +local prosody = prosody; + +module "certmanager" + +-- These are the defaults if not overridden in the config +local default_ssl_ctx = { mode = "client", protocol = "sslv23", capath = "/etc/ssl/certs", verify = "none", options = "no_sslv2"; }; +local default_ssl_ctx_in = { mode = "server", protocol = "sslv23", capath = "/etc/ssl/certs", verify = "none", options = "no_sslv2"; }; + +local default_ssl_ctx_mt = { __index = default_ssl_ctx }; +local default_ssl_ctx_in_mt = { __index = default_ssl_ctx_in }; + +-- Global SSL options if not overridden per-host +local default_ssl_config = configmanager.get("*", "core", "ssl"); + +function create_context(host, mode, config) + local ssl_config = config and config.core.ssl or default_ssl_config; + if ssl and ssl_config then + local ctx, err = ssl_newcontext(setmetatable(ssl_config, mode == "client" and default_ssl_ctx_mt or default_ssl_ctx_in_mt)); + 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."; + else + reason = "Reason: "..tostring(reason or "unknown"):lower(); + end + log("error", "SSL/TLS: Failed to load %s: %s", file, reason); + else + log("error", "SSL/TLS: Error initialising for host %s: %s", host, err ); + end + ssl = false + end + return ctx, err; + end + return nil; +end + +function reload_ssl_config() + default_ssl_config = config.get("*", "core", "ssl"); +end + +prosody.events.add_handler("config-reloaded", reload_ssl_config); + +return _M; diff --git a/core/componentmanager.lua b/core/componentmanager.lua index a16c01d2..cc505894 100644 --- a/core/componentmanager.lua +++ b/core/componentmanager.lua @@ -8,15 +8,18 @@ local prosody = _G.prosody; local log = require "util.logger".init("componentmanager"); +local certmanager = require "core.certmanager"; local configmanager = require "core.configmanager"; local modulemanager = require "core.modulemanager"; local jid_split = require "util.jid".split; local fire_event = require "core.eventmanager".fire_event; local events_new = require "util.events".new; local st = require "util.stanza"; -local hosts = hosts; +local prosody, hosts = prosody, prosody.hosts; +local ssl = ssl; +local uuid_gen = require "util.uuid".generate; -local pairs, type, tostring = pairs, type, tostring; +local pairs, setmetatable, type, tostring = pairs, setmetatable, type, tostring; local components = {}; @@ -73,18 +76,25 @@ end function create_component(host, component, events) -- TODO check for host well-formedness - local ssl_ctx; - if host then + local ssl_ctx, ssl_ctx_in; + if host and ssl then -- We need to find SSL context to use... -- Discussion in prosody@ concluded that -- 1 level back is usually enough by default local base_host = host:gsub("^[^%.]+%.", ""); if hosts[base_host] then ssl_ctx = hosts[base_host].ssl_ctx; + ssl_ctx_in = hosts[base_host].ssl_ctx_in; + else + -- We have no cert, and no parent host to borrow a cert from + -- Use global/default cert if there is one + ssl_ctx = certmanager.create_context(host, "client"); + ssl_ctx_in = certmanager.create_context(host, "server"); end end return { type = "component", host = host, connected = true, s2sout = {}, - ssl_ctx = ssl_ctx, events = events or events_new() }; + ssl_ctx = ssl_ctx, ssl_ctx_in = ssl_ctx_in, events = events or events_new(), + dialback_secret = configmanager.get(host, "core", "dialback_secret") or uuid_gen() }; end function register_component(host, component, session) @@ -93,12 +103,16 @@ function register_component(host, component, session) components[host] = component; hosts[host] = session or create_component(host, component, old_events); - + -- Add events object if not already one if not hosts[host].events then hosts[host].events = old_events or events_new(); end - + + if not hosts[host].dialback_secret then + hosts[host].dialback_secret = configmanager.get(host, "core", "dialback_secret") or uuid_gen(); + end + -- add to disco_items if not(host:find("@", 1, true) or host:find("/", 1, true)) and host:find(".", 1, true) then disco_items:set(host:sub(host:find(".", 1, true)+1), host, true); diff --git a/core/configmanager.lua b/core/configmanager.lua index 1fbe83b8..0f20fd3e 100644 --- a/core/configmanager.lua +++ b/core/configmanager.lua @@ -9,8 +9,9 @@ local _G = _G; -local setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type = - setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type; +local setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table, format = + setmetatable, loadfile, pcall, rawget, rawset, io, error, dofile, type, pairs, table, string.format; + local eventmanager = require "core.eventmanager"; @@ -67,7 +68,7 @@ function load(filename, format) if parsers[format] and parsers[format].load then local f, err = io.open(filename); - if f then + if f then local ok, err = parsers[format].load(f:read("*a"), filename); f:close(); if ok then @@ -94,6 +95,15 @@ function addparser(format, parser) end end +-- _M needed to avoid name clash with local 'parsers' +function _M.parsers() + local p = {}; + for format in pairs(parsers) do + table.insert(p, format); + end + return p; +end + -- Built-in Lua parser do local loadstring, pcall, setmetatable = _G.loadstring, _G.pcall, _G.setmetatable; @@ -115,6 +125,10 @@ do rawset(env, "__currenthost", "*") -- Default is global function env.Host(name) + if rawget(config, name) and rawget(config[name].core, "component_module") then + error(format("Host %q clashes with previously defined %s Component %q, for services use a sub-domain like conference.%s", + name, config[name].core.component_module:gsub("^%a+$", { component = "external", muc = "MUC"}), name, name), 0); + end rawset(env, "__currenthost", name); -- Needs at least one setting to logically exist :) set(name or "*", "core", "defined", true); @@ -122,6 +136,10 @@ do env.host = env.Host; function env.Component(name) + if rawget(config, name) and rawget(config[name].core, "defined") and not rawget(config[name].core, "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(name, "core", "component_module", "component"); -- Don't load the global modules by default set(name, "core", "load_global_modules", false); diff --git a/core/hostmanager.lua b/core/hostmanager.lua index f89eaeba..7071296f 100644 --- a/core/hostmanager.lua +++ b/core/hostmanager.lua @@ -9,20 +9,19 @@ local ssl = ssl local hosts = hosts; +local certmanager = require "core.certmanager"; local configmanager = require "core.configmanager"; local eventmanager = require "core.eventmanager"; local modulemanager = require "core.modulemanager"; local events_new = require "util.events".new; +local uuid_gen = require "util.uuid".generate; + if not _G.prosody.incoming_s2s then require "core.s2smanager"; end local incoming_s2s = _G.prosody.incoming_s2s; --- These are the defaults if not overridden in the config -local default_ssl_ctx = { mode = "client", protocol = "sslv23", capath = "/etc/ssl/certs", verify = "none"; }; -local default_ssl_ctx_in = { mode = "server", protocol = "sslv23", capath = "/etc/ssl/certs", verify = "none"; }; - local log = require "util.logger".init("hostmanager"); local pairs, setmetatable = pairs, setmetatable; @@ -33,12 +32,19 @@ local hosts_loaded_once; local function load_enabled_hosts(config) local defined_hosts = config or configmanager.getconfig(); + local activated_any_host; for host, host_config in pairs(defined_hosts) do - if host ~= "*" and (host_config.core.enabled == nil or host_config.core.enabled) and not host_config.core.component_module then + if host ~= "*" and host_config.core.enabled ~= false and not host_config.core.component_module then + activated_any_host = true; activate(host, host_config); end end + + if not activated_any_host then + log("error", "No hosts defined in the config file. This may cause unexpected behaviour as no modules will be loaded."); + end + eventmanager.fire_event("hosts-activated", defined_hosts); hosts_loaded_once = true; end @@ -46,11 +52,12 @@ end eventmanager.add_event_hook("server-starting", load_enabled_hosts); function activate(host, host_config) - hosts[host] = {type = "local", connected = true, sessions = {}, - host = host, s2sout = {}, events = events_new(), - disallow_s2s = configmanager.get(host, "core", "disallow_s2s") - or (configmanager.get(host, "core", "anonymous_login") - and (configmanager.get(host, "core", "disallow_s2s") ~= false)) + hosts[host] = {type = "local", connected = true, sessions = {}, + host = host, s2sout = {}, events = events_new(), + disallow_s2s = configmanager.get(host, "core", "disallow_s2s") + or (configmanager.get(host, "core", "anonymous_login") + and (configmanager.get(host, "core", "disallow_s2s") ~= false)); + dialback_secret = configmanager.get(host, "core", "dialback_secret") or uuid_gen(); }; for option_name in pairs(host_config.core) do if option_name:match("_ports$") then @@ -58,14 +65,9 @@ function activate(host, host_config) end end - if ssl then - local ssl_config = host_config.core.ssl or configmanager.get("*", "core", "ssl"); - if ssl_config then - hosts[host].ssl_ctx = ssl.newcontext(setmetatable(ssl_config, { __index = default_ssl_ctx })); - hosts[host].ssl_ctx_in = ssl.newcontext(setmetatable(ssl_config, { __index = default_ssl_ctx_in })); - end - end - + hosts[host].ssl_ctx = certmanager.create_context(host, "client", host_config); -- for outgoing connections + hosts[host].ssl_ctx_in = certmanager.create_context(host, "server", host_config); -- for incoming connections + log((hosts_loaded_once and "info") or "debug", "Activated host: %s", host); eventmanager.fire_event("host-activated", host, host_config); end diff --git a/core/loggingmanager.lua b/core/loggingmanager.lua index 4154e1a7..1bf90db1 100644 --- a/core/loggingmanager.lua +++ b/core/loggingmanager.lua @@ -94,7 +94,7 @@ function apply_sink_rules(sink_type) end end elseif type(logging_config) == "string" and (not logging_config:match("^%*")) and sink_type == "file" then - -- User specified simply a filename, and the "file" sink type + -- User specified simply a filename, and the "file" sink type -- was just added for _, sink_config in pairs(default_file_logging) do sink_config.filename = logging_config; @@ -128,7 +128,7 @@ function get_levels(criteria, set) return set; elseif in_range then set[level] = true; - end + end end end @@ -161,12 +161,12 @@ function log_sink_types.stdout() if timestamps then io_write(os_date(timestamps), " "); end - if ... then + if ... then io_write(name, rep(" ", sourcewidth-namelen), level, "\t", format(message, ...), "\n"); else io_write(name, rep(" ", sourcewidth-namelen), level, "\t", message, "\n"); end - end + end end do @@ -197,7 +197,7 @@ do if timestamps then io_write(os_date(timestamps), " "); end - if ... then + if ... then io_write(name, rep(" ", sourcewidth-namelen), getstring(logstyles[level], level), "\t", format(message, ...), "\n"); else io_write(name, rep(" ", sourcewidth-namelen), getstring(logstyles[level], level), "\t", message, "\n"); @@ -237,7 +237,7 @@ function log_sink_types.file(config) if timestamps then write(logfile, os_date(timestamps), " "); end - if ... then + if ... then write(logfile, name, "\t", level, "\t", format(message, ...), "\n"); else write(logfile, name, "\t" , level, "\t", message, "\n"); diff --git a/core/modulemanager.lua b/core/modulemanager.lua index 9cd56187..1174352b 100644 --- a/core/modulemanager.lua +++ b/core/modulemanager.lua @@ -13,7 +13,6 @@ local log = logger.init("modulemanager"); local eventmanager = require "core.eventmanager"; local config = require "core.configmanager"; local multitable_new = require "util.multitable".new; -local register_actions = require "core.actions".register; local st = require "util.stanza"; local pluginloader = require "util.pluginloader"; @@ -28,7 +27,9 @@ local type = type; local next = next; local rawget = rawget; local error = error; -local tostring = tostring; +local tostring, tonumber = tostring, tonumber; + +local array, set = require "util.array", require "util.set"; local autoload_modules = {"presence", "message", "iq"}; @@ -126,6 +127,7 @@ function load(host, module_name, config) local api_instance = setmetatable({ name = module_name, host = host, config = config, _log = _log, log = function (self, ...) return _log(...); end }, { __index = api }); local pluginenv = setmetatable({ module = api_instance }, { __index = _G }); + api_instance.environment = pluginenv; setfenv(mod, pluginenv); if not hosts[host] then @@ -156,6 +158,7 @@ function load(host, module_name, config) 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); @@ -172,7 +175,7 @@ function is_loaded(host, name) end function unload(host, name, ...) - local mod = get_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 @@ -197,7 +200,17 @@ function unload(host, name, ...) end end hooks:remove(host, name); + if mod.module.items then -- remove items + for key,t in pairs(mod.module.items) do + for i = #t,1,-1 do + local value = t[i]; + t[i] = nil; + hosts[host].events.fire_event("item-removed/"..key, {source = self, item = value}); + end + end + end modulemap[host][name] = nil; + (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host }); return true; end @@ -278,7 +291,7 @@ function module_has_method(module, method) end function call_module_method(module, method, ...) - if module_has_method(module, method) then + if module_has_method(module, method) then local f = module.module[method]; return pcall(f, ...); else @@ -287,7 +300,7 @@ function call_module_method(module, method, ...) end ----- API functions exposed to modules ----------- --- Must all be in api.* +-- Must all be in api.* -- Returns the name of the current module function api:get_name() @@ -385,7 +398,7 @@ function api:require(lib) 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, setmetatable({ module = self }, { __index = _G })); + setfenv(f, self.environment); return f(); end @@ -400,6 +413,85 @@ function api:get_option(name, default_value) 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 t_remove = _G.table.remove; local module_items = multitable_new(); function api:add_item(key, value) @@ -440,19 +532,4 @@ function api:get_host_items(key) return result; end --------------------------------------------------------------------- - -local actions = {}; - -function actions.load(params) - --return true, "Module loaded ("..params.module.." on "..params.host..")"; - return load(params.host, params.module); -end - -function actions.unload(params) - return unload(params.host, params.module); -end - -register_actions("/modules", actions); - return _M; diff --git a/core/objectmanager.lua b/core/objectmanager.lua deleted file mode 100644 index e96cbd90..00000000 --- a/core/objectmanager.lua +++ /dev/null @@ -1,68 +0,0 @@ --- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -
-local new_multitable = require "util.multitable".new;
-local t_insert = table.insert;
-local t_concat = table.concat;
-local tostring = tostring;
-local unpack = unpack;
-local pairs = pairs;
-local error = error;
-local type = type;
-local _G = _G;
-
-local data = new_multitable();
-
-module "objectmanager"
-
-function set(...)
- return data:set(...);
-end
-function remove(...)
- return data:remove(...);
-end
-function get(...)
- return data:get(...);
-end
-
-local function get_path(path)
- if type(path) == "table" then return path; end
- local s = {};
- for part in tostring(path):gmatch("[%w_]+") do
- t_insert(s, part);
- end
- return s;
-end
-
-function get_object(path)
- path = get_path(path)
- return data:get(unpack(path)), path;
-end
-function set_object(path, object)
- path = get_path(path);
- data:set(unpack(path), object);
-end
-
-data:set("ls", function(_dir)
- local obj, dir = get_object(_dir);
- if not obj then error("object not found: " .. t_concat(dir, '/')); end
- local r = {};
- if type(obj) == "table" then
- for key, val in pairs(obj) do
- r[key] = type(val);
- end
- end
- return r;
-end);
-data:set("get", get_object);
-data:set("set", set_object);
-data:set("echo", function(...) return {...}; end);
-data:set("_G", _G);
-
-return _M;
diff --git a/core/s2smanager.lua b/core/s2smanager.lua index bfa3069a..16ede7b6 100644 --- a/core/s2smanager.lua +++ b/core/s2smanager.lua @@ -36,8 +36,6 @@ local log = logger_init("s2smanager"); local sha256_hash = require "util.hashes".sha256; -local dialback_secret = uuid_gen(); - local adns, dns = require "net.adns", require "net.dns"; local config = require "core.configmanager"; local connect_timeout = config.get("*", "core", "s2s_timeout") or 60; @@ -54,7 +52,7 @@ function compare_srv_priorities(a,b) return a.priority < b.priority or (a.priority == b.priority and a.weight > b.weight); end -local function bounce_sendq(session) +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)); @@ -72,6 +70,9 @@ local function bounce_sendq(session) reply.attr.type = "error"; reply:tag("error", {type = "cancel"}) :tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up(); + if reason then + reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):text("Connection failed: "..reason):up(); + end core_process_stanza(dummy, reply); end sendq[i] = nil; @@ -134,7 +135,7 @@ function new_incoming(conn) open_sessions = open_sessions + 1; local w, log = conn.write, logger_init("s2sin"..tostring(conn):match("[a-f0-9]+$")); session.log = log; - session.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(tostring(t)); end + session.sends2s = function (t) log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)")); w(conn, tostring(t)); end incoming_s2s[session] = true; add_task(connect_timeout, function () if session.conn ~= conn or @@ -142,16 +143,17 @@ function new_incoming(conn) return; -- Ok, we're connect[ed|ing] end -- Not connected, need to close session and clean up - (session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity", + (session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity", session.from_host or "(unknown)", session.to_host or "(unknown)"); session:close("connection-timeout"); end); return session; end -function new_outgoing(from_host, to_host) - local host_session = { to_host = to_host, from_host = from_host, host = from_host, - notopen = true, type = "s2sout_unauthed", direction = "outgoing" }; +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; @@ -162,10 +164,12 @@ function new_outgoing(from_host, to_host) host_session.log = log; end - -- Kick the connection attempting machine - attempt_connection(host_session); + if connect ~= false then + -- Kick the connection attempting machine into life + attempt_connection(host_session); + end - if not host_session.sends2s then + 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 @@ -179,7 +183,6 @@ function new_outgoing(from_host, to_host) buffer[#buffer+1] = data; log("debug", "Buffered item %d: %s", #buffer, tostring(data)); end - end return host_session; @@ -224,7 +227,7 @@ function attempt_connection(host_session, err) if not ok then if not attempt_connection(host_session, err) then -- No more attempts will be made - destroy_session(host_session); + destroy_session(host_session, err); end end end, "_xmpp-server._tcp."..connect_host..".", "SRV"); @@ -284,7 +287,7 @@ function try_connect(host_session, connect_host, connect_port) log("debug", "DNS lookup failed to get a response for %s", connect_host); if not attempt_connection(host_session, "name resolution failed") then -- Retry if we can log("debug", "No other records to try for %s - destroying", host_session.to_host); - destroy_session(host_session); -- End of the line, we can't + destroy_session(host_session, "DNS resolution failed"); -- End of the line, we can't end end end, connect_host, "A", "IN"); @@ -295,12 +298,12 @@ function try_connect(host_session, connect_host, connect_port) adns.cancel(handle, true); end end); - + return true; end function make_connect(host_session, connect_host, connect_port) - host_session.log("info", "Beginning new connection attempt to %s (%s:%d)", host_session.to_host, connect_host, connect_port); + (host_session.log or log)("info", "Beginning new connection attempt to %s (%s:%d)", host_session.to_host, connect_host, connect_port); -- Ok, we're going to try to connect local from_host, to_host = host_session.from_host, host_session.to_host; @@ -320,7 +323,7 @@ function make_connect(host_session, connect_host, connect_port) end local cl = connlisteners_get("xmppserver"); - conn = wrapclient(conn, connect_host, connect_port, cl, cl.default_mode or 1, hosts[from_host].ssl_ctx, false ); + conn = wrapclient(conn, connect_host, connect_port, cl, cl.default_mode or 1 ); host_session.conn = conn; -- Register this outgoing connection so that xmppserver_listener knows about it @@ -328,9 +331,10 @@ function make_connect(host_session, connect_host, connect_port) cl.register_outgoing(conn, host_session); local w, log = conn.write, host_session.log; - host_session.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(tostring(t)); end + host_session.sends2s = function (t) log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?")); w(conn, tostring(t)); end + + host_session:open_stream(from_host, to_host); - conn.write(format([[<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' from='%s' to='%s' version='1.0' xml:lang='en'>]], from_host, to_host)); log("debug", "Connection attempt in progress..."); add_task(connect_timeout, function () if host_session.conn ~= conn or @@ -339,13 +343,20 @@ function make_connect(host_session, connect_host, connect_port) 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.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 + function streamopened(session, attr) local send = session.sends2s; @@ -357,7 +368,6 @@ function streamopened(session, attr) end if session.version >= 1.0 and not (attr.to and attr.from) then - (session.log or log)("warn", "Remote of stream "..(session.from_host or "(unknown)").."->"..(session.to_host or "(unknown)") .." failed to specify to (%s) and/or from (%s) hostname as per RFC", tostring(attr.to), tostring(attr.from)); end @@ -369,19 +379,19 @@ function streamopened(session, attr) session.streamid = uuid_gen(); (session.log or log)("debug", "incoming s2s received <stream:stream>"); - send("<?xml version='1.0'?>"); - send(stanza("stream:stream", { xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback', - ["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.to_host, version=(session.version > 0 and "1.0" or nil) }):top_tag()); if session.to_host and not hosts[session.to_host] then -- Attempting to connect to a host we don't serve session:close({ condition = "host-unknown"; text = "This host does not serve "..session.to_host }); return; end + 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", { session = session, features = features }); + 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 @@ -396,7 +406,7 @@ function streamopened(session, attr) -- Send unauthed buffer -- (stanzas which are fine to send before dialback) - -- Note that this is *not* the stanza queue (which + -- 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 @@ -418,7 +428,6 @@ function streamopened(session, attr) end end end - session.notopen = nil; end @@ -438,7 +447,7 @@ function initiate_dialback(session) end function generate_dialback(id, to, from) - return sha256_hash(id..to..from..dialback_secret, true); + return sha256_hash(id..to..from..hosts[from].dialback_secret, true); end function verify_dialback(id, to, from, key) @@ -446,6 +455,16 @@ function verify_dialback(id, to, from, key) end function make_authenticated(session, host) + if not session.secure then + local local_host = session.direction == "incoming" and session.to_host or session.from_host; + if config.get(local_host, "core", "s2s_require_encryption") then + session:close({ + condition = "policy-violation", + text = "Encrypted server-to-server communication is required but was not " + ..((session.direction == "outgoing" and "offered") or "used") + }); + end + end if session.type == "s2sout_unauthed" then session.type = "s2sout"; elseif session.type == "s2sin_unauthed" then @@ -491,12 +510,14 @@ function mark_connected(session) end end -function destroy_session(session) +local function null_data_handler(conn, data) log("debug", "Discarding data from destroyed s2s session: %s", data); end + +function destroy_session(session, reason) (session.log or log)("info", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host)); if session.direction == "outgoing" then hosts[session.from_host].s2sout[session.to_host] = nil; - bounce_sendq(session); + bounce_sendq(session, reason); elseif session.direction == "incoming" then incoming_s2s[session] = nil; end @@ -506,6 +527,7 @@ function destroy_session(session) session[k] = nil; end end + session.data = null_data_handler; end return _M; diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 5e7fe06d..29adcfbb 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -10,7 +10,6 @@ local tonumber, tostring = tonumber, tostring; local ipairs, pairs, print, next= ipairs, pairs, print, next; -local collectgarbage = collectgarbage; local format = import("string", "format"); local hosts = hosts; @@ -25,6 +24,7 @@ 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 fire_event = require "core.eventmanager".fire_event; local add_task = require "util.timer".add_task; @@ -50,8 +50,8 @@ function new_session(conn) open_sessions = open_sessions + 1; log("debug", "open sessions now: ".. open_sessions); local w = conn.write; - session.send = function (t) w(tostring(t)); end - session.ip = conn.ip(); + session.send = function (t) w(conn, tostring(t)); end + session.ip = conn:ip(); local conn_name = "c2s"..tostring(conn):match("[a-f0-9]+$"); session.log = logger.init(conn_name); @@ -66,21 +66,23 @@ function new_session(conn) return session; end +local function null_data_handler(conn, data) log("debug", "Discarding data from destroyed c2s session: %s", data); end + function destroy_session(session, err) (session.log or log)("info", "Destroying session for %s (%s@%s)", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)"); -- Remove session/resource from user's session list if session.full_jid then - hosts[session.host].events.fire_event("resource-unbind", {session=session, error=err}); - hosts[session.host].sessions[session.username].sessions[session.resource] = nil; full_sessions[session.full_jid] = nil; - + 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; bare_sessions[session.username..'@'..session.host] = nil; end + + hosts[session.host].events.fire_event("resource-unbind", {session=session, error=err}); end for k in pairs(session) do @@ -88,6 +90,7 @@ function destroy_session(session, err) session[k] = nil; end end + session.data = null_data_handler; end function make_authenticated(session, username) @@ -106,7 +109,8 @@ function bind_resource(session, resource) if session.resource then return nil, "cancel", "already-bound", "Cannot bind multiple resources on a single connection"; end -- We don't support binding multiple resources - resource = resource or uuid_generate(); + resource = resourceprep(resource); + resource = resource ~= "" and resource or uuid_generate(); --FIXME: Randomly-generated resources must be unique per-user, and never conflict with existing if not hosts[session.host].sessions[session.username] then @@ -189,6 +193,7 @@ function streamopened(session, attr) 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); diff --git a/core/stanza_router.lua b/core/stanza_router.lua index 00c37ed7..72ddebd1 100644 --- a/core/stanza_router.lua +++ b/core/stanza_router.lua @@ -98,7 +98,7 @@ function core_process_stanza(origin, stanza) return; -- FIXME what should we do here? does this work with subdomains? end end - core_post_stanza(origin, stanza); + core_post_stanza(origin, stanza, origin.full_jid); else local h = hosts[stanza.attr.to or origin.host or origin.to_host]; if h then @@ -119,12 +119,12 @@ function core_process_stanza(origin, stanza) end end -function core_post_stanza(origin, stanza) +function core_post_stanza(origin, stanza, preevents) local to = stanza.attr.to; local node, host, resource = jid_split(to); local to_bare = node and (node.."@"..host) or host; -- bare JID - local to_type; + local to_type, to_self; if node then if resource then to_type = '/full'; @@ -132,6 +132,7 @@ function core_post_stanza(origin, stanza) to_type = '/bare'; if node == origin.username and host == origin.host then stanza.attr.to = nil; + to_self = true; end end else @@ -139,16 +140,18 @@ function core_post_stanza(origin, stanza) to_type = '/host'; else to_type = '/bare'; + to_self = true; end end local event_data = {origin=origin, stanza=stanza}; - if origin.full_jid == stanza.attr.from then -- c2s connection + if preevents then -- c2s connection if hosts[origin.host].events.fire_event('pre-'..stanza.name..to_type, event_data) then return; end -- do preprocessing end local h = hosts[to_bare] or hosts[host or origin.host]; if h then if h.events.fire_event(stanza.name..to_type, event_data) then return; end -- do processing + if to_self and h.events.fire_event(stanza.name..'/self', event_data) then return; end -- do processing if h.type == "component" then component_handle_stanza(origin, stanza); @@ -180,7 +183,7 @@ function core_route_stanza(origin, stanza) local xmlns = stanza.attr.xmlns; --stanza.attr.xmlns = "jabber:server"; stanza.attr.xmlns = nil; - log("debug", "sending s2s stanza: %s", tostring(stanza)); + 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 stanza.attr.xmlns = xmlns; -- reset else @@ -191,6 +194,6 @@ function core_route_stanza(origin, stanza) log("debug", "Routing outgoing stanza for %s to %s", from_host, host); send_s2s(from_host, host, stanza); else - log("warn", "received stanza from unhandled connection type: %s", origin.type); + log("warn", "received %s stanza from unhandled connection type: %s", tostring(stanza.name), tostring(origin.type)); end end diff --git a/core/xmlhandlers.lua b/core/xmlhandlers.lua index 82c2d0b8..77f00bea 100644 --- a/core/xmlhandlers.lua +++ b/core/xmlhandlers.lua @@ -12,8 +12,6 @@ require "util.stanza" local st = stanza; local tostring = tostring; -local pairs = pairs; -local ipairs = ipairs; local t_insert = table.insert; local t_concat = table.concat; @@ -24,103 +22,92 @@ local error = error; module "xmlhandlers" local ns_prefixes = { - ["http://www.w3.org/XML/1998/namespace"] = "xml"; - } + ["http://www.w3.org/XML/1998/namespace"] = "xml"; +}; + +local xmlns_streams = "http://etherx.jabber.org/streams"; + +local ns_separator = "\1"; +local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$"; function init_xmlhandlers(session, stream_callbacks) - local ns_stack = { "" }; - local curr_tag; - local chardata = {}; - local xml_handlers = {}; - local log = session.log or default_log; - - local cb_streamopened = stream_callbacks.streamopened; - local cb_streamclosed = stream_callbacks.streamclosed; - local cb_error = stream_callbacks.error or function (session, e) error("XML stream error: "..tostring(e)); end; - local cb_handlestanza = stream_callbacks.handlestanza; - - local stream_tag = stream_callbacks.stream_tag; - local stream_default_ns = stream_callbacks.default_ns; - - local stanza - function xml_handlers:StartElement(tagname, attr) - if stanza and #chardata > 0 then - -- We have some character data in the buffer - stanza:text(t_concat(chardata)); - chardata = {}; - end - local curr_ns,name = tagname:match("^([^\1]*)\1?(.*)$"); - if name == "" then - curr_ns, name = "", curr_ns; - end + local chardata = {}; + local xml_handlers = {}; + local log = session.log or default_log; + + local cb_streamopened = stream_callbacks.streamopened; + local cb_streamclosed = stream_callbacks.streamclosed; + local cb_error = stream_callbacks.error or function(session, e) error("XML stream error: "..tostring(e)); end; + local cb_handlestanza = stream_callbacks.handlestanza; + + local stream_ns = stream_callbacks.stream_ns or xmlns_streams; + local stream_tag = stream_ns..ns_separator..(stream_callbacks.stream_tag or "stream"); + local stream_error_tag = stream_ns..ns_separator..(stream_callbacks.error_tag or "error"); + + local stream_default_ns = stream_callbacks.default_ns; + + local stanza; + function xml_handlers:StartElement(tagname, attr) + if stanza and #chardata > 0 then + -- We have some character data in the buffer + stanza:text(t_concat(chardata)); + chardata = {}; + end + local curr_ns,name = tagname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end - if curr_ns ~= stream_default_ns then - attr.xmlns = curr_ns; - end - - -- FIXME !!!!! - for i=1,#attr do - local k = attr[i]; - attr[i] = nil; - local ns, nm = k:match("^([^\1]*)\1?(.*)$"); - if nm ~= "" then - ns = ns_prefixes[ns]; - if ns then - attr[ns..":"..nm] = attr[k]; - attr[k] = nil; - end - end - end - - if not stanza then --if we are not currently inside a stanza - if session.notopen then - if tagname == stream_tag then - if cb_streamopened then - cb_streamopened(session, attr); - end - else - -- Garbage before stream? - cb_error(session, "no-stream"); - end - return; - end - if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then - cb_error(session, "invalid-top-level-element"); - end - - stanza = st.stanza(name, attr); - curr_tag = stanza; - else -- we are inside a stanza, so add a tag - attr.xmlns = nil; - if curr_ns ~= stream_default_ns then - attr.xmlns = curr_ns; - end - stanza:tag(name, attr); - end + if curr_ns ~= stream_default_ns then + attr.xmlns = curr_ns; end - function xml_handlers:CharacterData(data) - if stanza then - t_insert(chardata, data); + + -- FIXME !!!!! + for i=1,#attr do + local k = attr[i]; + attr[i] = nil; + local ns, nm = k:match(ns_pattern); + if nm ~= "" then + ns = ns_prefixes[ns]; + if ns then + attr[ns..":"..nm] = attr[k]; + attr[k] = nil; + end end end - function xml_handlers:EndElement(tagname) - local curr_ns,name = tagname:match("^([^\1]*)\1?(.*)$"); - if name == "" then - curr_ns, name = "", curr_ns; - end - if (not stanza) or (#stanza.last_add > 0 and name ~= stanza.last_add[#stanza.last_add].name) then + + if not stanza then --if we are not currently inside a stanza + if session.notopen then if tagname == stream_tag then - if cb_streamclosed then - cb_streamclosed(session); + if cb_streamopened then + cb_streamopened(session, attr); end - elseif name == "error" then - cb_error(session, "stream-error", stanza); else - cb_error(session, "parse-error", "unexpected-element-close", name); + -- Garbage before stream? + cb_error(session, "no-stream"); end - stanza, chardata = nil, {}; return; end + if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then + cb_error(session, "invalid-top-level-element"); + end + + stanza = st.stanza(name, attr); + else -- we are inside a stanza, so add a tag + attr.xmlns = nil; + if curr_ns ~= stream_default_ns then + attr.xmlns = curr_ns; + end + stanza:tag(name, attr); + end + end + function xml_handlers:CharacterData(data) + if stanza then + t_insert(chardata, data); + end + end + function xml_handlers:EndElement(tagname) + if stanza then if #chardata > 0 then -- We have some character data in the buffer stanza:text(t_concat(chardata)); @@ -128,12 +115,30 @@ function init_xmlhandlers(session, stream_callbacks) end -- Complete stanza if #stanza.last_add == 0 then - cb_handlestanza(session, stanza); + if tagname ~= stream_error_tag then + cb_handlestanza(session, stanza); + else + cb_error(session, "stream-error", stanza); + end stanza = nil; else stanza:up(); end + else + if tagname == stream_tag then + if cb_streamclosed then + cb_streamclosed(session); + end + else + local curr_ns,name = tagname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end + cb_error(session, "parse-error", "unexpected-element-close", name); + end + stanza, chardata = nil, {}; end + end return xml_handlers; end |