diff options
78 files changed, 3559 insertions, 2130 deletions
@@ -28,9 +28,11 @@ install: prosody.install prosodyctl.install prosody.cfg.lua.install util/encodin install -m644 net/* $(SOURCE)/net install -m644 util/* $(SOURCE)/util install -m644 fallbacks/* $(SOURCE)/fallbacks - install -m644 plugins/* $(MODULES) + install -m644 plugins/*.lua $(MODULES) + install -d $(MODULES)/muc + install -m644 plugins/muc/* $(MODULES)/muc install -m644 certs/* $(CONFIG)/certs - install -m644 plugins/* $(MODULES) + install -m644 plugins/*.lua $(MODULES) install -m644 man/prosodyctl.man $(MAN)/man1/prosodyctl.1 test -e $(CONFIG)/prosody.cfg.lua || install -m644 prosody.cfg.lua.install $(CONFIG)/prosody.cfg.lua test -e prosody.version && install prosody.version $(SOURCE)/prosody.version || true @@ -71,7 +73,7 @@ prosody.cfg.lua.install: sed 's|certs/|$(INSTALLEDCONFIG)/certs/|' prosody.cfg.lua.dist > prosody.cfg.lua.install prosody.release: - test -e .hg/dirstate && hexdump -n6 -e'6/1 "%01x"' .hg/dirstate \ + test -e .hg/dirstate && hexdump -n6 -e'6/1 "%02x"' .hg/dirstate \ > prosody.version || true prosody.version: prosody.release diff --git a/certs/localhost.cert b/certs/localhost.cert index 2459b913..5156d307 100644 --- a/certs/localhost.cert +++ b/certs/localhost.cert @@ -1,22 +1,22 @@ -----BEGIN CERTIFICATE----- -MIIDkDCCAvmgAwIBAgIJAO6CeZTVrfDwMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD -VQQGEwJHQjETMBEGA1UECBMKU29tZS1TdGF0ZTETMBEGA1UEBxMKSmFiYmVybGFu -ZDETMBEGA1UEChMKUHJvc29keSBJTTEcMBoGA1UEAxMTRXhhbXBsZSBjZXJ0aWZp -Y2F0ZTEhMB8GCSqGSIb3DQEJARYScHJvc29keUBwcm9zb2R5LmltMB4XDTA4MTEy -OTE3MTQyNFoXDTA5MTEyOTE3MTQyNFowgY0xCzAJBgNVBAYTAkdCMRMwEQYDVQQI -EwpTb21lLVN0YXRlMRMwEQYDVQQHEwpKYWJiZXJsYW5kMRMwEQYDVQQKEwpQcm9z -b2R5IElNMRwwGgYDVQQDExNFeGFtcGxlIGNlcnRpZmljYXRlMSEwHwYJKoZIhvcN -AQkBFhJwcm9zb2R5QHByb3NvZHkuaW0wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ -AoGBALVAPZ/hONuU5P1okNPNfE/bSDj3AsOrRb+Kj4a7MPyRzVCARAm5KvCkPwI3 -zfDoemp6PpjVk+K8buYTKD+FT3ZxHu8mVHOnnDid/Z3KjxXOh0q1fnzKCCWH49Lu -hKz7AtAXxvyGvTqTrfquxYVu3U4jxNIVdy//8K0+qPt69aJTAgMBAAGjgfUwgfIw -HQYDVR0OBBYEFA7Ehhe9zSpASafg6MXFXjAA5jTcMIHCBgNVHSMEgbowgbeAFA7E -hhe9zSpASafg6MXFXjAA5jTcoYGTpIGQMIGNMQswCQYDVQQGEwJHQjETMBEGA1UE -CBMKU29tZS1TdGF0ZTETMBEGA1UEBxMKSmFiYmVybGFuZDETMBEGA1UEChMKUHJv -c29keSBJTTEcMBoGA1UEAxMTRXhhbXBsZSBjZXJ0aWZpY2F0ZTEhMB8GCSqGSIb3 -DQEJARYScHJvc29keUBwcm9zb2R5LmltggkA7oJ5lNWt8PAwDAYDVR0TBAUwAwEB -/zANBgkqhkiG9w0BAQUFAAOBgQBCYiXpGULtMCsIi/yo3NxdeC7SjgsY8KKxxkB9 -VynZpC+R6+BMtEloOgl0uvjnGy1cu7l2ddQBN4NxpZjezo9KQjRjJxXSBgMKglXH -ybsPjB5b61zmCnr/uvjuthRCVuHfcVD0wptoHkb1VDd+lQT1/+QQCm1hlDbgb8NI -nfxA7A== +MIIDojCCAwugAwIBAgIJAPO1OI+vmUi8MA0GCSqGSIb3DQEBBQUAMIGTMQswCQYD +VQQGEwJHQjETMBEGA1UECBMKSmFiYmVybGFuZDETMBEGA1UEChMKUHJvc29keSBJ +TTE8MDoGA1UECxQzaHR0cDovL3Byb3NvZHkuaW0vZG9jL2FkdmFuY2VkX3NzbF90 +bHMjY2VydGlmaWNhdGVzMRwwGgYDVQQDExNFeGFtcGxlIGNlcnRpZmljYXRlMB4X +DTA5MTAxNzE3MDc1NloXDTEwMTAxNzE3MDc1NlowgZMxCzAJBgNVBAYTAkdCMRMw +EQYDVQQIEwpKYWJiZXJsYW5kMRMwEQYDVQQKEwpQcm9zb2R5IElNMTwwOgYDVQQL +FDNodHRwOi8vcHJvc29keS5pbS9kb2MvYWR2YW5jZWRfc3NsX3RscyNjZXJ0aWZp +Y2F0ZXMxHDAaBgNVBAMTE0V4YW1wbGUgY2VydGlmaWNhdGUwgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBAN5n5y7+A7V6WZ5n/+n4eqjHiQ+p0XD1BYA2435AgzKE +R+ilmrCFv59aWVIi3jS0YB3goMmuSk8PLv8pi/rjEKYhzDoiuoW/LvzjK5pVzbFM +NlkW5I0t4Lrjb2lMkxbQr/B/k07RDlJJJRTmr2j4N7vMoznVFbjQY6dRAv3svYZF +AgMBAAGjgfswgfgwHQYDVR0OBBYEFJhMTxNc3LEYA1vm3v4sCdHzRnUDMIHIBgNV +HSMEgcAwgb2AFJhMTxNc3LEYA1vm3v4sCdHzRnUDoYGZpIGWMIGTMQswCQYDVQQG +EwJHQjETMBEGA1UECBMKSmFiYmVybGFuZDETMBEGA1UEChMKUHJvc29keSBJTTE8 +MDoGA1UECxQzaHR0cDovL3Byb3NvZHkuaW0vZG9jL2FkdmFuY2VkX3NzbF90bHMj +Y2VydGlmaWNhdGVzMRwwGgYDVQQDExNFeGFtcGxlIGNlcnRpZmljYXRlggkA87U4 +j6+ZSLwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQCtLrTOSpQn+j+/ +5zoiP5wAGLpdZE+Iatzd26QwVsL61zd5399nEb1yFs3Hl9jo4W3idyNoofa67atX +2/+3juA0Q/oN/ZT16bWihmcrzv+Qd/CsQfMOZ5ApYV4SEw40L6GITtrZuBDjO4mU +TavhtScoGRzrZavhJG+PyhDH0Scglg== -----END CERTIFICATE----- diff --git a/certs/localhost.key b/certs/localhost.key index 8fb6e514..93fae5ed 100644 --- a/certs/localhost.key +++ b/certs/localhost.key @@ -1,15 +1,15 @@ -----BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQC1QD2f4TjblOT9aJDTzXxP20g49wLDq0W/io+GuzD8kc1QgEQJ -uSrwpD8CN83w6Hpqej6Y1ZPivG7mEyg/hU92cR7vJlRzp5w4nf2dyo8VzodKtX58 -ygglh+PS7oSs+wLQF8b8hr06k636rsWFbt1OI8TSFXcv//CtPqj7evWiUwIDAQAB -AoGAbk0w83oxite630hiUrMLguGUuy3/Xap+YMlm/PwwHJRyWRolzbEFI7sgqS3i -w0gHL4NDUuku/V3lM1jXNojfSNOq2T+M8L7G8q5e+Ch89RKiJvqKPqBsxu5bEL4m -lyJi+Vt0SXUqJkxBHWLRJb8W6++aM2ByZ7CKDyjomg5fplkCQQDhnMMIyVSKM7a1 -VTbUbeqfcJmyDRaCkbA5X7NsEtatrEWusulFtPExCUUdpgFACJYj35PhCqLzmCpJ -MxKL8zGdAkEAzanffEouT1eDlqdfLc/LVcKj3QTMmLck9KP0AhRy0vaiCqkYE/tE -M+l9HTwxGmveLngfuw8p0HdztUFO6lAYrwJBAJhpHzRjVfIa51XuoCC3tGVLWvj2 -cHt6UhMgPIRI4a/njhdrk7zcdIeM3J0f1P5eDpdjZXIEjnqDFCXpE6Fpg90CQC1l -a8FBlotI4/DjLO0tytI5TnZA0vB6rJubfQbggJ/0dLwpqvjuI5XZ2hYT7TrJyJc1 -SLu/kxlC5LWDnum1mF0CQDHt9x7DnGLquBhRUzcKmFcmaYsVl37A9tAfQSnrGqq+ -GBc3K1k0bhYc1/I1Ym1PfVCfLENXhhA0hHmaYviHF6U= +MIICWwIBAAKBgQDeZ+cu/gO1elmeZ//p+Hqox4kPqdFw9QWANuN+QIMyhEfopZqw +hb+fWllSIt40tGAd4KDJrkpPDy7/KYv64xCmIcw6IrqFvy784yuaVc2xTDZZFuSN +LeC6429pTJMW0K/wf5NO0Q5SSSUU5q9o+De7zKM51RW40GOnUQL97L2GRQIDAQAB +AoGAYaWw5Pr12en8CwaSX8GO6SeiT9Q5dqS9Y4u12iqs77MQd16uSi6O8YITkXJp +qS5AvR1wutvhGFEMS0+Me/zRw62OFc2VVrKmX6eqgRMR8d/+SZjqzUxb4pNIAPQU +dHbQzqGXermf6UWm6Cbi7vN0diohd8Qoj98PeWfRQrXju0kCQQD3OXD2SEevEhNe +g4YTREsyUkZV1etkldhAeDAJzlitCQdQF5zE9Wt/Ahv0BKlLTaz3mvSDwrI+lXYQ +1iDzOrXrAkEA5kzu1A3Y2gclyRupTg7crgp+afh1fLKCIVUaFdOYgwQDX90YnnIq +TaY4uQ8Eutoixha4ZM4/bJq17YjjY1O4jwJAZMEHNYftlv7h3/HwMWfy0XZQbej5 +vwuGj3er9EMhRpvYXB7TaD2w6pkcdU11BViJtntzTUOKyxC0hlYOJbJ2swJAOL3N +vhtnSVine6RAE4Zf4tWdDdj0gXOt0i6YjbYjhmwvtKfR0AAK4jTJFvdXT/48wReJ ++PRD9issFck7VRakiwJAPTgFUTsFCR1ZPcuCPHSCK/wz2NFma/O5Eqm0qTIbNUfw +3qDRyUuKbyr3bAc+K+asN5ok2PAnhiRUIpu146M17w== -----END RSA PRIVATE KEY----- diff --git a/core/componentmanager.lua b/core/componentmanager.lua index 08868236..a16c01d2 100644 --- a/core/componentmanager.lua +++ b/core/componentmanager.lua @@ -6,18 +6,15 @@ -- COPYING file in the source package for more information. -- - - -local prosody = prosody; +local prosody = _G.prosody; local log = require "util.logger".init("componentmanager"); local configmanager = require "core.configmanager"; local modulemanager = require "core.modulemanager"; -local core_route_stanza = core_route_stanza; local jid_split = require "util.jid".split; +local fire_event = require "core.eventmanager".fire_event; local events_new = require "util.events".new; local st = require "util.stanza"; local hosts = hosts; -local serialize = require "util.serialization".serialize local pairs, type, tostring = pairs, type, tostring; @@ -25,45 +22,38 @@ local components = {}; local disco_items = require "util.multitable".new(); local NULL = {}; -require "core.discomanager".addDiscoItemsHandler("*host", function(reply, to, from, node) - if #node == 0 and hosts[to] then - for jid in pairs(disco_items:get(to) or NULL) do - reply:tag("item", {jid = jid}):up(); - end - return true; - end -end); - -prosody.events.add_handler("server-starting", function () core_route_stanza = _G.core_route_stanza; end); module "componentmanager" local function default_component_handler(origin, stanza) - log("warn", "Stanza being handled by default component, bouncing error"); - if stanza.attr.type ~= "error" then - core_route_stanza(nil, st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable")); + log("warn", "Stanza being handled by default component; bouncing error for: %s", stanza:top_tag()); + if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then + origin.send(st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable")); end end -local components_loaded_once; function load_enabled_components(config) local defined_hosts = config or configmanager.getconfig(); for host, host_config in pairs(defined_hosts) do if host ~= "*" and ((host_config.core.enabled == nil or host_config.core.enabled) and type(host_config.core.component_module) == "string") then - hosts[host] = { type = "component", host = host, connected = false, s2sout = {}, events = events_new() }; + hosts[host] = create_component(host); + hosts[host].connected = false; components[host] = default_component_handler; local ok, err = modulemanager.load(host, host_config.core.component_module); if not ok then log("error", "Error loading %s component %s: %s", tostring(host_config.core.component_module), tostring(host), tostring(err)); else + fire_event("component-activated", host, host_config); log("debug", "Activated %s component: %s", host_config.core.component_module, host); end end end end -prosody.events.add_handler("server-starting", load_enabled_components); +if prosody and prosody.events then + prosody.events.add_handler("server-starting", load_enabled_components); +end function handle_stanza(origin, stanza) local node, host = jid_split(stanza.attr.to); @@ -76,13 +66,25 @@ function handle_stanza(origin, stanza) log("debug", "%s stanza being handled by component: %s", stanza.name, host); component(origin, stanza, hosts[host]); else - log("error", "Component manager recieved a stanza for a non-existing component: " .. (stanza.attr.to or serialize(stanza))); + log("error", "Component manager recieved a stanza for a non-existing component: "..tostring(stanza)); + default_component_handler(origin, stanza); end end -function create_component(host, component) +function create_component(host, component, events) -- TODO check for host well-formedness - return { type = "component", host = host, connected = true, s2sout = {}, events = events_new() }; + local ssl_ctx; + if host 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; + end + end + return { type = "component", host = host, connected = true, s2sout = {}, + ssl_ctx = ssl_ctx, events = events or events_new() }; end function register_component(host, component, session) @@ -90,7 +92,7 @@ function register_component(host, component, session) local old_events = hosts[host] and hosts[host].events; components[host] = component; - hosts[host] = session or create_component(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 @@ -101,8 +103,8 @@ function register_component(host, component, session) if not(host:find("@", 1, true) or host:find("/", 1, true)) and host:find(".", 1, true) then disco_items:set(host:sub(host:find(".", 1, true)+1), host, true); end - -- FIXME only load for a.b.c if b.c has dialback, and/or check in config modulemanager.load(host, "dialback"); + modulemanager.load(host, "tls"); log("debug", "component added: "..host); return session or hosts[host]; else @@ -112,6 +114,7 @@ end function deregister_component(host) if components[host] then + modulemanager.unload(host, "tls"); modulemanager.unload(host, "dialback"); hosts[host].connected = nil; local host_config = configmanager.getconfig()[host]; @@ -120,7 +123,7 @@ function deregister_component(host) components[host] = default_component_handler; else -- Component not in config, or disabled, remove - hosts[host] = nil; + hosts[host] = nil; -- FIXME do proper unload of all modules and other cleanup before removing components[host] = nil; end -- remove from disco_items @@ -138,4 +141,8 @@ function set_component_handler(host, handler) components[host] = handler; end +function get_children(host) + return disco_items:get(host) or NULL; +end + return _M; diff --git a/core/configmanager.lua b/core/configmanager.lua index b7ee605f..1fbe83b8 100644 --- a/core/configmanager.lua +++ b/core/configmanager.lua @@ -68,7 +68,7 @@ function load(filename, format) if parsers[format] and parsers[format].load then local f, err = io.open(filename); if f then - local ok, err = parsers[format].load(f:read("*a")); + local ok, err = parsers[format].load(f:read("*a"), filename); f:close(); if ok then eventmanager.fire_event("config-reloaded", { filename = filename, format = format }); @@ -99,7 +99,7 @@ do local loadstring, pcall, setmetatable = _G.loadstring, _G.pcall, _G.setmetatable; local setfenv, rawget, tostring = _G.setfenv, _G.rawget, _G.tostring; parsers.lua = {}; - function parsers.lua.load(data) + function parsers.lua.load(data, filename) local env; -- The ' = true' are needed so as not to set off __newindex when we assign the functions below env = setmetatable({ Host = true; host = true; Component = true, component = true, @@ -139,7 +139,7 @@ do local f, err = io.open(file); if f then local data = f:read("*a"); - local ok, err = parsers.lua.load(data); + local ok, err = parsers.lua.load(data, file); if not ok then error(err:gsub("%[string.-%]", file), 0); end end if not f then error("Error loading included "..file..": "..err, 0); end @@ -147,7 +147,7 @@ do end env.include = env.Include; - local chunk, err = loadstring(data); + local chunk, err = loadstring(data, "@"..filename); if not chunk then return nil, err; diff --git a/core/discomanager.lua b/core/discomanager.lua deleted file mode 100644 index 742907dd..00000000 --- a/core/discomanager.lua +++ /dev/null @@ -1,66 +0,0 @@ --- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - - - -local helper = require "util.discohelper".new(); -local hosts = hosts; -local jid_split = require "util.jid".split; -local jid_bare = require "util.jid".bare; -local usermanager_user_exists = require "core.usermanager".user_exists; -local rostermanager_is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; -local print = print; - -do - helper:addDiscoInfoHandler("*host", function(reply, to, from, node) - if hosts[to] then - reply:tag("identity", {category="server", type="im", name="Prosody"}):up(); - return true; - end - end); - helper:addDiscoInfoHandler("*node", function(reply, to, from, node) - local node, host = jid_split(to); - if hosts[host] and rostermanager_is_contact_subscribed(node, host, jid_bare(from)) then - reply:tag("identity", {category="account", type="registered"}):up(); - return true; - end - end); - helper:addDiscoItemsHandler("*host", function(reply, to, from, node) - if hosts[to] and hosts[to].type == "local" then - return true; - end - end); -end - -module "discomanager" - -function handle(stanza) - return helper:handle(stanza); -end - -function addDiscoItemsHandler(jid, func) - return helper:addDiscoItemsHandler(jid, func); -end - -function addDiscoInfoHandler(jid, func) - return helper:addDiscoInfoHandler(jid, func); -end - -function set(plugin, var, origin) - -- TODO handle origin and host based on plugin. - local handler = function(reply, to, from, node) -- service discovery - if #node == 0 then - reply:tag("feature", {var = var}):up(); - return true; - end - end - addDiscoInfoHandler("*node", handler); - addDiscoInfoHandler("*host", handler); -end - -return _M; diff --git a/core/hostmanager.lua b/core/hostmanager.lua index ba363273..f89eaeba 100644 --- a/core/hostmanager.lua +++ b/core/hostmanager.lua @@ -6,15 +6,26 @@ -- COPYING file in the source package for more information. -- +local ssl = ssl local hosts = hosts; local configmanager = require "core.configmanager"; local eventmanager = require "core.eventmanager"; +local modulemanager = require "core.modulemanager"; local events_new = require "util.events".new; +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 = pairs; +local pairs, setmetatable = pairs, setmetatable; module "hostmanager" @@ -24,7 +35,7 @@ local function load_enabled_hosts(config) local defined_hosts = config or configmanager.getconfig(); for host, host_config in pairs(defined_hosts) do - if host ~= "*" and (host_config.core.enabled == nil or host_config.core.enabled) then + if host ~= "*" and (host_config.core.enabled == nil or host_config.core.enabled) and not host_config.core.component_module then activate(host, host_config); end end @@ -46,23 +57,57 @@ function activate(host, host_config) log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in global Host \"*\" instead", host, option_name); 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 + log((hosts_loaded_once and "info") or "debug", "Activated host: %s", host); eventmanager.fire_event("host-activated", host, host_config); end -function deactivate(host) +function deactivate(host, reason) local host_session = hosts[host]; log("info", "Deactivating host: %s", host); eventmanager.fire_event("host-deactivating", host, host_session); + reason = reason or { condition = "host-gone", text = "This server has stopped serving "..host }; + -- Disconnect local users, s2s connections - for user, session_list in pairs(host_session.sessions) do - for resource, session in pairs(session_list) do - session:close("host-gone"); + if host_session.sessions then + for username, user in pairs(host_session.sessions) do + for resource, session in pairs(user.sessions) do + log("debug", "Closing connection for %s@%s/%s", username, host, resource); + session:close(reason); + end end end - -- Components? - + if host_session.s2sout then + for remotehost, session in pairs(host_session.s2sout) do + if session.close then + log("debug", "Closing outgoing connection to %s", remotehost); + if session.srv_hosts then session.srv_hosts = nil; end + session:close(reason); + end + end + end + for remote_session in pairs(incoming_s2s) do + if remote_session.to_host == host then + log("debug", "Closing incoming connection from %s", remote_session.from_host or "<unknown>"); + remote_session:close(reason); + end + end + + if host_session.modules then + for module in pairs(host_session.modules) do + modulemanager.unload(host, module); + end + end + hosts[host] = nil; eventmanager.fire_event("host-deactivated", host); log("info", "Deactivated host: %s", host); @@ -71,3 +116,4 @@ end function getconfig(name) end +return _M; diff --git a/core/loggingmanager.lua b/core/loggingmanager.lua index d701511e..c26fdc71 100644 --- a/core/loggingmanager.lua +++ b/core/loggingmanager.lua @@ -187,6 +187,7 @@ do return function (name, level, message, ...) sourcewidth = math_max(#name+2, sourcewidth); local namelen = #name; + if timestamps then io_write(os_date(timestamps), " "); end diff --git a/core/modulemanager.lua b/core/modulemanager.lua index c2e6e68e..9cd56187 100644 --- a/core/modulemanager.lua +++ b/core/modulemanager.lua @@ -6,13 +6,10 @@ -- COPYING file in the source package for more information. -- - - local plugin_dir = CFG_PLUGINDIR or "./plugins/"; local logger = require "util.logger"; local log = logger.init("modulemanager"); -local addDiscoInfoHandler = require "core.discomanager".addDiscoInfoHandler; local eventmanager = require "core.eventmanager"; local config = require "core.configmanager"; local multitable_new = require "util.multitable".new; @@ -50,8 +47,6 @@ local handler_info = {}; local modulehelpers = setmetatable({}, { __index = _G }); -local features_table = multitable_new(); -local identities_table = multitable_new(); local handler_table = multitable_new(); local hooked = multitable_new(); local hooks = multitable_new(); @@ -61,22 +56,27 @@ local NULL = {}; -- Load modules when a host is activated function load_modules_for_host(host) + local disabled_set = {}; + local modules_disabled = config.get(host, "core", "modules_disabled"); + if modules_disabled then + for _, module in ipairs(modules_disabled) do + disabled_set[module] = true; + end + end + + -- Load auto-loaded modules for this host + if hosts[host].type == "local" then + for _, module in ipairs(autoload_modules) do + if not disabled_set[module] then + load(host, module); + end + end + end + + -- Load modules from global section if config.get(host, "core", "load_global_modules") ~= false then - -- Load modules from global section local modules_enabled = config.get("*", "core", "modules_enabled"); - local modules_disabled = config.get(host, "core", "modules_disabled"); - local disabled_set = {}; if modules_enabled then - if modules_disabled then - for _, module in ipairs(modules_disabled) do - disabled_set[module] = true; - end - end - for _, module in ipairs(autoload_modules) do - if not disabled_set[module] then - load(host, module); - end - end for _, module in ipairs(modules_enabled) do if not disabled_set[module] and not is_loaded(host, module) then load(host, module); @@ -96,6 +96,7 @@ function load_modules_for_host(host) end end eventmanager.add_event_hook("host-activated", load_modules_for_host); +eventmanager.add_event_hook("component-activated", load_modules_for_host); -- function load(host, module_name, config) @@ -127,29 +128,39 @@ function load(host, module_name, config) local pluginenv = setmetatable({ module = api_instance }, { __index = _G }); setfenv(mod, pluginenv); - if not hosts[host] then hosts[host] = { type = "component", host = host, connected = false, s2sout = {} }; end - - local success, ret = pcall(mod); - if not success then - log("error", "Error initialising module '%s': %s", module_name or "nil", ret or "nil"); - return nil, ret; + if not hosts[host] then + local create_component = _G.require "core.componentmanager".create_component; + hosts[host] = create_component(host); + hosts[host].connected = false; + log("debug", "Created new component: %s", host); end + hosts[host].modules = modulemap[host]; + modulemap[host][module_name] = pluginenv; - if module_has_method(pluginenv, "load") then - local ok, err = call_module_method(pluginenv, "load"); - if (not ok) and err then - log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err); + local success, err = pcall(mod); + if success then + if module_has_method(pluginenv, "load") then + success, err = call_module_method(pluginenv, "load"); + if not success then + log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err or "nil"); + end end - end - -- Use modified host, if the module set one - modulemap[api_instance.host][module_name] = pluginenv; - - if api_instance.host == "*" and host ~= "*" then - api_instance:set_global(); + -- 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 + return true; + else -- load failed, unloading + unload(api_instance.host, module_name); + return nil, err; end - - return true; end function get_module(host, name) @@ -170,9 +181,6 @@ function unload(host, name, ...) log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err); end end - modulemap[host][name] = nil; - features_table:remove(host, name); - identities_table:remove(host, name); local params = handler_table:get(host, name); -- , {module.host, origin_type, tag, xmlns} for _, param in pairs(params or NULL) do local handlers = stanza_handlers:get(param[1], param[2], param[3], param[4]); @@ -189,6 +197,7 @@ function unload(host, name, ...) end end hooks:remove(host, name); + modulemap[host][name] = nil; return true; end @@ -235,7 +244,7 @@ function reload(host, name, ...) end function handle_stanza(host, origin, stanza) - local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns, origin.type; + local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns or "jabber:client", origin.type; if name == "iq" and xmlns == "jabber:client" then if stanza.attr.type == "get" or stanza.attr.type == "set" then xmlns = stanza.tags[1].attr.xmlns or "jabber:client"; @@ -252,12 +261,13 @@ function handle_stanza(host, origin, stanza) (handlers[1])(origin, stanza); return true; else - log("debug", "Unhandled %s stanza: %s; xmlns=%s", origin.type, stanza.name, xmlns); -- we didn't handle it if stanza.attr.xmlns == "jabber:client" then + log("debug", "Unhandled %s stanza: %s; xmlns=%s", origin.type, stanza.name, xmlns); -- we didn't handle it if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end elseif not((name == "features" or name == "error") and xmlns == "http://etherx.jabber.org/streams") then -- FIXME remove check once we handle S2S features + log("warn", "Unhandled %s stream element: %s; xmlns=%s: %s", origin.type, stanza.name, xmlns, tostring(stanza)); -- we didn't handle it origin:close("unsupported-stanza-type"); end end @@ -328,50 +338,11 @@ function api:add_iq_handler(origin_type, xmlns, handler) self:add_handler(origin_type, "iq", xmlns, handler); end -addDiscoInfoHandler("*host", function(reply, to, from, node) - if #node == 0 then - local done = {}; - for module, identities in pairs(identities_table:get(to) or NULL) do -- for each module - for identity, attr in pairs(identities) do - if not done[identity] then - reply:tag("identity", attr):up(); -- TODO cache - done[identity] = true; - end - end - end - for module, identities in pairs(identities_table:get("*") or NULL) do -- for each module - for identity, attr in pairs(identities) do - if not done[identity] then - reply:tag("identity", attr):up(); -- TODO cache - done[identity] = true; - end - end - end - for module, features in pairs(features_table:get(to) or NULL) do -- for each module - for feature in pairs(features) do - if not done[feature] then - reply:tag("feature", {var = feature}):up(); -- TODO cache - done[feature] = true; - end - end - end - for module, features in pairs(features_table:get("*") or NULL) do -- for each module - for feature in pairs(features) do - if not done[feature] then - reply:tag("feature", {var = feature}):up(); -- TODO cache - done[feature] = true; - end - end - end - return next(done) ~= nil; - end -end); - function api:add_feature(xmlns) - features_table:set(self.host, self.name, xmlns, true); + self:add_item("feature", xmlns); end -function api:add_identity(category, type) - identities_table:set(self.host, self.name, category.."\0"..type, {category = category, type = type}); +function api:add_identity(category, type, name) + self:add_item("identity", {category = category, type = type, name = name}); end local event_hook = function(host, mod_name, event_name, ...) @@ -419,7 +390,54 @@ function api:require(lib) end function api:get_option(name, default_value) - return config.get(self.host, self.name, name) or config.get(self.host, "core", name) or default_value; + 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 + +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 +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; end -------------------------------------------------------------------- diff --git a/core/rostermanager.lua b/core/rostermanager.lua index 0163e343..516983a9 100644 --- a/core/rostermanager.lua +++ b/core/rostermanager.lua @@ -18,6 +18,7 @@ local pairs, ipairs = pairs, ipairs; local tostring = tostring; local hosts = hosts; +local bare_sessions = bare_sessions; local datamanager = require "util.datamanager" local st = require "util.stanza"; @@ -81,33 +82,41 @@ function roster_push(username, host, jid) end function load_roster(username, host) - log("debug", "load_roster: asked for: "..username.."@"..host); + local jid = username.."@"..host; + log("debug", "load_roster: asked for: "..jid); + local user = bare_sessions[jid]; local roster; - if hosts[host] and hosts[host].sessions[username] then - roster = hosts[host].sessions[username].roster; - if not roster then - log("debug", "load_roster: loading for new user: "..username.."@"..host); - roster = datamanager.load(username, host, "roster") or {}; - if not roster[false] then roster[false] = { }; end - hosts[host].sessions[username].roster = roster; - hosts[host].events.fire_event("roster-load", username, host, roster); - end - return roster; + if user then + roster = user.roster; + if roster then return roster; end + log("debug", "load_roster: loading for new user: "..username.."@"..host); + else -- Attempt to load roster for non-loaded user + log("debug", "load_roster: loading for offline user: "..username.."@"..host); end - - -- Attempt to load roster for non-loaded user - log("debug", "load_roster: loading for offline user: "..username.."@"..host); roster = datamanager.load(username, host, "roster") or {}; + if user then user.roster = roster; end + if not roster[false] then roster[false] = { }; end + if roster[jid] then + roster[jid] = nil; + log("warn", "roster for "..jid.." has a self-contact"); + end hosts[host].events.fire_event("roster-load", username, host, roster); return roster; end -function save_roster(username, host) +function save_roster(username, host, roster) log("debug", "save_roster: saving roster for "..username.."@"..host); - if hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster then - local roster = hosts[host].sessions[username].roster; - roster[false].version = (roster[false].version or 1) + 1; - return datamanager.store(username, host, "roster", hosts[host].sessions[username].roster); + if not roster then + roster = hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster; + --if not roster then + -- --roster = load_roster(username, host); + -- return true; -- roster unchanged, no reason to save + --end + end + if roster then + if not roster[false] then roster[false] = {}; end + roster[false].version = (roster[false].version or 0) + 1; + return datamanager.store(username, host, "roster", roster); end log("warn", "save_roster: user had no roster to save"); return nil; @@ -123,7 +132,7 @@ function process_inbound_subscription_approval(username, host, jid) item.subscription = "both"; end item.ask = nil; - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end end @@ -145,7 +154,7 @@ function process_inbound_subscription_cancellation(username, host, jid) end end if changed then - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end end @@ -167,7 +176,7 @@ function process_inbound_unsubscribe(username, host, jid) end end if changed then - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end end @@ -189,7 +198,7 @@ function set_contact_pending_in(username, host, jid, pending) end if not roster.pending then roster.pending = {}; end roster.pending[jid] = true; - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end function is_contact_pending_out(username, host, jid) local roster = load_roster(username, host); @@ -208,7 +217,7 @@ function set_contact_pending_out(username, host, jid) -- subscribe end item.ask = "subscribe"; log("debug", "set_contact_pending_out: saving roster; set "..username.."@"..host..".roster["..jid.."].ask=subscribe"); - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end function unsubscribe(username, host, jid) local roster = load_roster(username, host); @@ -223,7 +232,7 @@ function unsubscribe(username, host, jid) elseif item.subscription == "to" then item.subscription = "none"; end - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end function subscribed(username, host, jid) if is_contact_pending_in(username, host, jid) then @@ -240,7 +249,7 @@ function subscribed(username, host, jid) end roster.pending[jid] = nil; -- TODO maybe remove roster.pending if empty - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end -- TODO else implement optional feature pre-approval (ask = subscribed) end function unsubscribed(username, host, jid) @@ -262,7 +271,7 @@ function unsubscribed(username, host, jid) end end if changed then - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end end @@ -271,7 +280,7 @@ function process_outbound_subscription_request(username, host, jid) local item = roster[jid]; if item and (item.subscription == "none" or item.subscription == "from") then item.ask = "subscribe"; - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end end @@ -280,7 +289,7 @@ end local item = roster[jid]; if item and (item.subscription == "none" or item.subscription == "from" then item.ask = "subscribe"; - return datamanager.store(username, host, "roster", roster); + return save_roster(username, host, roster); end end]] diff --git a/core/s2smanager.lua b/core/s2smanager.lua index 0589e024..3613707c 100644 --- a/core/s2smanager.lua +++ b/core/s2smanager.lua @@ -27,6 +27,7 @@ local st = require "stanza"; local stanza = st.stanza; local nameprep = require "util.encodings".stringprep.nameprep; +local fire_event = require "core.eventmanager".fire_event; local uuid_gen = require "util.uuid".generate; local logger_init = require "util.logger".init; @@ -37,11 +38,14 @@ local sha256_hash = require "util.hashes".sha256; local dialback_secret = uuid_gen(); -local adns = require "net.adns"; - +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 60; +local max_dns_depth = config.get("*", "core", "dns_max_depth") or 3; incoming_s2s = {}; +_G.prosody.incoming_s2s = incoming_s2s; local incoming_s2s = incoming_s2s; module "s2smanager" @@ -126,13 +130,26 @@ function new_incoming(conn) end 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 incoming_s2s[session] = true; + add_task(connect_timeout, function () + if session.conn ~= conn or + session.type == "s2sin" then + return; -- Ok, we're connect[ed|ing] + end + -- Not connected, need to close session and clean up + (session.log or log)("warn", "Destroying incomplete session %s->%s due to inactivity", + session.from_host or "(unknown)", session.to_host or "(unknown)"); + session:close("connection-timeout"); + end); return session; end function new_outgoing(from_host, to_host) - local host_session = { to_host = to_host, from_host = from_host, notopen = true, type = "s2sout_unauthed", direction = "outgoing" }; + 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 log; @@ -173,7 +190,7 @@ function attempt_connection(host_session, err) if not err then -- This is our first attempt log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host); host_session.connecting = true; - local answer, handle; + local handle; handle = adns.lookup(function (answer) handle = nil; host_session.connecting = nil; @@ -235,6 +252,47 @@ function attempt_connection(host_session, err) end function try_connect(host_session, connect_host, connect_port) + host_session.connecting = true; + local handle; + handle = adns.lookup(function (reply) + handle = nil; + host_session.connecting = nil; + + -- COMPAT: This is a compromise for all you CNAME-(ab)users :) + if not (reply and reply[#reply] and reply[#reply].a) then + local count = max_dns_depth; + reply = dns.peek(connect_host, "CNAME", "IN"); + while count > 0 and reply and reply[#reply] and not reply[#reply].a and reply[#reply].cname do + log("debug", "Looking up %s (DNS depth is %d)", tostring(reply[#reply].cname), count); + reply = dns.peek(reply[#reply].cname, "A", "IN") or dns.peek(reply[#reply].cname, "CNAME", "IN"); + count = count - 1; + end + end + -- end of CNAME resolving + + if reply and reply[#reply] and reply[#reply].a then + log("debug", "DNS reply for %s gives us %s", connect_host, reply[#reply].a); + return make_connect(host_session, reply[#reply].a, connect_port); + else + log("debug", "DNS lookup failed to get a response for %s", connect_host); + if not attempt_connection(host_session, "name resolution failed") then -- Retry if we can + log("debug", "No other records to try for %s - destroying", host_session.to_host); + destroy_session(host_session); -- End of the line, we can't + end + end + end, connect_host, "A", "IN"); + + -- Set handler for DNS timeout + add_task(dns_timeout, function () + if handle then + 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); -- Ok, we're going to try to connect @@ -257,11 +315,22 @@ function try_connect(host_session, connect_host, connect_port) -- otherwise it will assume it is a new incoming connection cl.register_outgoing(conn, host_session); - local w = conn.write; + local w, log = conn.write, host_session.log; host_session.sends2s = function (t) log("debug", "sending: %s", tostring(t)); w(tostring(t)); end conn.write(format([[<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' from='%s' to='%s' version='1.0' xml:lang='en'>]], from_host, to_host)); log("debug", "Connection attempt in progress..."); + 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 @@ -269,10 +338,16 @@ function streamopened(session, attr) local send = session.sends2s; -- TODO: #29: SASL/TLS on s2s streams - session.version = 0; --tonumber(attr.version) or 0; + session.version = tonumber(attr.version) or 0; + + if session.secure == false then + session.secure = true; + end if session.version >= 1.0 and not (attr.to and attr.from) then - log("warn", (session.to_host or "(unknown)").." failed to specify 'to' or 'from' hostname as per RFC"); + + (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 if session.direction == "incoming" then @@ -284,15 +359,23 @@ function streamopened(session, attr) (session.log or log)("debug", "incoming s2s received <stream:stream>"); send("<?xml version='1.0'?>"); send(stanza("stream:stream", { xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback', - ["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.to_host }):top_tag()); + ["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 if session.version >= 1.0 then - send(st.stanza("stream:features") - :tag("dialback", { xmlns='urn:xmpp:features:dialback' }):tag("optional"):up():up()); + 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 }); + 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 @@ -313,10 +396,14 @@ function streamopened(session, attr) end session.send_buffer = nil; - if not session.dialback_verifying then - initiate_dialback(session); - else - mark_connected(session); + -- 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 @@ -366,6 +453,7 @@ function make_authenticated(session, host) return true; end +-- Stream is authorised, and ready for normal stanzas function mark_connected(session) local sendq, send = session.sendq, session.sends2s; diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 1b1b36df..08e70d44 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -11,7 +11,6 @@ local tonumber, tostring = tonumber, tostring; local ipairs, pairs, print, next= ipairs, pairs, print, next; local collectgarbage = collectgarbage; -local m_random = import("math", "random"); local format = import("string", "format"); local hosts = hosts; @@ -19,7 +18,8 @@ local full_sessions = full_sessions; local bare_sessions = bare_sessions; local modulemanager = require "core.modulemanager"; -local log = require "util.logger".init("sessionmanager"); +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; @@ -27,11 +27,13 @@ local config_get = require "core.configmanager".get; local nameprep = require "util.encodings".stringprep.nameprep; local fire_event = require "core.eventmanager".fire_event; - +local add_task = require "util.timer".add_task; local gettime = require "socket".gettime; local st = require "util.stanza"; +local c2s_timeout = config_get("*", "core", "c2s_timeout"); + local newproxy = newproxy; local getmetatable = getmetatable; @@ -50,6 +52,17 @@ function new_session(conn) local w = conn.write; session.send = function (t) w(tostring(t)); end 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 @@ -154,31 +167,32 @@ function streamopened(session, attr) session.host = attr.to or error("Client failed to specify destination hostname"); session.host = nameprep(session.host); session.version = tonumber(attr.version) or 0; - session.streamid = m_random(1000000, 99999999); + session.streamid = uuid_generate(); (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host); - - send("<?xml version='1.0'?>"); - send(format("<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='%s' from='%s' version='1.0' xml:lang='en'>", session.streamid, session.host)); if not hosts[session.host] then -- We don't serve this host... session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)}; return; end - + + 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"); fire_event("stream-features", session, features); - + send(features); - - (session.log or log)("debug", "Sent reply <stream:stream> to client"); - session.notopen = nil; + end function streamclosed(session) diff --git a/core/stanza_router.lua b/core/stanza_router.lua index dac098bb..00c37ed7 100644 --- a/core/stanza_router.lua +++ b/core/stanza_router.lua @@ -8,7 +8,7 @@ local log = require "util.logger".init("stanzarouter") -local hosts = _G.hosts; +local hosts = _G.prosody.hosts; local tostring = tostring; local st = require "util.stanza"; local send_s2s = require "core.s2smanager".send_to_host; @@ -17,6 +17,9 @@ local component_handle_stanza = require "core.componentmanager".handle_stanza; local jid_split = require "util.jid".split; local jid_prepped_split = require "util.jid".prepped_split; +local full_sessions = _G.prosody.full_sessions; +local bare_sessions = _G.prosody.bare_sessions; + function core_process_stanza(origin, stanza) (origin.log or log)("debug", "Received[%s]: %s", origin.type, stanza:top_tag()) @@ -26,7 +29,8 @@ function core_process_stanza(origin, stanza) -- TODO verify validity of stanza (as well as JID validity) if stanza.attr.type == "error" and #stanza.tags == 0 then return; end -- TODO invalid stanza, log if stanza.name == "iq" then - if (stanza.attr.type == "set" or stanza.attr.type == "get") and #stanza.tags ~= 1 then + if not stanza.attr.id then stanza.attr.id = ""; end -- COMPAT Jabiru doesn't send the id attribute on roster requests + if (stanza.attr.type == "set" or stanza.attr.type == "get") and (#stanza.tags ~= 1) then origin.send(st.error_reply(stanza, "modify", "bad-request")); return; end @@ -110,7 +114,7 @@ function core_process_stanza(origin, stanza) end if h.events.fire_event(event, {origin = origin, stanza = stanza}) then return; end end - if host and not hosts[host] then host = nil; end -- workaround for a Pidgin bug which sets 'to' to the SRV result + if host and not hosts[host] then host = nil; end -- COMPAT: workaround for a Pidgin bug which sets 'to' to the SRV result modules_handle_stanza(host or origin.host or origin.to_host, origin, stanza); end end diff --git a/core/usermanager.lua b/core/usermanager.lua index 6c36fa29..925ac774 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -6,10 +6,7 @@ -- COPYING file in the source package for more information. -- - - -require "util.datamanager" -local datamanager = datamanager; +local datamanager = require "util.datamanager"; local log = require "util.logger".init("usermanager"); local type = type; local error = error; @@ -66,14 +63,18 @@ function get_supported_methods(host) return {["PLAIN"] = true, ["DIGEST-MD5"] = true}; -- TODO this should be taken from the config end -function is_admin(jid) - local admins = config.get("*", "core", "admins"); +function is_admin(jid, host) + host = host or "*"; + local admins = config.get(host, "core", "admins"); + if host ~= "*" and admins == config.get("*", "core", "admins") then + return nil; + end if type(admins) == "table" then jid = jid_bare(jid); for _,admin in ipairs(admins) do if admin == jid then return true; end end - else log("debug", "Option core.admins is not a table"); end + elseif admins then log("warn", "Option 'admins' for host '%s' is not a table", host); end return nil; end diff --git a/core/xmlhandlers.lua b/core/xmlhandlers.lua index 7f47cf70..d679af97 100644 --- a/core/xmlhandlers.lua +++ b/core/xmlhandlers.lua @@ -29,7 +29,6 @@ local ns_prefixes = { function init_xmlhandlers(session, stream_callbacks) local ns_stack = { "" }; - local curr_ns, name = ""; local curr_tag; local chardata = {}; local xml_handlers = {}; @@ -50,7 +49,7 @@ function init_xmlhandlers(session, stream_callbacks) stanza:text(t_concat(chardata)); chardata = {}; end - local curr_ns,name = tagname:match("^(.-)|?([^%|]-)$"); + local curr_ns,name = tagname:match("^([^\1]*)\1?(.*)$"); if not name then curr_ns, name = "", curr_ns; end @@ -63,7 +62,7 @@ function init_xmlhandlers(session, stream_callbacks) for i=1,#attr do local k = attr[i]; attr[i] = nil; - local ns, nm = k:match("^([^|]+)|?([^|]-)$") + local ns, nm = k:match("^([^\1]*)\1?(.*)$"); if ns and nm then ns = ns_prefixes[ns]; if ns then @@ -105,7 +104,7 @@ function init_xmlhandlers(session, stream_callbacks) end end function xml_handlers:EndElement(tagname) - curr_ns,name = tagname:match("^(.-)|?([^%|]-)$"); + local curr_ns,name = tagname:match("^([^\1]*)\1?(.*)$"); if not name then curr_ns, name = "", curr_ns; end @@ -114,12 +113,13 @@ function init_xmlhandlers(session, stream_callbacks) if cb_streamclosed then cb_streamclosed(session); end - return; elseif name == "error" then cb_error(session, "stream-error", stanza); else cb_error(session, "parse-error", "unexpected-element-close", name); end + stanza, chardata = nil, {}; + return; end if #chardata > 0 then -- We have some character data in the buffer diff --git a/net/adns.lua b/net/adns.lua index 34ef5d77..b0c9a625 100644 --- a/net/adns.lua +++ b/net/adns.lua @@ -11,6 +11,7 @@ local dns = require "net.dns"; local log = require "util.logger".init("adns"); +local t_insert, t_remove = table.insert, table.remove; local coroutine, tostring, pcall = coroutine, tostring, pcall; module "adns" @@ -28,7 +29,7 @@ function lookup(handler, qname, qtype, qclass) log("debug", "Reply for %s (%s)", qname, tostring(coroutine.running())); local ok, err = pcall(handler, dns.peek(qname, qtype, qclass)); if not ok then - log("debug", "Error in DNS response handler: %s", tostring(err)); + log("error", "Error in DNS response handler: %s", tostring(err)); end end)(dns.peek(qname, qtype, qclass)); end @@ -41,18 +42,31 @@ function cancel(handle, call_handler) end end -function new_async_socket(sock) - local newconn = {}; +function new_async_socket(sock, resolver) + local newconn, peername = {}, "<unknown>"; local listener = {}; function listener.incoming(conn, data) dns.feed(sock, data); end - function listener.disconnect() + function listener.disconnect(conn, err) + log("warn", "DNS socket for %s disconnected: %s", peername, err); + local servers = resolver.server; + if resolver.socketset[newconn.handler] == resolver.best_server and resolver.best_server == #servers then + log("error", "Exhausted all %d configured DNS servers, next lookup will try %s again", #servers, servers[1]); + end + + resolver:servfail(conn); -- Let the magic commence end newconn.handler, newconn._socket = server.wrapclient(sock, "dns", 53, listener); + if not newconn.handler then + log("warn", "handler is nil"); + end + if not newconn._socket then + log("warn", "socket is nil"); + end newconn.handler.settimeout = function () end newconn.handler.setsockname = function (_, ...) return sock:setsockname(...); end - newconn.handler.setpeername = function (_, ...) local ret = sock:setpeername(...); _.setsend(sock.send); return ret; end + newconn.handler.setpeername = function (_, ...) peername = (...); local ret = sock:setpeername(...); _.setsend(sock.send); return ret; end newconn.handler.connect = function (_, ...) return sock:connect(...) end newconn.handler.send = function (_, data) _.write(data); return _.sendbuffer(); end return newconn.handler; diff --git a/net/connlisteners.lua b/net/connlisteners.lua index ebb3cc18..230d92a4 100644 --- a/net/connlisteners.lua +++ b/net/connlisteners.lua @@ -11,6 +11,7 @@ local listeners_dir = (CFG_SOURCEDIR or ".").."/net/"; local server = require "net.server"; local log = require "util.logger".init("connlisteners"); +local tostring = tostring; local dofile, pcall, error = dofile, pcall, error @@ -37,7 +38,10 @@ function get(name) local h = listeners[name]; if not h then local ok, ret = pcall(dofile, listeners_dir..name:gsub("[^%w%-]", "_").."_listener.lua"); - if not ok then return nil, ret; end + if not ok then + log("error", "Error while loading listener '%s': %s", tostring(name), tostring(ret)); + return nil, ret; + end h = listeners[name]; end return h; diff --git a/net/dns.lua b/net/dns.lua index 48c08218..04b2cf22 100644 --- a/net/dns.lua +++ b/net/dns.lua @@ -14,21 +14,22 @@ -- reference: http://tools.ietf.org/html/rfc1876 (LOC) -require 'socket' -local ztact = require 'util.ztact' -local require = require +local socket = require "socket"; +local ztact = require "util.ztact"; +local _, windows = pcall(require, "util.windows"); +local is_windows = (_ and windows) or os.getenv("WINDIR"); -local coroutine, io, math, socket, string, table = - coroutine, io, math, socket, string, table +local coroutine, io, math, string, table = + coroutine, io, math, string, table; local ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack = - ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack + ipairs, next, pairs, print, setmetatable, tostring, assert, error, unpack; -local get, set = ztact.get, ztact.set +local get, set = ztact.get, ztact.set; -------------------------------------------------- module dns -module ('dns') +module('dns') local dns = _M; @@ -38,826 +39,928 @@ local dns = _M; local append = table.insert -local function highbyte (i) -- - - - - - - - - - - - - - - - - - - highbyte - return (i-(i%0x100))/0x100 - end +local function highbyte(i) -- - - - - - - - - - - - - - - - - - - highbyte + return (i-(i%0x100))/0x100; +end local function augment (t) -- - - - - - - - - - - - - - - - - - - - augment - local a = {} - for i,s in pairs (t) do a[i] = s a[s] = s a[string.lower (s)] = s end - return a - end + local a = {}; + for i,s in pairs(t) do + a[i] = s; + a[s] = s; + a[string.lower(s)] = s; + end + return a; +end local function encode (t) -- - - - - - - - - - - - - - - - - - - - - encode - local code = {} - for i,s in pairs (t) do - local word = string.char (highbyte (i), i %0x100) - code[i] = word - code[s] = word - code[string.lower (s)] = word - end - return code - end + local code = {}; + for i,s in pairs(t) do + local word = string.char(highbyte(i), i%0x100); + code[i] = word; + code[s] = word; + code[string.lower(s)] = word; + end + return code; +end dns.types = { - 'A', 'NS', 'MD', 'MF', 'CNAME', 'SOA', 'MB', 'MG', 'MR', 'NULL', 'WKS', - 'PTR', 'HINFO', 'MINFO', 'MX', 'TXT', - [ 28] = 'AAAA', [ 29] = 'LOC', [ 33] = 'SRV', - [252] = 'AXFR', [253] = 'MAILB', [254] = 'MAILA', [255] = '*' } + 'A', 'NS', 'MD', 'MF', 'CNAME', 'SOA', 'MB', 'MG', 'MR', 'NULL', 'WKS', + 'PTR', 'HINFO', 'MINFO', 'MX', 'TXT', + [ 28] = 'AAAA', [ 29] = 'LOC', [ 33] = 'SRV', + [252] = 'AXFR', [253] = 'MAILB', [254] = 'MAILA', [255] = '*' }; -dns.classes = { 'IN', 'CS', 'CH', 'HS', [255] = '*' } +dns.classes = { 'IN', 'CS', 'CH', 'HS', [255] = '*' }; -dns.type = augment (dns.types) -dns.class = augment (dns.classes) -dns.typecode = encode (dns.types) -dns.classcode = encode (dns.classes) +dns.type = augment (dns.types); +dns.class = augment (dns.classes); +dns.typecode = encode (dns.types); +dns.classcode = encode (dns.classes); -local function standardize (qname, qtype, qclass) -- - - - - - - standardize - if string.byte (qname, -1) ~= 0x2E then qname = qname..'.' end - qname = string.lower (qname) - return qname, dns.type[qtype or 'A'], dns.class[qclass or 'IN'] - end - - -local function prune (rrs, time, soft) -- - - - - - - - - - - - - - - prune - - time = time or socket.gettime () - for i,rr in pairs (rrs) do +local function standardize(qname, qtype, qclass) -- - - - - - - standardize + if string.byte(qname, -1) ~= 0x2E then qname = qname..'.'; end + qname = string.lower(qname); + return qname, dns.type[qtype or 'A'], dns.class[qclass or 'IN']; +end - if rr.tod then - -- rr.tod = rr.tod - 50 -- accelerated decripitude - rr.ttl = math.floor (rr.tod - time) - if rr.ttl <= 0 then rrs[i] = nil end - elseif soft == 'soft' then -- What is this? I forget! - assert (rr.ttl == 0) - rrs[i] = nil - end end end +local function prune(rrs, time, soft) -- - - - - - - - - - - - - - - prune + time = time or socket.gettime(); + for i,rr in pairs(rrs) do + if rr.tod then + -- rr.tod = rr.tod - 50 -- accelerated decripitude + rr.ttl = math.floor(rr.tod - time); + if rr.ttl <= 0 then + table.remove(rrs, i); + return prune(rrs, time, soft); -- Re-iterate + end + elseif soft == 'soft' then -- What is this? I forget! + assert(rr.ttl == 0); + rrs[i] = nil; + end + end +end -- metatables & co. ------------------------------------------ metatables & co. -local resolver = {} -resolver.__index = resolver - - -local SRV_tostring - - -local rr_metatable = {} -- - - - - - - - - - - - - - - - - - - rr_metatable -function rr_metatable.__tostring (rr) - local s0 = string.format ( - '%2s %-5s %6i %-28s', rr.class, rr.type, rr.ttl, rr.name ) - local s1 = '' - if rr.type == 'A' then s1 = ' '..rr.a - elseif rr.type == 'MX' then - s1 = string.format (' %2i %s', rr.pref, rr.mx) - elseif rr.type == 'CNAME' then s1 = ' '..rr.cname - elseif rr.type == 'LOC' then s1 = ' '..resolver.LOC_tostring (rr) - elseif rr.type == 'NS' then s1 = ' '..rr.ns - elseif rr.type == 'SRV' then s1 = ' '..SRV_tostring (rr) - elseif rr.type == 'TXT' then s1 = ' '..rr.txt - else s1 = ' <UNKNOWN RDATA TYPE>' end - return s0..s1 - end +local resolver = {}; +resolver.__index = resolver; + + +local SRV_tostring; + + +local rr_metatable = {}; -- - - - - - - - - - - - - - - - - - - rr_metatable +function rr_metatable.__tostring(rr) + local s0 = string.format('%2s %-5s %6i %-28s', rr.class, rr.type, rr.ttl, rr.name); + local s1 = ''; + if rr.type == 'A' then + s1 = ' '..rr.a; + elseif rr.type == 'MX' then + s1 = string.format(' %2i %s', rr.pref, rr.mx); + elseif rr.type == 'CNAME' then + s1 = ' '..rr.cname; + elseif rr.type == 'LOC' then + s1 = ' '..resolver.LOC_tostring(rr); + elseif rr.type == 'NS' then + s1 = ' '..rr.ns; + elseif rr.type == 'SRV' then + s1 = ' '..SRV_tostring(rr); + elseif rr.type == 'TXT' then + s1 = ' '..rr.txt; + else + s1 = ' <UNKNOWN RDATA TYPE>'; + end + return s0..s1; +end -local rrs_metatable = {} -- - - - - - - - - - - - - - - - - - rrs_metatable -function rrs_metatable.__tostring (rrs) - local t = {} - for i,rr in pairs (rrs) do append (t, tostring (rr)..'\n') end - return table.concat (t) - end +local rrs_metatable = {}; -- - - - - - - - - - - - - - - - - - rrs_metatable +function rrs_metatable.__tostring(rrs) + local t = {}; + for i,rr in pairs(rrs) do + append(t, tostring(rr)..'\n'); + end + return table.concat(t); +end -local cache_metatable = {} -- - - - - - - - - - - - - - - - cache_metatable -function cache_metatable.__tostring (cache) - local time = socket.gettime () - local t = {} - for class,types in pairs (cache) do - for type,names in pairs (types) do - for name,rrs in pairs (names) do - prune (rrs, time) - append (t, tostring (rrs)) end end end - return table.concat (t) - end +local cache_metatable = {}; -- - - - - - - - - - - - - - - - cache_metatable +function cache_metatable.__tostring(cache) + local time = socket.gettime(); + local t = {}; + for class,types in pairs(cache) do + for type,names in pairs(types) do + for name,rrs in pairs(names) do + prune(rrs, time); + append(t, tostring(rrs)); + end + end + end + return table.concat(t); +end -function resolver:new () -- - - - - - - - - - - - - - - - - - - - - resolver - local r = { active = {}, cache = {}, unsorted = {} } - setmetatable (r, resolver) - setmetatable (r.cache, cache_metatable) - setmetatable (r.unsorted, { __mode = 'kv' }) - return r - end +function resolver:new() -- - - - - - - - - - - - - - - - - - - - - resolver + local r = { active = {}, cache = {}, unsorted = {} }; + setmetatable(r, resolver); + setmetatable(r.cache, cache_metatable); + setmetatable(r.unsorted, { __mode = 'kv' }); + return r; +end -- packet layer -------------------------------------------------- packet layer -function dns.random (...) -- - - - - - - - - - - - - - - - - - - dns.random - math.randomseed (10000*socket.gettime ()) - dns.random = math.random - return dns.random (...) - end - - -local function encodeHeader (o) -- - - - - - - - - - - - - - - encodeHeader +function dns.random(...) -- - - - - - - - - - - - - - - - - - - dns.random + math.randomseed(10000*socket.gettime()); + dns.random = math.random; + return dns.random(...); +end - o = o or {} - o.id = o.id or -- 16b (random) id - dns.random (0, 0xffff) +local function encodeHeader(o) -- - - - - - - - - - - - - - - encodeHeader + o = o or {}; + o.id = o.id or dns.random(0, 0xffff); -- 16b (random) id - o.rd = o.rd or 1 -- 1b 1 recursion desired - o.tc = o.tc or 0 -- 1b 1 truncated response - o.aa = o.aa or 0 -- 1b 1 authoritative response - o.opcode = o.opcode or 0 -- 4b 0 query - -- 1 inverse query + o.rd = o.rd or 1; -- 1b 1 recursion desired + o.tc = o.tc or 0; -- 1b 1 truncated response + o.aa = o.aa or 0; -- 1b 1 authoritative response + o.opcode = o.opcode or 0; -- 4b 0 query + -- 1 inverse query -- 2 server status request -- 3-15 reserved - o.qr = o.qr or 0 -- 1b 0 query, 1 response + o.qr = o.qr or 0; -- 1b 0 query, 1 response - o.rcode = o.rcode or 0 -- 4b 0 no error + o.rcode = o.rcode or 0; -- 4b 0 no error -- 1 format error -- 2 server failure -- 3 name error -- 4 not implemented -- 5 refused -- 6-15 reserved - o.z = o.z or 0 -- 3b 0 resvered - o.ra = o.ra or 0 -- 1b 1 recursion available - - o.qdcount = o.qdcount or 1 -- 16b number of question RRs - o.ancount = o.ancount or 0 -- 16b number of answers RRs - o.nscount = o.nscount or 0 -- 16b number of nameservers RRs - o.arcount = o.arcount or 0 -- 16b number of additional RRs - - -- string.char() rounds, so prevent roundup with -0.4999 - local header = string.char ( - highbyte (o.id), o.id %0x100, - o.rd + 2*o.tc + 4*o.aa + 8*o.opcode + 128*o.qr, - o.rcode + 16*o.z + 128*o.ra, - highbyte (o.qdcount), o.qdcount %0x100, - highbyte (o.ancount), o.ancount %0x100, - highbyte (o.nscount), o.nscount %0x100, - highbyte (o.arcount), o.arcount %0x100 ) - - return header, o.id - end - - -local function encodeName (name) -- - - - - - - - - - - - - - - - encodeName - local t = {} - for part in string.gmatch (name, '[^.]+') do - append (t, string.char (string.len (part))) - append (t, part) - end - append (t, string.char (0)) - return table.concat (t) - end - - -local function encodeQuestion (qname, qtype, qclass) -- - - - encodeQuestion - qname = encodeName (qname) - qtype = dns.typecode[qtype or 'a'] - qclass = dns.classcode[qclass or 'in'] - return qname..qtype..qclass; - end - - -function resolver:byte (len) -- - - - - - - - - - - - - - - - - - - - - byte - len = len or 1 - local offset = self.offset - local last = offset + len - 1 - if last > #self.packet then - error (string.format ('out of bounds: %i>%i', last, #self.packet)) end - self.offset = offset + len - return string.byte (self.packet, offset, last) - end - - -function resolver:word () -- - - - - - - - - - - - - - - - - - - - - - word - local b1, b2 = self:byte (2) - return 0x100*b1 + b2 - end + o.z = o.z or 0; -- 3b 0 resvered + o.ra = o.ra or 0; -- 1b 1 recursion available + + o.qdcount = o.qdcount or 1; -- 16b number of question RRs + o.ancount = o.ancount or 0; -- 16b number of answers RRs + o.nscount = o.nscount or 0; -- 16b number of nameservers RRs + o.arcount = o.arcount or 0; -- 16b number of additional RRs + + -- string.char() rounds, so prevent roundup with -0.4999 + local header = string.char( + highbyte(o.id), o.id %0x100, + o.rd + 2*o.tc + 4*o.aa + 8*o.opcode + 128*o.qr, + o.rcode + 16*o.z + 128*o.ra, + highbyte(o.qdcount), o.qdcount %0x100, + highbyte(o.ancount), o.ancount %0x100, + highbyte(o.nscount), o.nscount %0x100, + highbyte(o.arcount), o.arcount %0x100 + ); + + return header, o.id; +end + + +local function encodeName(name) -- - - - - - - - - - - - - - - - encodeName + local t = {}; + for part in string.gmatch(name, '[^.]+') do + append(t, string.char(string.len(part))); + append(t, part); + end + append(t, string.char(0)); + return table.concat(t); +end + + +local function encodeQuestion(qname, qtype, qclass) -- - - - encodeQuestion + qname = encodeName(qname); + qtype = dns.typecode[qtype or 'a']; + qclass = dns.classcode[qclass or 'in']; + return qname..qtype..qclass; +end + + +function resolver:byte(len) -- - - - - - - - - - - - - - - - - - - - - byte + len = len or 1; + local offset = self.offset; + local last = offset + len - 1; + if last > #self.packet then + error(string.format('out of bounds: %i>%i', last, #self.packet)); + end + self.offset = offset + len; + return string.byte(self.packet, offset, last); +end + + +function resolver:word() -- - - - - - - - - - - - - - - - - - - - - - word + local b1, b2 = self:byte(2); + return 0x100*b1 + b2; +end function resolver:dword () -- - - - - - - - - - - - - - - - - - - - - dword - local b1, b2, b3, b4 = self:byte (4) - --print ('dword', b1, b2, b3, b4) - return 0x1000000*b1 + 0x10000*b2 + 0x100*b3 + b4 - end + local b1, b2, b3, b4 = self:byte(4); + --print('dword', b1, b2, b3, b4); + return 0x1000000*b1 + 0x10000*b2 + 0x100*b3 + b4; +end -function resolver:sub (len) -- - - - - - - - - - - - - - - - - - - - - - sub - len = len or 1 - local s = string.sub (self.packet, self.offset, self.offset + len - 1) - self.offset = self.offset + len - return s - end +function resolver:sub(len) -- - - - - - - - - - - - - - - - - - - - - - sub + len = len or 1; + local s = string.sub(self.packet, self.offset, self.offset + len - 1); + self.offset = self.offset + len; + return s; +end -function resolver:header (force) -- - - - - - - - - - - - - - - - - - header - - local id = self:word () - --print (string.format (':header id %x', id)) - if not self.active[id] and not force then return nil end - - local h = { id = id } - - local b1, b2 = self:byte (2) - - h.rd = b1 %2 - h.tc = b1 /2%2 - h.aa = b1 /4%2 - h.opcode = b1 /8%16 - h.qr = b1 /128 +function resolver:header(force) -- - - - - - - - - - - - - - - - - - header + local id = self:word(); + --print(string.format(':header id %x', id)); + if not self.active[id] and not force then return nil; end - h.rcode = b2 %16 - h.z = b2 /16%8 - h.ra = b2 /128 - - h.qdcount = self:word () - h.ancount = self:word () - h.nscount = self:word () - h.arcount = self:word () - - for k,v in pairs (h) do h[k] = v-v%1 end - - return h - end - - -function resolver:name () -- - - - - - - - - - - - - - - - - - - - - - name - local remember, pointers = nil, 0 - local len = self:byte () - local n = {} - while len > 0 do - if len >= 0xc0 then -- name is "compressed" - pointers = pointers + 1 - if pointers >= 20 then error ('dns error: 20 pointers') end - local offset = ((len-0xc0)*0x100) + self:byte () - remember = remember or self.offset - self.offset = offset + 1 -- +1 for lua - else -- name is not compressed - append (n, self:sub (len)..'.') - end - len = self:byte () - end - self.offset = remember or self.offset - return table.concat (n) - end + local h = { id = id }; + local b1, b2 = self:byte(2); -function resolver:question () -- - - - - - - - - - - - - - - - - - question - local q = {} - q.name = self:name () - q.type = dns.type[self:word ()] - q.class = dns.class[self:word ()] - return q - end + h.rd = b1 %2; + h.tc = b1 /2%2; + h.aa = b1 /4%2; + h.opcode = b1 /8%16; + h.qr = b1 /128; + h.rcode = b2 %16; + h.z = b2 /16%8; + h.ra = b2 /128; -function resolver:A (rr) -- - - - - - - - - - - - - - - - - - - - - - - - A - local b1, b2, b3, b4 = self:byte (4) - rr.a = string.format ('%i.%i.%i.%i', b1, b2, b3, b4) - end + h.qdcount = self:word(); + h.ancount = self:word(); + h.nscount = self:word(); + h.arcount = self:word(); + for k,v in pairs(h) do h[k] = v-v%1; end -function resolver:CNAME (rr) -- - - - - - - - - - - - - - - - - - - - CNAME - rr.cname = self:name () - end + return h; +end -function resolver:MX (rr) -- - - - - - - - - - - - - - - - - - - - - - - MX - rr.pref = self:word () - rr.mx = self:name () - end +function resolver:name() -- - - - - - - - - - - - - - - - - - - - - - name + local remember, pointers = nil, 0; + local len = self:byte(); + local n = {}; + while len > 0 do + if len >= 0xc0 then -- name is "compressed" + pointers = pointers + 1; + if pointers >= 20 then error('dns error: 20 pointers'); end; + local offset = ((len-0xc0)*0x100) + self:byte(); + remember = remember or self.offset; + self.offset = offset + 1; -- +1 for lua + else -- name is not compressed + append(n, self:sub(len)..'.'); + end + len = self:byte(); + end + self.offset = remember or self.offset; + return table.concat(n); +end -function resolver:LOC_nibble_power () -- - - - - - - - - - LOC_nibble_power - local b = self:byte () - --print ('nibbles', ((b-(b%0x10))/0x10), (b%0x10)) - return ((b-(b%0x10))/0x10) * (10^(b%0x10)) - end +function resolver:question() -- - - - - - - - - - - - - - - - - - question + local q = {}; + q.name = self:name(); + q.type = dns.type[self:word()]; + q.class = dns.class[self:word()]; + return q; +end -function resolver:LOC (rr) -- - - - - - - - - - - - - - - - - - - - - - LOC - rr.version = self:byte () - if rr.version == 0 then - rr.loc = rr.loc or {} - rr.loc.size = self:LOC_nibble_power () - rr.loc.horiz_pre = self:LOC_nibble_power () - rr.loc.vert_pre = self:LOC_nibble_power () - rr.loc.latitude = self:dword () - rr.loc.longitude = self:dword () - rr.loc.altitude = self:dword () - end end +function resolver:A(rr) -- - - - - - - - - - - - - - - - - - - - - - - - A + local b1, b2, b3, b4 = self:byte(4); + rr.a = string.format('%i.%i.%i.%i', b1, b2, b3, b4); +end -local function LOC_tostring_degrees (f, pos, neg) -- - - - - - - - - - - - - - f = f - 0x80000000 - if f < 0 then pos = neg f = -f end - local deg, min, msec - msec = f%60000 - f = (f-msec)/60000 - min = f%60 - deg = (f-min)/60 - return string.format ('%3d %2d %2.3f %s', deg, min, msec/1000, pos) - end - - -function resolver.LOC_tostring (rr) -- - - - - - - - - - - - - LOC_tostring - - local t = {} - - --[[ - for k,name in pairs { 'size', 'horiz_pre', 'vert_pre', - 'latitude', 'longitude', 'altitude' } do - append (t, string.format ('%4s%-10s: %12.0f\n', '', name, rr.loc[name])) - end - --]] - - append ( t, string.format ( - '%s %s %.2fm %.2fm %.2fm %.2fm', - LOC_tostring_degrees (rr.loc.latitude, 'N', 'S'), - LOC_tostring_degrees (rr.loc.longitude, 'E', 'W'), - (rr.loc.altitude - 10000000) / 100, - rr.loc.size / 100, - rr.loc.horiz_pre / 100, - rr.loc.vert_pre / 100 ) ) - - return table.concat (t) - end - - -function resolver:NS (rr) -- - - - - - - - - - - - - - - - - - - - - - - NS - rr.ns = self:name () - end - +function resolver:CNAME(rr) -- - - - - - - - - - - - - - - - - - - - CNAME + rr.cname = self:name(); +end -function resolver:SOA (rr) -- - - - - - - - - - - - - - - - - - - - - - SOA - end +function resolver:MX(rr) -- - - - - - - - - - - - - - - - - - - - - - - MX + rr.pref = self:word(); + rr.mx = self:name(); +end -function resolver:SRV (rr) -- - - - - - - - - - - - - - - - - - - - - - SRV - rr.srv = {} - rr.srv.priority = self:word () - rr.srv.weight = self:word () - rr.srv.port = self:word () - rr.srv.target = self:name () - end +function resolver:LOC_nibble_power() -- - - - - - - - - - LOC_nibble_power + local b = self:byte(); + --print('nibbles', ((b-(b%0x10))/0x10), (b%0x10)); + return ((b-(b%0x10))/0x10) * (10^(b%0x10)); +end -function SRV_tostring (rr) -- - - - - - - - - - - - - - - - - - SRV_tostring - local s = rr.srv - return string.format ( '%5d %5d %5d %s', - s.priority, s.weight, s.port, s.target ) - end +function resolver:LOC(rr) -- - - - - - - - - - - - - - - - - - - - - - LOC + rr.version = self:byte(); + if rr.version == 0 then + rr.loc = rr.loc or {}; + rr.loc.size = self:LOC_nibble_power(); + rr.loc.horiz_pre = self:LOC_nibble_power(); + rr.loc.vert_pre = self:LOC_nibble_power(); + rr.loc.latitude = self:dword(); + rr.loc.longitude = self:dword(); + rr.loc.altitude = self:dword(); + end +end -function resolver:TXT (rr) -- - - - - - - - - - - - - - - - - - - - - - TXT - rr.txt = self:sub (rr.rdlength) - end +local function LOC_tostring_degrees(f, pos, neg) -- - - - - - - - - - - - - + f = f - 0x80000000; + if f < 0 then pos = neg; f = -f; end + local deg, min, msec; + msec = f%60000; + f = (f-msec)/60000; + min = f%60; + deg = (f-min)/60; + return string.format('%3d %2d %2.3f %s', deg, min, msec/1000, pos); +end -function resolver:rr () -- - - - - - - - - - - - - - - - - - - - - - - - rr - local rr = {} - setmetatable (rr, rr_metatable) - rr.name = self:name (self) - rr.type = dns.type[self:word ()] or rr.type - rr.class = dns.class[self:word ()] or rr.class - rr.ttl = 0x10000*self:word () + self:word () - rr.rdlength = self:word () - if rr.ttl == 0 then -- pass - else rr.tod = self.time + rr.ttl end +function resolver.LOC_tostring(rr) -- - - - - - - - - - - - - LOC_tostring + local t = {}; - local remember = self.offset - local rr_parser = self[dns.type[rr.type]] - if rr_parser then rr_parser (self, rr) end - self.offset = remember - rr.rdata = self:sub (rr.rdlength) - return rr - end + --[[ + for k,name in pairs { 'size', 'horiz_pre', 'vert_pre', 'latitude', 'longitude', 'altitude' } do + append(t, string.format('%4s%-10s: %12.0f\n', '', name, rr.loc[name])); + end + --]] + + append(t, string.format( + '%s %s %.2fm %.2fm %.2fm %.2fm', + LOC_tostring_degrees (rr.loc.latitude, 'N', 'S'), + LOC_tostring_degrees (rr.loc.longitude, 'E', 'W'), + (rr.loc.altitude - 10000000) / 100, + rr.loc.size / 100, + rr.loc.horiz_pre / 100, + rr.loc.vert_pre / 100 + )); + + return table.concat(t); +end -function resolver:rrs (count) -- - - - - - - - - - - - - - - - - - - - - rrs - local rrs = {} - for i = 1,count do append (rrs, self:rr ()) end - return rrs - end +function resolver:NS(rr) -- - - - - - - - - - - - - - - - - - - - - - - NS + rr.ns = self:name(); +end -function resolver:decode (packet, force) -- - - - - - - - - - - - - - decode +function resolver:SOA(rr) -- - - - - - - - - - - - - - - - - - - - - - SOA +end - self.packet, self.offset = packet, 1 - local header = self:header (force) - if not header then return nil end - local response = { header = header } - response.question = {} - local offset = self.offset - for i = 1,response.header.qdcount do - append (response.question, self:question ()) end - response.question.raw = string.sub (self.packet, offset, self.offset - 1) +function resolver:SRV(rr) -- - - - - - - - - - - - - - - - - - - - - - SRV + rr.srv = {}; + rr.srv.priority = self:word(); + rr.srv.weight = self:word(); + rr.srv.port = self:word(); + rr.srv.target = self:name(); +end - if not force then - if not self.active[response.header.id] or - not self.active[response.header.id][response.question.raw] then - return nil end end - response.answer = self:rrs (response.header.ancount) - response.authority = self:rrs (response.header.nscount) - response.additional = self:rrs (response.header.arcount) +function SRV_tostring(rr) -- - - - - - - - - - - - - - - - - - SRV_tostring + local s = rr.srv; + return string.format( '%5d %5d %5d %s', s.priority, s.weight, s.port, s.target ); +end - return response - end +function resolver:TXT(rr) -- - - - - - - - - - - - - - - - - - - - - - TXT + rr.txt = self:sub (rr.rdlength); +end --- socket layer -------------------------------------------------- socket layer +function resolver:rr() -- - - - - - - - - - - - - - - - - - - - - - - - rr + local rr = {}; + setmetatable(rr, rr_metatable); + rr.name = self:name(self); + rr.type = dns.type[self:word()] or rr.type; + rr.class = dns.class[self:word()] or rr.class; + rr.ttl = 0x10000*self:word() + self:word(); + rr.rdlength = self:word(); -resolver.delays = { 1, 3, 11, 45 } + if rr.ttl <= 0 then + rr.tod = self.time + 30; + else + rr.tod = self.time + rr.ttl; + end + local remember = self.offset; + local rr_parser = self[dns.type[rr.type]]; + if rr_parser then rr_parser(self, rr); end + self.offset = remember; + rr.rdata = self:sub(rr.rdlength); + return rr; +end -function resolver:addnameserver (address) -- - - - - - - - - - addnameserver - self.server = self.server or {} - append (self.server, address) - end +function resolver:rrs (count) -- - - - - - - - - - - - - - - - - - - - - rrs + local rrs = {}; + for i = 1,count do append(rrs, self:rr()); end + return rrs; +end -function resolver:setnameserver (address) -- - - - - - - - - - setnameserver - self.server = {} - self:addnameserver (address) - end +function resolver:decode(packet, force) -- - - - - - - - - - - - - - decode + self.packet, self.offset = packet, 1; + local header = self:header(force); + if not header then return nil; end + local response = { header = header }; -function resolver:adddefaultnameservers () -- - - - - adddefaultnameservers - local resolv_conf = io.open("/etc/resolv.conf"); - if resolv_conf then - for line in resolv_conf:lines() do - local address = string.match (line, 'nameserver%s+(%d+%.%d+%.%d+%.%d+)') - if address then self:addnameserver (address) end - end - else -- FIXME correct for windows, using opendns nameservers for now - self:addnameserver ("208.67.222.222") - self:addnameserver ("208.67.220.220") - end + response.question = {}; + local offset = self.offset; + for i = 1,response.header.qdcount do + append(response.question, self:question()); + end + response.question.raw = string.sub(self.packet, offset, self.offset - 1); + + if not force then + if not self.active[response.header.id] or not self.active[response.header.id][response.question.raw] then + return nil; + end + end + + response.answer = self:rrs(response.header.ancount); + response.authority = self:rrs(response.header.nscount); + response.additional = self:rrs(response.header.arcount); + + return response; end -function resolver:getsocket (servernum) -- - - - - - - - - - - - - getsocket +-- socket layer -------------------------------------------------- socket layer - self.socket = self.socket or {} - self.socketset = self.socketset or {} - local sock = self.socket[servernum] - if sock then return sock end +resolver.delays = { 1, 3 }; - sock = socket.udp () - if self.socket_wrapper then sock = self.socket_wrapper (sock) end - sock:settimeout (0) - -- todo: attempt to use a random port, fallback to 0 - sock:setsockname ('*', 0) - sock:setpeername (self.server[servernum], 53) - self.socket[servernum] = sock - self.socketset[sock] = sock - return sock - end +function resolver:addnameserver(address) -- - - - - - - - - - addnameserver + self.server = self.server or {}; + append(self.server, address); +end -function resolver:socket_wrapper_set (func) -- - - - - - - socket_wrapper_set - self.socket_wrapper = func - end +function resolver:setnameserver(address) -- - - - - - - - - - setnameserver + self.server = {}; + self:addnameserver(address); +end -function resolver:closeall () -- - - - - - - - - - - - - - - - - - closeall - for i,sock in ipairs (self.socket) do self.socket[i]:close () end - self.socket = {} - end +function resolver:adddefaultnameservers() -- - - - - adddefaultnameservers + if is_windows then + if windows then + for _, server in ipairs(windows.get_nameservers()) do + self:addnameserver(server); + end + end + if not self.server or #self.server == 0 then + -- TODO log warning about no nameservers, adding opendns servers as fallback + self:addnameserver("208.67.222.222"); + self:addnameserver("208.67.220.220") ; + end + else -- posix + local resolv_conf = io.open("/etc/resolv.conf"); + if resolv_conf then + for line in resolv_conf:lines() do + local address = line:gsub("#.*$", ""):match('^%s*nameserver%s+(%d+%.%d+%.%d+%.%d+)%s*$'); + if address then self:addnameserver(address) end + end + end + if not self.server or #self.server == 0 then + -- TODO log warning about no nameservers, adding localhost as the default nameserver + self:addnameserver("127.0.0.1"); + end + end +end -function resolver:remember (rr, type) -- - - - - - - - - - - - - - remember - --print ('remember', type, rr.class, rr.type, rr.name) +function resolver:getsocket(servernum) -- - - - - - - - - - - - - getsocket + self.socket = self.socket or {}; + self.socketset = self.socketset or {}; - if type ~= '*' then - type = rr.type - local all = get (self.cache, rr.class, '*', rr.name) - --print ('remember all', all) - if all then append (all, rr) end - end + local sock = self.socket[servernum]; + if sock then return sock; end - self.cache = self.cache or setmetatable ({}, cache_metatable) - local rrs = get (self.cache, rr.class, type, rr.name) or - set (self.cache, rr.class, type, rr.name, setmetatable ({}, rrs_metatable)) - append (rrs, rr) + sock = socket.udp(); + if self.socket_wrapper then sock = self.socket_wrapper(sock, self); end + sock:settimeout(0); + -- todo: attempt to use a random port, fallback to 0 + sock:setsockname('*', 0); + sock:setpeername(self.server[servernum], 53); + self.socket[servernum] = sock; + self.socketset[sock] = servernum; + return sock; +end - if type == 'MX' then self.unsorted[rrs] = true end - end +function resolver:voidsocket(sock) + if self.socket[sock] then + self.socketset[self.socket[sock]] = nil; + self.socket[sock] = nil; + elseif self.socketset[sock] then + self.socket[self.socketset[sock]] = nil; + self.socketset[sock] = nil; + end +end +function resolver:socket_wrapper_set(func) -- - - - - - - socket_wrapper_set + self.socket_wrapper = func; +end -local function comp_mx (a, b) -- - - - - - - - - - - - - - - - - - - comp_mx - return (a.pref == b.pref) and (a.mx < b.mx) or (a.pref < b.pref) - end + +function resolver:closeall () -- - - - - - - - - - - - - - - - - - closeall + for i,sock in ipairs(self.socket) do + self.socket[i] = nil; + self.socketset[sock] = nil; + sock:close(); + end +end + + +function resolver:remember(rr, type) -- - - - - - - - - - - - - - remember + --print ('remember', type, rr.class, rr.type, rr.name) + + if type ~= '*' then + type = rr.type; + local all = get(self.cache, rr.class, '*', rr.name); + --print('remember all', all); + if all then append(all, rr); end + end + + self.cache = self.cache or setmetatable({}, cache_metatable); + local rrs = get(self.cache, rr.class, type, rr.name) or + set(self.cache, rr.class, type, rr.name, setmetatable({}, rrs_metatable)); + append(rrs, rr); + + if type == 'MX' then self.unsorted[rrs] = true; end +end + + +local function comp_mx(a, b) -- - - - - - - - - - - - - - - - - - - comp_mx + return (a.pref == b.pref) and (a.mx < b.mx) or (a.pref < b.pref); +end function resolver:peek (qname, qtype, qclass) -- - - - - - - - - - - - peek - qname, qtype, qclass = standardize (qname, qtype, qclass) - local rrs = get (self.cache, qclass, qtype, qname) - if not rrs then return nil end - if prune (rrs, socket.gettime ()) and qtype == '*' or not next (rrs) then - set (self.cache, qclass, qtype, qname, nil) return nil end - if self.unsorted[rrs] then table.sort (rrs, comp_mx) end - return rrs - end - - -function resolver:purge (soft) -- - - - - - - - - - - - - - - - - - - purge - if soft == 'soft' then - self.time = socket.gettime () - for class,types in pairs (self.cache or {}) do - for type,names in pairs (types) do - for name,rrs in pairs (names) do - prune (rrs, self.time, 'soft') - end end end - else self.cache = {} end - end - - -function resolver:query (qname, qtype, qclass) -- - - - - - - - - - -- query - - qname, qtype, qclass = standardize (qname, qtype, qclass) - - if not self.server then self:adddefaultnameservers () end - - local question = encodeQuestion (qname, qtype, qclass) - local peek = self:peek (qname, qtype, qclass) - if peek then return peek end - - local header, id = encodeHeader () - --print ('query id', id, qclass, qtype, qname) - local o = { packet = header..question, - server = 1, - delay = 1, - retry = socket.gettime () + self.delays[1] } - self:getsocket (o.server):send (o.packet) + qname, qtype, qclass = standardize(qname, qtype, qclass); + local rrs = get(self.cache, qclass, qtype, qname); + if not rrs then return nil; end + if prune(rrs, socket.gettime()) and qtype == '*' or not next(rrs) then + set(self.cache, qclass, qtype, qname, nil); + return nil; + end + if self.unsorted[rrs] then table.sort (rrs, comp_mx); end + return rrs; +end - -- remember the query - self.active[id] = self.active[id] or {} - self.active[id][question] = o - -- remember which coroutine wants the answer - local co = coroutine.running () - if co then - set (self.wanted, qclass, qtype, qname, co, true) - --set (self.yielded, co, qclass, qtype, qname, true) - end +function resolver:purge(soft) -- - - - - - - - - - - - - - - - - - - purge + if soft == 'soft' then + self.time = socket.gettime(); + for class,types in pairs(self.cache or {}) do + for type,names in pairs(types) do + for name,rrs in pairs(names) do + prune(rrs, self.time, 'soft') + end + end + end + else self.cache = {}; end end +function resolver:query(qname, qtype, qclass) -- - - - - - - - - - -- query + qname, qtype, qclass = standardize(qname, qtype, qclass) -function resolver:receive (rset) -- - - - - - - - - - - - - - - - - receive + if not self.server then self:adddefaultnameservers(); end - --print 'receive' print (self.socket) - self.time = socket.gettime () - rset = rset or self.socket + local question = encodeQuestion(qname, qtype, qclass); + local peek = self:peek (qname, qtype, qclass); + if peek then return peek; end - local response - for i,sock in pairs (rset) do + local header, id = encodeHeader(); + --print ('query id', id, qclass, qtype, qname) + local o = { + packet = header..question, + server = self.best_server, + delay = 1, + retry = socket.gettime() + self.delays[1] + }; - if self.socketset[sock] then - local packet = sock:receive () - if packet then + -- remember the query + self.active[id] = self.active[id] or {}; + self.active[id][question] = o; - response = self:decode (packet) - if response then - --print 'received response' - --self.print (response) + -- remember which coroutine wants the answer + local co = coroutine.running(); + if co then + set(self.wanted, qclass, qtype, qname, co, true); + --set(self.yielded, co, qclass, qtype, qname, true); + end - for i,section in pairs { 'answer', 'authority', 'additional' } do - for j,rr in pairs (response[section]) do - self:remember (rr, response.question[1].type) end end + self:getsocket (o.server):send (o.packet) +end - -- retire the query - local queries = self.active[response.header.id] - if queries[response.question.raw] then - queries[response.question.raw] = nil end - if not next (queries) then self.active[response.header.id] = nil end - if not next (self.active) then self:closeall () end +function resolver:servfail(sock) + -- Resend all queries for this server + + local num = self.socketset[sock] + + -- Socket is dead now + self:voidsocket(sock); + + -- Find all requests to the down server, and retry on the next server + self.time = socket.gettime(); + for id,queries in pairs(self.active) do + for question,o in pairs(queries) do + if o.server == num then -- This request was to the broken server + o.server = o.server + 1 -- Use next server + if o.server > #self.server then + o.server = 1; + end + + o.retries = (o.retries or 0) + 1; + if o.retries >= #self.server then + --print('timeout'); + queries[question] = nil; + else + local _a = self:getsocket(o.server); + if _a then _a:send(o.packet); end + end + end + end + end + + if num == self.best_server then + self.best_server = self.best_server + 1; + if self.best_server > #self.server then + -- Exhausted all servers, try first again + self.best_server = 1; + end + end +end - -- was the query on the wanted list? - local q = response.question - local cos = get (self.wanted, q.class, q.type, q.name) - if cos then - for co in pairs (cos) do - set (self.yielded, co, q.class, q.type, q.name, nil) - if coroutine.status(co) == "suspended" then coroutine.resume (co) end - end - set (self.wanted, q.class, q.type, q.name, nil) - end end end end end +function resolver:receive(rset) -- - - - - - - - - - - - - - - - - receive + --print('receive'); print(self.socket); + self.time = socket.gettime(); + rset = rset or self.socket; + + local response; + for i,sock in pairs(rset) do + + if self.socketset[sock] then + local packet = sock:receive(); + if packet then + response = self:decode(packet); + if response then + --print('received response'); + --self.print(response); + + for i,section in pairs({ 'answer', 'authority', 'additional' }) do + for j,rr in pairs(response[section]) do + self:remember(rr, response.question[1].type) + end + end + + -- retire the query + local queries = self.active[response.header.id]; + if queries[response.question.raw] then + queries[response.question.raw] = nil; + end + if not next(queries) then self.active[response.header.id] = nil; end + if not next(self.active) then self:closeall(); end + + -- was the query on the wanted list? + local q = response.question; + local cos = get(self.wanted, q.class, q.type, q.name); + if cos then + for co in pairs(cos) do + set(self.yielded, co, q.class, q.type, q.name, nil); + if coroutine.status(co) == "suspended" then coroutine.resume(co); end + end + set(self.wanted, q.class, q.type, q.name, nil); + end + end + end + end + end - return response - end + return response; +end function resolver:feed(sock, packet) - --print 'receive' print (self.socket) - self.time = socket.gettime () - - local response = self:decode (packet) - if response then - --print 'received response' - --self.print (response) - - for i,section in pairs { 'answer', 'authority', 'additional' } do - for j,rr in pairs (response[section]) do - self:remember (rr, response.question[1].type) - end - end - - -- retire the query - local queries = self.active[response.header.id] - if queries[response.question.raw] then - queries[response.question.raw] = nil - end - if not next (queries) then self.active[response.header.id] = nil end - if not next (self.active) then self:closeall () end - - -- was the query on the wanted list? - local q = response.question[1] - if q then - local cos = get (self.wanted, q.class, q.type, q.name) - if cos then - for co in pairs (cos) do - set (self.yielded, co, q.class, q.type, q.name, nil) - if coroutine.status(co) == "suspended" then coroutine.resume (co) end - end - set (self.wanted, q.class, q.type, q.name, nil) - end - end - end - - return response + --print('receive'); print(self.socket); + self.time = socket.gettime(); + + local response = self:decode(packet); + if response then + --print('received response'); + --self.print(response); + + for i,section in pairs({ 'answer', 'authority', 'additional' }) do + for j,rr in pairs(response[section]) do + self:remember(rr, response.question[1].type); + end + end + + -- retire the query + local queries = self.active[response.header.id]; + if queries[response.question.raw] then + queries[response.question.raw] = nil; + end + if not next(queries) then self.active[response.header.id] = nil; end + if not next(self.active) then self:closeall(); end + + -- was the query on the wanted list? + local q = response.question[1]; + if q then + local cos = get(self.wanted, q.class, q.type, q.name); + if cos then + for co in pairs(cos) do + set(self.yielded, co, q.class, q.type, q.name, nil); + if coroutine.status(co) == "suspended" then coroutine.resume(co); end + end + set(self.wanted, q.class, q.type, q.name, nil); + end + end + end + + return response; end function resolver:cancel(data) - local cos = get (self.wanted, unpack(data, 1, 3)) + local cos = get(self.wanted, unpack(data, 1, 3)); if cos then cos[data[4]] = nil; end end -function resolver:pulse () -- - - - - - - - - - - - - - - - - - - - - pulse +function resolver:pulse() -- - - - - - - - - - - - - - - - - - - - - pulse + --print(':pulse'); + while self:receive() do end + if not next(self.active) then return nil; end + + self.time = socket.gettime(); + for id,queries in pairs(self.active) do + for question,o in pairs(queries) do + if self.time >= o.retry then + + o.server = o.server + 1; + if o.server > #self.server then + o.server = 1; + o.delay = o.delay + 1; + end + + if o.delay > #self.delays then + --print('timeout'); + queries[question] = nil; + if not next(queries) then self.active[id] = nil; end + if not next(self.active) then return nil; end + else + --print('retry', o.server, o.delay); + local _a = self.socket[o.server]; + if _a then _a:send(o.packet); end + o.retry = self.time + self.delays[o.delay]; + end + end + end + end - --print ':pulse' - while self:receive() do end - if not next (self.active) then return nil end + if next(self.active) then return true; end + return nil; +end - self.time = socket.gettime () - for id,queries in pairs (self.active) do - for question,o in pairs (queries) do - if self.time >= o.retry then - o.server = o.server + 1 - if o.server > #self.server then - o.server = 1 - o.delay = o.delay + 1 - end +function resolver:lookup(qname, qtype, qclass) -- - - - - - - - - - lookup + self:query (qname, qtype, qclass) + while self:pulse() do socket.select(self.socket, nil, 4); end + --print(self.cache); + return self:peek(qname, qtype, qclass); +end - if o.delay > #self.delays then - --print ('timeout') - queries[question] = nil - if not next (queries) then self.active[id] = nil end - if not next (self.active) then return nil end - else - --print ('retry', o.server, o.delay) - local _a = self.socket[o.server]; - if _a then _a:send (o.packet) end - o.retry = self.time + self.delays[o.delay] - end end end end +function resolver:lookupex(handler, qname, qtype, qclass) -- - - - - - - - - - lookup + return self:peek(qname, qtype, qclass) or self:query(qname, qtype, qclass); +end - if next (self.active) then return true end - return nil - end +--print ---------------------------------------------------------------- print -function resolver:lookup (qname, qtype, qclass) -- - - - - - - - - - lookup - self:query (qname, qtype, qclass) - while self:pulse () do socket.select (self.socket, nil, 4) end - --print (self.cache) - return self:peek (qname, qtype, qclass) - end -function resolver:lookupex (handler, qname, qtype, qclass) -- - - - - - - - - - lookup - return self:peek (qname, qtype, qclass) or self:query (qname, qtype, qclass) - end +local hints = { -- - - - - - - - - - - - - - - - - - - - - - - - - - - hints + qr = { [0]='query', 'response' }, + opcode = { [0]='query', 'inverse query', 'server status request' }, + aa = { [0]='non-authoritative', 'authoritative' }, + tc = { [0]='complete', 'truncated' }, + rd = { [0]='recursion not desired', 'recursion desired' }, + ra = { [0]='recursion not available', 'recursion available' }, + z = { [0]='(reserved)' }, + rcode = { [0]='no error', 'format error', 'server failure', 'name error', 'not implemented' }, + + type = dns.type, + class = dns.class +}; + + +local function hint(p, s) -- - - - - - - - - - - - - - - - - - - - - - hint + return (hints[s] and hints[s][p[s]]) or ''; +end ---print ---------------------------------------------------------------- print +function resolver.print(response) -- - - - - - - - - - - - - resolver.print + for s,s in pairs { 'id', 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z', + 'rcode', 'qdcount', 'ancount', 'nscount', 'arcount' } do + print( string.format('%-30s', 'header.'..s), response.header[s], hint(response.header, s) ); + end + for i,question in ipairs(response.question) do + print(string.format ('question[%i].name ', i), question.name); + print(string.format ('question[%i].type ', i), question.type); + print(string.format ('question[%i].class ', i), question.class); + end -local hints = { -- - - - - - - - - - - - - - - - - - - - - - - - - - - hints - qr = { [0]='query', 'response' }, - opcode = { [0]='query', 'inverse query', 'server status request' }, - aa = { [0]='non-authoritative', 'authoritative' }, - tc = { [0]='complete', 'truncated' }, - rd = { [0]='recursion not desired', 'recursion desired' }, - ra = { [0]='recursion not available', 'recursion available' }, - z = { [0]='(reserved)' }, - rcode = { [0]='no error', 'format error', 'server failure', 'name error', - 'not implemented' }, - - type = dns.type, - class = dns.class, } - - -local function hint (p, s) -- - - - - - - - - - - - - - - - - - - - - - hint - return (hints[s] and hints[s][p[s]]) or '' end - - -function resolver.print (response) -- - - - - - - - - - - - - resolver.print - - for s,s in pairs { 'id', 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z', - 'rcode', 'qdcount', 'ancount', 'nscount', 'arcount' } do - print ( string.format ('%-30s', 'header.'..s), - response.header[s], hint (response.header, s) ) - end - - for i,question in ipairs (response.question) do - print (string.format ('question[%i].name ', i), question.name) - print (string.format ('question[%i].type ', i), question.type) - print (string.format ('question[%i].class ', i), question.class) - end - - local common = { name=1, type=1, class=1, ttl=1, rdlength=1, rdata=1 } - local tmp - for s,s in pairs {'answer', 'authority', 'additional'} do - for i,rr in pairs (response[s]) do - for j,t in pairs { 'name', 'type', 'class', 'ttl', 'rdlength' } do - tmp = string.format ('%s[%i].%s', s, i, t) - print (string.format ('%-30s', tmp), rr[t], hint (rr, t)) - end - for j,t in pairs (rr) do - if not common[j] then - tmp = string.format ('%s[%i].%s', s, i, j) - print (string.format ('%-30s %s', tostring(tmp), tostring(t))) - end end end end end + local common = { name=1, type=1, class=1, ttl=1, rdlength=1, rdata=1 }; + local tmp; + for s,s in pairs({'answer', 'authority', 'additional'}) do + for i,rr in pairs(response[s]) do + for j,t in pairs({ 'name', 'type', 'class', 'ttl', 'rdlength' }) do + tmp = string.format('%s[%i].%s', s, i, t); + print(string.format('%-30s', tmp), rr[t], hint(rr, t)); + end + for j,t in pairs(rr) do + if not common[j] then + tmp = string.format('%s[%i].%s', s, i, j); + print(string.format('%-30s %s', tostring(tmp), tostring(t))); + end + end + end + end +end -- module api ------------------------------------------------------ module api -local function resolve (func, ...) -- - - - - - - - - - - - - - resolver_get - dns._resolver = dns._resolver or dns.resolver () - return func (dns._resolver, ...) - end +local function resolve(func, ...) -- - - - - - - - - - - - - - resolver_get + return func(dns._resolver, ...); +end function dns.resolver () -- - - - - - - - - - - - - - - - - - - - - resolver + -- this function seems to be redundant with resolver.new () - -- this function seems to be redundant with resolver.new () - - local r = { active = {}, cache = {}, unsorted = {}, wanted = {}, yielded = {} } - setmetatable (r, resolver) - setmetatable (r.cache, cache_metatable) - setmetatable (r.unsorted, { __mode = 'kv' }) - return r - end + local r = { active = {}, cache = {}, unsorted = {}, wanted = {}, yielded = {}, best_server = 1 }; + setmetatable (r, resolver); + setmetatable (r.cache, cache_metatable); + setmetatable (r.unsorted, { __mode = 'kv' }); + return r; +end -function dns.lookup (...) -- - - - - - - - - - - - - - - - - - - - - lookup - return resolve (resolver.lookup, ...) end +function dns.lookup(...) -- - - - - - - - - - - - - - - - - - - - - lookup + return resolve(resolver.lookup, ...); +end -function dns.purge (...) -- - - - - - - - - - - - - - - - - - - - - - purge - return resolve (resolver.purge, ...) end +function dns.purge(...) -- - - - - - - - - - - - - - - - - - - - - - purge + return resolve(resolver.purge, ...); +end -function dns.peek (...) -- - - - - - - - - - - - - - - - - - - - - - - peek - return resolve (resolver.peek, ...) end +function dns.peek(...) -- - - - - - - - - - - - - - - - - - - - - - - peek + return resolve(resolver.peek, ...); +end -function dns.query (...) -- - - - - - - - - - - - - - - - - - - - - - query - return resolve (resolver.query, ...) end +function dns.query(...) -- - - - - - - - - - - - - - - - - - - - - - query + return resolve(resolver.query, ...); +end -function dns.feed (...) -- - - - - - - - - - - - - - - - - - - - - - feed - return resolve (resolver.feed, ...) end +function dns.feed(...) -- - - - - - - - - - - - - - - - - - - - - - feed + return resolve(resolver.feed, ...); +end function dns.cancel(...) -- - - - - - - - - - - - - - - - - - - - - - cancel - return resolve(resolver.cancel, ...) end + return resolve(resolver.cancel, ...); +end -function dns:socket_wrapper_set (...) -- - - - - - - - - socket_wrapper_set - return resolve (resolver.socket_wrapper_set, ...) end +function dns:socket_wrapper_set(...) -- - - - - - - - - socket_wrapper_set + return resolve(resolver.socket_wrapper_set, ...); +end +dns._resolver = dns.resolver(); -return dns +return dns; diff --git a/net/httpserver.lua b/net/httpserver.lua index 57c8eede..ddb4475c 100644 --- a/net/httpserver.lua +++ b/net/httpserver.lua @@ -61,7 +61,7 @@ local function send_response(request, response) end else -- Response we have is just a string (the body) - log("debug", "Sending response to %s: %s", request.id or "<none>", response or "<none>"); + log("debug", "Sending 200 response to %s", request.id or "<none>"); resp = { "HTTP/1.0 200 OK\r\n" }; t_insert(resp, "Connection: close\r\n"); @@ -89,9 +89,6 @@ local function call_callback(request, err) end callback = (request.server and request.server.handlers[base]) or default_handler; - if callback == default_handler then - log("debug", "Default callback for this request (base: "..tostring(base)..")") - end end if callback then if err then @@ -233,7 +230,7 @@ function destroy_request(request) end request.handler.close() if request.conn then - listener.disconnect(request.conn, "closed"); + listener.disconnect(request.handler, "closed"); end end end @@ -251,13 +248,27 @@ function new(params) end end -function new_from_config(ports, default_base, handle_request) +function set_default_handler(handler) + default_handler = handler; +end + +function new_from_config(ports, handle_request, default_options) + if type(handle_request) == "string" then -- COMPAT with old plugins + log("warn", "Old syntax of httpserver.new_from_config being used to register %s", handle_request); + handle_request, default_options = default_options, { base = handle_request }; + end for _, options in ipairs(ports) do - local port, base, ssl, interface = 5280, default_base, false, nil; + local port = default_options.port or 5280; + local base = default_options.base; + local ssl = default_options.ssl or false; + local interface = default_options.interface; if type(options) == "number" then port = options; elseif type(options) == "table" then - port, base, ssl, interface = options.port or 5280, options.path or default_base, options.ssl or false, options.interface; + port = options.port or port; + base = options.path or base; + ssl = options.ssl or ssl; + interface = options.interface or interface; elseif type(options) == "string" then base = options; end @@ -267,7 +278,9 @@ function new_from_config(ports, default_base, handle_request) ssl.protocol = "sslv23"; end - new{ port = port, base = base, handler = handle_request, ssl = ssl, type = (ssl and "ssl") or "tcp" } + new{ port = port, interface = interface, + base = base, handler = handle_request, + ssl = ssl, type = (ssl and "ssl") or "tcp" }; end end diff --git a/net/server.lua b/net/server.lua index 966006c1..6ab8ce91 100644 --- a/net/server.lua +++ b/net/server.lua @@ -157,6 +157,7 @@ _cleanqueue = false -- clean bufferqueue after using _maxclientsperserver = 1000
+_maxsslhandshake = 30 -- max handshake round-trips
----------------------------------// PRIVATE //--
wrapserver = function( listeners, socket, ip, serverport, pattern, sslctx, maxconnections, startssl ) -- this function wraps a server
@@ -230,6 +231,9 @@ wrapserver = function( listeners, socket, ip, serverport, pattern, sslctx, maxco handler.ssl = function( )
return ssl
end
+ handler.sslctx = function( )
+ return sslctx
+ end
handler.remove = function( )
connections = connections - 1
end
@@ -246,7 +250,7 @@ wrapserver = function( listeners, socket, ip, serverport, pattern, sslctx, maxco _socketlist[ socket ] = nil
handler = nil
socket = nil
- mem_free( )
+ --mem_free( )
out_put "server.lua: closed server handler and removed sockets from list"
end
handler.ip = function( )
@@ -297,6 +301,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport local ssl
local dispatch = listeners.incoming or listeners.listener
+ local status = listeners.status
local disconnect = listeners.disconnect
local bufferqueue = { } -- buffer array
@@ -336,6 +341,9 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport handler.ssl = function( )
return ssl
end
+ handler.sslctx = function ( )
+ return sslctx
+ end
handler.send = function( _, data, i, j )
return send( socket, data, i, j )
end
@@ -363,17 +371,20 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport send( socket, table_concat( bufferqueue, "", 1, bufferqueuelen ), 1, bufferlen ) -- forced send
end
end
- _ = shutdown and shutdown( socket )
- socket:close( )
- _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
- _socketlist[ socket ] = nil
+ if socket then
+ _ = shutdown and shutdown( socket )
+ socket:close( )
+ _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
+ _socketlist[ socket ] = nil
+ socket = nil
+ else
+ out_put "server.lua: socket already closed"
+ end
if handler then
_writetimes[ handler ] = nil
_closelist[ handler ] = nil
handler = nil
end
- socket = nil
- mem_free( )
if server then
server.remove( )
end
@@ -396,9 +407,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport handler.write = idfalse -- dont write anymore
return false
elseif socket and not _sendlist[ socket ] then
- _sendlistlen = _sendlistlen + 1
- _sendlist[ _sendlistlen ] = socket
- _sendlist[ socket ] = _sendlistlen
+ _sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
end
bufferqueuelen = bufferqueuelen + 1
bufferqueue[ bufferqueuelen ] = data
@@ -446,9 +455,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport handler.write = write
if noread then
noread = false
- _readlistlen = _readlistlen + 1
- _readlist[ socket ] = _readlistlen
- _readlist[ _readlistlen ] = socket
+ _readlistlen = addsocket(_readlist, socket, _readlistlen)
_readtimes[ handler ] = _currenttime
end
if nosend then
@@ -472,10 +479,10 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport readtraffic = readtraffic + count
_readtraffic = _readtraffic + count
_readtimes[ handler ] = _currenttime
- --out_put( "server.lua: read data '", buffer, "', error: ", err )
+ --out_put( "server.lua: read data '", buffer:gsub("[^%w%p ]", "."), "', error: ", err )
return dispatch( handler, buffer, err )
else -- connections was closed or fatal error
- out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " error: ", tostring(err) )
+ out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " read error: ", tostring(err) )
fatalerror = true
disconnect( handler, err )
_ = handler and handler.close( )
@@ -483,13 +490,19 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport end
end
local _sendbuffer = function( ) -- this function sends data
- local buffer = table_concat( bufferqueue, "", 1, bufferqueuelen )
- local succ, err, byte = send( socket, buffer, 1, bufferlen )
- local count = ( succ or byte or 0 ) * STAT_UNIT
- sendtraffic = sendtraffic + count
- _sendtraffic = _sendtraffic + count
- _ = _cleanqueue and clean( bufferqueue )
- --out_put( "server.lua: sended '", buffer, "', bytes: ", tostring(succ), ", error: ", tostring(err), ", part: ", tostring(byte), ", to: ", tostring(ip), ":", tostring(clientport) )
+ local succ, err, byte, buffer, count;
+ local count;
+ if socket then
+ buffer = table_concat( bufferqueue, "", 1, bufferqueuelen )
+ succ, err, byte = send( socket, buffer, 1, bufferlen )
+ count = ( succ or byte or 0 ) * STAT_UNIT
+ sendtraffic = sendtraffic + count
+ _sendtraffic = _sendtraffic + count
+ _ = _cleanqueue and clean( bufferqueue )
+ --out_put( "server.lua: sended '", buffer, "', bytes: ", tostring(succ), ", error: ", tostring(err), ", part: ", tostring(byte), ", to: ", tostring(ip), ":", tostring(clientport) )
+ else
+ succ, err, count = false, "closed", 0;
+ end
if succ then -- sending succesful
bufferqueuelen = 0
bufferlen = 0
@@ -506,7 +519,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport _writetimes[ handler ] = _currenttime
return true
else -- connection was closed during sending or fatal error
- out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " error: ", tostring(err) )
+ out_put( "server.lua: client ", tostring(ip), ":", tostring(clientport), " write error: ", tostring(err) )
fatalerror = true
disconnect( handler, err )
_ = handler and handler.close( )
@@ -514,38 +527,40 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport end
end
- if sslctx then -- ssl?
+ -- Set the sslctx
+ local handshake;
+ function handler.set_sslctx(new_sslctx)
ssl = true
+ sslctx = new_sslctx;
local wrote
local read
- local handshake = coroutine_wrap( function( client ) -- create handshake coroutine
+ handshake = coroutine_wrap( function( client ) -- create handshake coroutine
local err
- for i = 1, 10 do -- 10 handshake attemps
- _sendlistlen = ( wrote and removesocket( _sendlist, socket, _sendlistlen ) ) or _sendlistlen
- _readlistlen = ( read and removesocket( _readlist, socket, _readlistlen ) ) or _readlistlen
+ for i = 1, _maxsslhandshake do
+ _sendlistlen = ( wrote and removesocket( _sendlist, client, _sendlistlen ) ) or _sendlistlen
+ _readlistlen = ( read and removesocket( _readlist, client, _readlistlen ) ) or _readlistlen
read, wrote = nil, nil
_, err = client:dohandshake( )
if not err then
out_put( "server.lua: ssl handshake done" )
handler.readbuffer = _readbuffer -- when handshake is done, replace the handshake function with regular functions
handler.sendbuffer = _sendbuffer
- -- return dispatch( handler )
+ _ = status and status( handler, "ssl-handshake-complete" )
+ _readlistlen = addsocket(_readlist, client, _readlistlen)
return true
else
- out_put( "server.lua: error during ssl handshake: ", tostring(err) )
- if err == "wantwrite" and not wrote then
- _sendlistlen = _sendlistlen + 1
- _sendlist[ _sendlistlen ] = client
- wrote = true
- elseif err == "wantread" and not read then
- _readlistlen = _readlistlen + 1
- _readlist [ _readlistlen ] = client
- read = true
- else
- break;
- end
- --coroutine_yield( handler, nil, err ) -- handshake not finished
- coroutine_yield( )
+ out_put( "server.lua: error during ssl handshake: ", tostring(err) )
+ if err == "wantwrite" and not wrote then
+ _sendlistlen = addsocket(_sendlist, client, _sendlistlen)
+ wrote = true
+ elseif err == "wantread" and not read then
+ _readlistlen = addsocket(_readlist, client, _readlistlen)
+ read = true
+ else
+ break;
+ end
+ --coroutine_yield( handler, nil, err ) -- handshake not finished
+ coroutine_yield( )
end
end
disconnect( handler, "ssl handshake failed" )
@@ -553,13 +568,16 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport return false -- handshake failed
end
)
+ end
+ if sslctx then -- ssl?
+ handler.set_sslctx(sslctx);
if startssl then -- ssl now?
--out_put("server.lua: ", "starting ssl handshake")
local err
socket, err = ssl_wrap( socket, sslctx ) -- wrap socket
if err then
out_put( "server.lua: ssl error: ", tostring(err) )
- mem_free( )
+ --mem_free( )
return nil, nil, err -- fatal error
end
socket:settimeout( 0 )
@@ -596,9 +614,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport shutdown = id
_socketlist[ socket ] = handler
- _readlistlen = _readlistlen + 1
- _readlist[ _readlistlen ] = socket
- _readlist[ socket ] = _readlistlen
+ _readlistlen = addsocket(_readlist, socket, _readlistlen)
-- remove traces of the old socket
@@ -630,9 +646,7 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport shutdown = ( ssl and id ) or socket.shutdown
_socketlist[ socket ] = handler
- _readlistlen = _readlistlen + 1
- _readlist[ _readlistlen ] = socket
- _readlist[ socket ] = _readlistlen
+ _readlistlen = addsocket(_readlist, socket, _readlistlen)
return handler, socket
end
@@ -644,6 +658,15 @@ idfalse = function( ) return false
end
+addsocket = function( list, socket, len )
+ if not list[ socket ] then
+ len = len + 1
+ list[ len ] = socket
+ list[ socket ] = len
+ end
+ return len;
+end
+
removesocket = function( list, socket, len ) -- this function removes sockets from a list ( copied from copas )
local pos = list[ socket ]
if pos then
@@ -664,7 +687,7 @@ closesocket = function( socket ) _readlistlen = removesocket( _readlist, socket, _readlistlen )
_socketlist[ socket ] = nil
socket:close( )
- mem_free( )
+ --mem_free( )
end
----------------------------------// PUBLIC //--
@@ -698,8 +721,7 @@ addserver = function( listeners, port, addr, pattern, sslctx, maxconnections, st return nil, err
end
server:settimeout( 0 )
- _readlistlen = _readlistlen + 1
- _readlist[ _readlistlen ] = server
+ _readlistlen = addsocket(_readlist, server, _readlistlen)
_server[ port ] = handler
_socketlist[ server ] = handler
out_put( "server.lua: new server listener on '", addr, ":", port, "'" )
@@ -713,7 +735,7 @@ end removeserver = function( port )
local handler = _server[ port ]
if not handler then
- return nil, "no server found on port '" .. tostring( port ) "'"
+ return nil, "no server found on port '" .. tostring( port ) .. "'"
end
handler.close( )
_server[ port ] = nil
@@ -733,11 +755,11 @@ closeall = function( ) _sendlist = { }
_timerlist = { }
_socketlist = { }
- mem_free( )
+ --mem_free( )
end
getsettings = function( )
- return _selecttimeout, _sleeptime, _maxsendlen, _maxreadlen, _checkinterval, _sendtimeout, _readtimeout, _cleanqueue, _maxclientsperserver
+ return _selecttimeout, _sleeptime, _maxsendlen, _maxreadlen, _checkinterval, _sendtimeout, _readtimeout, _cleanqueue, _maxclientsperserver, _maxsslhandshake
end
changesettings = function( new )
@@ -753,6 +775,7 @@ changesettings = function( new ) _readtimeout = tonumber( new.readtimeout ) or _readtimeout
_cleanqueue = new.cleanqueue
_maxclientsperserver = new._maxclientsperserver or _maxclientsperserver
+ _maxsslhandshake = new._maxsslhandshake or _maxsslhandshake
return true
end
@@ -805,7 +828,7 @@ loop = function( ) -- this is the main loop of the program _currenttime = os_time( )
if os_difftime( _currenttime - _timer ) >= 1 then
for i = 1, _timerlistlen do
- _timerlist[ i ]( ) -- fire timers
+ _timerlist[ i ]( _currenttime ) -- fire timers
end
_timer = _currenttime
end
@@ -820,9 +843,7 @@ end local wrapclient = function( socket, ip, serverport, listeners, pattern, sslctx, startssl )
local handler = wrapconnection( nil, listeners, socket, ip, serverport, "clientport", pattern, sslctx, startssl )
_socketlist[ socket ] = handler
- _sendlistlen = _sendlistlen + 1
- _sendlist[ _sendlistlen ] = socket
- _sendlist[ socket ] = _sendlistlen
+ _sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
return handler, socket
end
diff --git a/net/xmppclient_listener.lua b/net/xmppclient_listener.lua index dcc561f3..417dfd4a 100644 --- a/net/xmppclient_listener.lua +++ b/net/xmppclient_listener.lua @@ -27,7 +27,7 @@ local sm_streamopened = sessionmanager.streamopened; local sm_streamclosed = sessionmanager.streamclosed; local st = require "util.stanza"; -local stream_callbacks = { stream_tag = "http://etherx.jabber.org/streams|stream", +local stream_callbacks = { stream_tag = "http://etherx.jabber.org/streams\1stream", default_ns = "jabber:client", streamopened = sm_streamopened, streamclosed = sm_streamclosed, handlestanza = core_process_stanza }; @@ -53,7 +53,7 @@ local xmppclient = { default_port = 5222, default_mode = "*a" }; local function session_reset_stream(session) -- Reset stream - local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "|"); + local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "\1"); session.parser = parser; session.notopen = true; @@ -70,7 +70,7 @@ end local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; -local default_stream_attr = { ["xmlns:stream"] = stream_callbacks.stream_tag:gsub("%|[^|]+$", ""), xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; +local default_stream_attr = { ["xmlns:stream"] = stream_callbacks.stream_tag:match("[^\1]*"), xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; local function session_close(session, reason) local log = session.log or log; if session.conn then @@ -114,11 +114,6 @@ function xmppclient.listener(conn, data) session = sm_new_session(conn); sessions[conn] = session; - -- Logging functions -- - - local conn_name = "c2s"..tostring(conn):match("[a-f0-9]+$"); - session.log = logger.init(conn_name); - session.log("info", "Client connected"); -- Client is using legacy SSL (otherwise mod_tls sets this flag) diff --git a/net/xmppcomponent_listener.lua b/net/xmppcomponent_listener.lua index bee05967..c16f41a0 100644 --- a/net/xmppcomponent_listener.lua +++ b/net/xmppcomponent_listener.lua @@ -32,7 +32,7 @@ local xmlns_component = 'jabber:component:accept'; --- Callbacks/data for xmlhandlers to handle streams for us --- -local stream_callbacks = { stream_tag = "http://etherx.jabber.org/streams|stream", default_ns = xmlns_component }; +local stream_callbacks = { stream_tag = "http://etherx.jabber.org/streams\1stream", default_ns = xmlns_component }; function stream_callbacks.error(session, error, data, data2) log("warn", "Error processing component stream: "..tostring(error)); @@ -87,7 +87,7 @@ end --- Closing a component connection local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; -local default_stream_attr = { ["xmlns:stream"] = stream_callbacks.stream_tag:gsub("%|[^|]+$", ""), xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; +local default_stream_attr = { ["xmlns:stream"] = stream_callbacks.stream_tag:match("[^\1]*"), xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; local function session_close(session, reason) local log = session.log or log; if session.conn then @@ -138,7 +138,7 @@ function component_listener.listener(conn, data) session.log("info", "Incoming Jabber component connection"); - local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "|"); + local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "\1"); session.parser = parser; session.notopen = true; diff --git a/net/xmppserver_listener.lua b/net/xmppserver_listener.lua index 1f27d841..c7e02ec5 100644 --- a/net/xmppserver_listener.lua +++ b/net/xmppserver_listener.lua @@ -17,7 +17,7 @@ local s2s_streamopened = require "core.s2smanager".streamopened; local s2s_streamclosed = require "core.s2smanager".streamclosed; local s2s_destroy_session = require "core.s2smanager".destroy_session; local s2s_attempt_connect = require "core.s2smanager".attempt_connection; -local stream_callbacks = { stream_tag = "http://etherx.jabber.org/streams|stream", +local stream_callbacks = { stream_tag = "http://etherx.jabber.org/streams\1stream", default_ns = "jabber:server", streamopened = s2s_streamopened, streamclosed = s2s_streamclosed, handlestanza = core_process_stanza }; @@ -53,7 +53,7 @@ local xmppserver = { default_port = 5269, default_mode = "*a" }; local function session_reset_stream(session) -- Reset stream - local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "|"); + local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "\1"); session.parser = parser; session.notopen = true; @@ -61,16 +61,16 @@ local function session_reset_stream(session) function session.data(conn, data) local ok, err = parser:parse(data); if ok then return; end - log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " ")); + (session.log or log)("warn", "Received invalid XML: %s", data); + (session.log or log)("warn", "Problem was: %s", err); session:close("xml-not-well-formed"); end return true; end - local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; -local default_stream_attr = { ["xmlns:stream"] = stream_callbacks.stream_tag:gsub("%|[^|]+$", ""), xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; +local default_stream_attr = { ["xmlns:stream"] = stream_callbacks.stream_tag:match("[^\1]*"), xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; local function session_close(session, reason) local log = session.log or log; if session.conn then @@ -100,6 +100,9 @@ local function session_close(session, reason) end end session.sends2s("</stream:stream>"); + if session.notopen or not session.conn.close() then + session.conn.close(true); -- Force FIXME: timer? + end session.conn.close(); xmppserver.disconnect(session.conn, "stream error"); end @@ -134,6 +137,17 @@ function xmppserver.listener(conn, data) end end +function xmppserver.status(conn, status) + if status == "ssl-handshake-complete" then + local session = sessions[conn]; + if session and session.direction == "outgoing" then + local format, to_host, from_host = string.format, session.to_host, session.from_host; + session.log("debug", "Sending stream header..."); + session.sends2s(format([[<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' from='%s' to='%s' version='1.0'>]], from_host, to_host)); + end + end +end + function xmppserver.disconnect(conn, err) local session = sessions[conn]; if session then diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua index 743ebdef..af13bde9 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -6,7 +6,6 @@ -- COPYING file in the source package for more information. -- - module.host = "*" -- Global module local hosts = _G.hosts; @@ -22,17 +21,18 @@ local core_process_stanza = core_process_stanza; local st = require "util.stanza"; local logger = require "util.logger"; local log = logger.init("mod_bosh"); -local stream_callbacks = { stream_tag = "http://jabber.org/protocol/httpbind|body" }; -local config = require "core.configmanager"; + local xmlns_bosh = "http://jabber.org/protocol/httpbind"; -- (hard-coded into a literal in session.send) +local stream_callbacks = { stream_tag = "http://jabber.org/protocol/httpbind\1body", default_ns = xmlns_bosh }; -local BOSH_DEFAULT_HOLD = tonumber(config.get("*", "core", "bosh_default_hold")) or 1; -local BOSH_DEFAULT_INACTIVITY = tonumber(config.get("*", "core", "bosh_max_inactivity")) or 60; -local BOSH_DEFAULT_POLLING = tonumber(config.get("*", "core", "bosh_max_polling")) or 5; -local BOSH_DEFAULT_REQUESTS = tonumber(config.get("*", "core", "bosh_max_requests")) or 2; -local BOSH_DEFAULT_MAXPAUSE = tonumber(config.get("*", "core", "bosh_max_pause")) or 300; +local BOSH_DEFAULT_HOLD = tonumber(module:get_option("bosh_default_hold")) or 1; +local BOSH_DEFAULT_INACTIVITY = tonumber(module:get_option("bosh_max_inactivity")) or 60; +local BOSH_DEFAULT_POLLING = tonumber(module:get_option("bosh_max_polling")) or 5; +local BOSH_DEFAULT_REQUESTS = tonumber(module:get_option("bosh_max_requests")) or 2; +local BOSH_DEFAULT_MAXPAUSE = tonumber(module:get_option("bosh_max_pause")) or 300; local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" }; +local session_close_reply = { headers = default_headers, body = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate" }), attr = {} }; local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; local os_time = os.time; @@ -70,7 +70,7 @@ function handle_request(method, body, request) --log("debug", "Handling new request %s: %s\n----------", request.id, tostring(body)); request.notopen = true; request.log = log; - local parser = lxp.new(init_xmlhandlers(request, stream_callbacks), "|"); + local parser = lxp.new(init_xmlhandlers(request, stream_callbacks), "\1"); parser:parse(body); @@ -112,11 +112,9 @@ end local function bosh_reset_stream(session) session.notopen = true; end -local session_close_reply = { headers = default_headers, body = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate" }), attr = {} }; local function bosh_close_stream(session, reason) (session.log or log)("info", "BOSH client disconnected"); session_close_reply.attr.condition = reason; - local session_close_reply = tostring(session_close_reply); for _, held_request in ipairs(session.requests) do held_request:send(session_close_reply); held_request:destroy(); @@ -144,7 +142,7 @@ function stream_callbacks.streamopened(request, attr) -- New session sid = new_uuid(); - local session = { type = "c2s_unauthed", conn = {}, sid = sid, rid = attr.rid, host = attr.to, bosh_version = attr.ver, bosh_wait = attr.wait, streamid = sid, + local session = { type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid), host = attr.to, bosh_version = attr.ver, bosh_wait = attr.wait, streamid = sid, bosh_hold = BOSH_DEFAULT_HOLD, bosh_max_inactive = BOSH_DEFAULT_INACTIVITY, requests = { }, send_buffer = {}, reset_stream = bosh_reset_stream, close = bosh_close_stream, dispatch_stanza = core_process_stanza, log = logger.init("bosh"..sid), secure = request.secure }; @@ -209,6 +207,21 @@ function stream_callbacks.streamopened(request, attr) return; end + if session.rid then + local rid = tonumber(attr.rid); + local diff = rid - session.rid; + if diff > 1 then + session.log("warn", "rid too large (means a request was lost). Last rid: %d New rid: %s", session.rid, attr.rid); + elseif diff <= 0 then + -- Repeated, ignore + session.log("debug", "rid repeated (on request %s), ignoring: %d", request.id, session.rid); + request.notopen = nil; + t_insert(session.requests, request); + return; + end + session.rid = rid; + end + if attr.type == "terminate" then -- Client wants to end this session session:close(); @@ -245,6 +258,7 @@ function stream_callbacks.handlestanza(request, stanza) end end +local dead_sessions = {}; function on_timer() -- log("debug", "Checking for requests soon to timeout..."); -- Identify requests timing out within the next few seconds @@ -261,21 +275,29 @@ function on_timer() end now = now - 3; + local n_dead_sessions = 0; for session, inactive_since in pairs(inactive_sessions) do if session.bosh_max_inactive then if now - inactive_since > session.bosh_max_inactive then (session.log or log)("debug", "BOSH client inactive too long, destroying session at %d", now); sessions[session.sid] = nil; inactive_sessions[session] = nil; - sm_destroy_session(session, "BOSH client silent for over "..session.bosh_max_inactive.." seconds"); + n_dead_sessions = n_dead_sessions + 1; + dead_sessions[n_dead_sessions] = session; end else inactive_sessions[session] = nil; end end + + for i=1,n_dead_sessions do + local session = dead_sessions[i]; + dead_sessions[i] = nil; + sm_destroy_session(session, "BOSH client silent for over "..session.bosh_max_inactive.." seconds"); + end end -local ports = config.get(module.host, "core", "bosh_ports") or { 5280 }; -httpserver.new_from_config(ports, "http-bind", handle_request); +local ports = module:get_option("bosh_ports") or { 5280 }; +httpserver.new_from_config(ports, handle_request, { base = "http-bind" }); server.addtimer(on_timer); diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua new file mode 100644 index 00000000..f1cae737 --- /dev/null +++ b/plugins/mod_compression.lua @@ -0,0 +1,122 @@ +-- Prosody IM +-- Copyright (C) 2009 Tobias Markmann +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local st = require "util.stanza"; +local zlib = require "zlib"; +local pcall = pcall; + +local xmlns_compression_feature = "http://jabber.org/features/compress" +local xmlns_compression_protocol = "http://jabber.org/protocol/compress" +local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up(); + +local compression_level = module:get_option("compression_level"); + +-- if not defined assume admin wants best compression +if compression_level == nil then compression_level = 9 end; + +compression_level = tonumber(compression_level); +if not compression_level or compression_level < 1 or compression_level > 9 then + module:log("warn", "Invalid compression level in config: %s", tostring(compression_level)); + module:log("warn", "Module loading aborted. Compression won't be available."); + return; +end + +module:add_event_hook("stream-features", + function (session, features) + if not session.compressed then + -- FIXME only advertise compression support when TLS layer has no compression enabled + features:add_child(compression_stream_feature); + end + end +); + +-- TODO Support compression on S2S level too. +module:add_handler({"c2s_unauthed", "c2s"}, "compress", xmlns_compression_protocol, + function(session, stanza) + -- fail if we are already compressed + if session.compressed then + local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method"); + session.send(error_st); + session:log("warn", "Tried to establish another compression layer."); + end + + -- checking if the compression method is supported + local method = stanza:child_with_name("method")[1]; + if method == "zlib" then + session.log("info", method.." compression selected."); + session.send(st.stanza("compressed", {xmlns=xmlns_compression_protocol})); + session:reset_stream(); + + -- create deflate and inflate streams + local status, deflate_stream = pcall(zlib.deflate, compression_level); + if status == false then + local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); + session.send(error_st); + session:log("error", "Failed to create zlib.deflate filter."); + module:log("error", deflate_stream); + return + end + + local status, inflate_stream = pcall(zlib.inflate); + if status == false then + local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); + session.send(error_st); + session:log("error", "Failed to create zlib.deflate filter."); + module:log("error", inflate_stream); + return + end + + -- setup compression for session.w + local old_send = session.send; + + session.send = function(t) + local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync'); + if status == false then + session:close({ + condition = "undefined-condition"; + text = compressed; + extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); + }); + module:log("warn", compressed); + return; + end + old_send(compressed); + end; + + -- setup decompression for session.data + local function setup_decompression(session) + local old_data = session.data + session.data = function(conn, data) + local status, decompressed, eof = pcall(inflate_stream, data); + if status == false then + session:close({ + condition = "undefined-condition"; + text = decompressed; + extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); + }); + module:log("warn", decompressed); + return; + end + old_data(conn, decompressed); + end; + end + setup_decompression(session); + + local session_reset_stream = session.reset_stream; + session.reset_stream = function(session) + session_reset_stream(session); + setup_decompression(session); + return true; + end; + session.compressed = true; + else + session.log("info", method.." compression selected. But we don't support it."); + local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method"); + session.send(error_st); + end + end +); diff --git a/plugins/mod_console.lua b/plugins/mod_console.lua index 367c46b8..5a092298 100644 --- a/plugins/mod_console.lua +++ b/plugins/mod_console.lua @@ -127,7 +127,11 @@ function console_listener.listener(conn, data) end function console_listener.disconnect(conn, err) - + local session = sessions[conn]; + if session then + session.disconnect(); + sessions[conn] = nil; + end end connlisteners_register('console', console_listener); @@ -170,6 +174,7 @@ function commands.help(session, data) print [[s2s - Commands to manage sessions between this server and others]] print [[module - Commands to load/reload/unload modules/plugins]] print [[server - Uptime, version, shutting down, etc.]] + print [[config - Reloading the configuration, etc.]] print [[console - Help regarding the console itself]] elseif section == "c2s" then print [[c2s:show(jid) - Show all client sessions with the specified JID (or all if no JID given)]] @@ -183,10 +188,13 @@ function commands.help(session, data) print [[module:load(module, host) - Load the specified module on the specified host (or all hosts if none given)]] print [[module:reload(module, host) - The same, but unloads and loads the module (saving state if the module supports it)]] print [[module:unload(module, host) - The same, but just unloads the module from memory]] + print [[module:list(host) - List the modules loaded on the specified host]] elseif section == "server" then print [[server:version() - Show the server's version number]] print [[server:uptime() - Show how long the server has been running]] --print [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]] + elseif section == "config" then + print [[config:reload() - Reload the server configuration. Modules may need to be reloaded for changes to take effect.]] elseif section == "console" then print [[Hey! Welcome to Prosody's admin console.]] print [[First thing, if you're ever wondering how to get out, simply type 'quit'.]] @@ -327,6 +335,35 @@ function def_env.module:reload(name, hosts) return ok, (ok and "Module reloaded on "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); end +function def_env.module:list(hosts) + if hosts == nil then + hosts = array.collect(keys(prosody.hosts)); + end + if type(hosts) == "string" then + hosts = { hosts }; + end + if type(hosts) ~= "table" then + return false, "Please supply a host or a list of hosts you would like to see"; + end + + local print = self.session.print; + for _, host in ipairs(hosts) do + print(host..":"); + local modules = array.collect(keys(prosody.hosts[host] and prosody.hosts[host].modules or {})):sort(); + if #modules == 0 then + if prosody.hosts[host] then + print(" No modules loaded"); + else + print(" Host not found"); + end + else + for _, name in ipairs(modules) do + print(" "..name); + end + end + end +end + def_env.config = {}; function def_env.config:load(filename, format) local config_load = require "core.configmanager".load; @@ -373,7 +410,12 @@ end function def_env.c2s:show(match_jid) local print, count = self.session.print, 0; + local curr_host; show_c2s(function (jid, session) + if curr_host ~= session.host then + curr_host = session.host; + print(curr_host); + end if (not match_jid) or jid:match(match_jid) then count = count + 1; local status, priority = "unavailable", tostring(session.priority or "-"); @@ -385,7 +427,7 @@ function def_env.c2s:show(match_jid) status = "available"; end end - print(jid.." - "..status.."("..priority..")"); + print(" "..jid.." - "..status.."("..priority..")"); end end); return true, "Total: "..count.." clients"; @@ -436,7 +478,7 @@ function def_env.s2s:show(match_jid) for remotehost, session in pairs(host_session.s2sout) do if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then count_out = count_out + 1; - print(" "..host.." -> "..remotehost); + print(" "..host.." -> "..remotehost..(session.secure and " (encrypted)" or "")); if session.sendq then print(" There are "..#session.sendq.." queued outgoing stanzas for this connection"); end @@ -464,12 +506,16 @@ function def_env.s2s:show(match_jid) end end end - + local subhost_filter = function (h) + return (match_jid and h:match(match_jid)); + end for session in pairs(incoming_s2s) do if session.to_host == host and ((not match_jid) or host:match(match_jid) - or (session.from_host and session.from_host:match(match_jid))) then + or (session.from_host and session.from_host:match(match_jid)) + -- Pft! is what I say to list comprehensions + or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then count_in = count_in + 1; - print(" "..host.." <- "..(session.from_host or "(unknown)")); + print(" "..host.." <- "..(session.from_host or "(unknown)")..(session.secure and " (encrypted)" or "")); if session.type == "s2sin_unauthed" then print(" Connection not yet authenticated"); end @@ -510,7 +556,7 @@ function def_env.s2s:close(from, to) if not session then print("No outgoing connection from "..from.." to "..to) else - s2smanager.destroy_session(session); + (session.close or s2smanager.destroy_session)(session); count = count + 1; print("Closed outgoing session from "..from.." to "..to); end @@ -518,7 +564,7 @@ function def_env.s2s:close(from, to) -- Is an incoming connection for session in pairs(incoming_s2s) do if session.to_host == to and session.from_host == from then - s2smanager.destroy_session(session); + (session.close or s2smanager.destroy_session)(session); count = count + 1; end end @@ -537,6 +583,44 @@ function def_env.s2s:close(from, to) return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end +def_env.host = {}; def_env.hosts = def_env.host; +function def_env.host:activate(hostname, config) + local hostmanager_activate = require "core.hostmanager".activate; + if hosts[hostname] then + return false, "The host "..tostring(hostname).." is already activated"; + end + + local defined_hosts = config or configmanager.getconfig(); + if not config and not defined_hosts[hostname] then + return false, "Couldn't find "..tostring(hostname).." defined in the config, perhaps you need to config:reload()?"; + end + hostmanager_activate(hostname, config or defined_hosts[hostname]); + return true, "Host "..tostring(hostname).." activated"; +end + +function def_env.host:deactivate(hostname, reason) + local hostmanager_deactivate = require "core.hostmanager".deactivate; + local host = hosts[hostname]; + if not host then + return false, "The host "..tostring(hostname).." is not activated"; + end + if reason then + reason = { condition = "host-gone", text = reason }; + end + hostmanager_deactivate(hostname, reason); + return true, "Host "..tostring(hostname).." deactivated"; +end + +function def_env.host:list() + local print = self.session.print; + local i = 0; + for host in values(array.collect(keys(prosody.hosts)):sort()) do + i = i + 1; + print(host); + end + return true, i.." hosts"; +end + ------------- function printbanner(session) diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua index 5c956103..469044cd 100644 --- a/plugins/mod_dialback.lua +++ b/plugins/mod_dialback.lua @@ -10,6 +10,7 @@ local hosts = _G.hosts; local send_s2s = require "core.s2smanager".send_to_host; local s2s_make_authenticated = require "core.s2smanager".make_authenticated; +local s2s_initiate_dialback = require "core.s2smanager".initiate_dialback; local s2s_verify_dialback = require "core.s2smanager".verify_dialback; local s2s_destroy_session = require "core.s2smanager".destroy_session; @@ -17,6 +18,7 @@ local log = module._log; local st = require "util.stanza"; +local xmlns_stream = "http://etherx.jabber.org/streams"; local xmlns_dialback = "jabber:server:dialback"; local dialback_requests = setmetatable({}, { __mode = 'v' }); @@ -113,3 +115,13 @@ module:add_handler({ "s2sout_unauthed", "s2sout" }, "result", xmlns_dialback, s2s_destroy_session(origin) end end); + +module:hook_stanza(xmlns_stream, "features", function (origin, stanza) + s2s_initiate_dialback(origin); + return true; + end, 100); + +-- Offer dialback to incoming hosts +module:hook("s2s-stream-features", function (data) + data.features:tag("dialback", { xmlns='urn:xmpp:features:dialback' }):tag("optional"):up():up(); + end); diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua index 00ea01d8..06b29f0e 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -6,16 +6,47 @@ -- COPYING file in the source package for more information. -- +local componentmanager_get_children = require "core.componentmanager".get_children; +local st = require "util.stanza" - -local discomanager_handle = require "core.discomanager".handle; - +module:add_identity("server", "im", "Prosody"); -- FIXME should be in the non-existing mod_router module:add_feature("http://jabber.org/protocol/disco#info"); module:add_feature("http://jabber.org/protocol/disco#items"); -module:add_iq_handler({"c2s", "s2sin"}, "http://jabber.org/protocol/disco#info", function (session, stanza) - session.send(discomanager_handle(stanza)); +module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type ~= "get" then return; end + local node = stanza.tags[1].attr.node; + if node and node ~= "" then return; end -- TODO fire event? + + local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info"); + local done = {}; + for _,identity in ipairs(module:get_host_items("identity")) do + local identity_s = identity.category.."\0"..identity.type; + if not done[identity_s] then + reply:tag("identity", identity):up(); + done[identity_s] = true; + end + end + for _,feature in ipairs(module:get_host_items("feature")) do + if not done[feature] then + reply:tag("feature", {var=feature}):up(); + done[feature] = true; + end + end + origin.send(reply); + return true; end); -module:add_iq_handler({"c2s", "s2sin"}, "http://jabber.org/protocol/disco#items", function (session, stanza) - session.send(discomanager_handle(stanza)); +module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type ~= "get" then return; end + local node = stanza.tags[1].attr.node; + if node and node ~= "" then return; end -- TODO fire event? + + local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); + for jid in pairs(componentmanager_get_children(module.host)) do + reply:tag("item", {jid = jid}):up(); + end + origin.send(reply); + return true; end); diff --git a/plugins/mod_httpserver.lua b/plugins/mod_httpserver.lua index a8639281..545d4faf 100644 --- a/plugins/mod_httpserver.lua +++ b/plugins/mod_httpserver.lua @@ -12,20 +12,50 @@ local httpserver = require "net.httpserver"; local open = io.open; local t_concat = table.concat; -local http_base = "www_files"; +local http_base = config.get("*", "core", "http_path") or "www_files"; +local response_400 = { status = "400 Bad Request", body = "<h1>Bad Request</h1>Sorry, we didn't understand your request :(" }; local response_404 = { status = "404 Not Found", body = "<h1>Page Not Found</h1>Sorry, we couldn't find what you were looking for :(" }; -local http_path = { http_base }; -local function handle_request(method, body, request) - local path = request.url.path:gsub("%.%.%/", ""):gsub("^/[^/]+", ""); - http_path[2] = path; - local f, err = open(t_concat(http_path), "r"); +local function preprocess_path(path) + if path:sub(1,1) ~= "/" then + path = "/"..path; + end + local level = 0; + for component in path:gmatch("([^/]+)/") do + if component == ".." then + level = level - 1; + elseif component ~= "." then + level = level + 1; + end + if level < 0 then + return nil; + end + end + return path; +end + +function serve_file(path) + local f, err = open(http_base..path, "r"); if not f then return response_404; end local data = f:read("*a"); f:close(); return data; end +local function handle_file_request(method, body, request) + local path = preprocess_path(request.url.path); + if not path then return response_400; end + path = path:gsub("^/[^/]+", ""); -- Strip /files/ + return serve_file(path); +end + +local function handle_default_request(method, body, request) + local path = preprocess_path(request.url.path); + if not path then return response_400; end + return serve_file(path); +end + local ports = config.get(module.host, "core", "http_ports") or { 5280 }; -httpserver.new_from_config(ports, "files", handle_request); +httpserver.set_default_handler(handle_default_request); +httpserver.new_from_config(ports, handle_file_request, { base = "files" }); diff --git a/plugins/mod_lastactivity.lua b/plugins/mod_lastactivity.lua new file mode 100644 index 00000000..a0da9829 --- /dev/null +++ b/plugins/mod_lastactivity.lua @@ -0,0 +1,52 @@ +-- 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 st = require "util.stanza"; +local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; +local jid_bare = require "util.jid".bare; +local jid_split = require "util.jid".split; + +module:add_feature("jabber:iq:last"); + +local map = {}; + +module:hook("pre-presence/bare", function(event) + local stanza = event.stanza; + if not(stanza.attr.to) and stanza.attr.type == "unavailable" then + local t = os.time(); + local s = stanza:child_with_name("status"); + s = s and #s.tags == 0 and s[1] or ""; + map[event.origin.username] = {s = s, t = t}; + end +end, 10); + +module:hook("iq/bare/jabber:iq:last:query", function(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "get" then + local username = jid_split(stanza.attr.to) or origin.username; + if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then + local seconds, text = "0", ""; + if map[username] then + seconds = tostring(os.difftime(os.time(), map[username].t)); + text = map[username].s; + end + origin.send(st.reply(stanza):tag('query', {xmlns='jabber:iq:last', seconds=seconds}):text(text)); + else + origin.send(st.error_reply(stanza, 'auth', 'forbidden')); + end + return true; + end +end); + +module.save = function() + return {map = map}; +end +module.restore = function(data) + map = data.map or {}; +end + diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index de94411e..c678dce1 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -11,11 +11,12 @@ local st = require "util.stanza"; local t_concat = table.concat; -local config = require "core.configmanager"; -local secure_auth_only = config.get(module:get_host(), "core", "require_encryption"); +local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption"); local sessionmanager = require "core.sessionmanager"; local usermanager = require "core.usermanager"; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local resourceprep = require "util.encodings".stringprep.resourceprep; module:add_feature("jabber:iq:auth"); module:add_event_hook("stream-features", function (session, features) @@ -43,11 +44,11 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:auth", :tag("username"):up() :tag("password"):up() :tag("resource"):up()); - return true; else username, password, resource = t_concat(username), t_concat(password), t_concat(resource); + username = nodeprep(username); + resource = resourceprep(resource) local reply = st.reply(stanza); - require "core.usermanager" if usermanager.validate_credentials(session.host, username, password) then -- Authentication successful! local success, err = sessionmanager.make_authenticated(session, username); @@ -56,19 +57,18 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:auth", success, err_type, err, err_msg = sessionmanager.bind_resource(session, resource); if not success then session.send(st.error_reply(stanza, err_type, err, err_msg)); + session.username, session.type = nil, "c2s_unauthed"; -- FIXME should this be placed in sessionmanager? + return true; + elseif resource ~= session.resource then -- server changed resource, not supported by legacy auth + session.send(st.error_reply(stanza, "cancel", "conflict", "The requested resource could not be assigned to this session.")); + session:close(); -- FIXME undo resource bind and auth instead of closing the session? return true; end end session.send(st.reply(stanza)); - return true; else - local reply = st.reply(stanza); - reply.attr.type = "error"; - reply:tag("error", { code = "401", type = "auth" }) - :tag("not-authorized", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" }); - session.send(reply); - return true; + session.send(st.error_reply(stanza, "auth", "not-authorized")); end end - + return true; end); diff --git a/plugins/mod_muc.lua b/plugins/mod_muc.lua deleted file mode 100644 index b38468ea..00000000 --- a/plugins/mod_muc.lua +++ /dev/null @@ -1,90 +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. --- - - -if module:get_host_type() ~= "component" then - error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0); -end - -local muc_host = module:get_host(); -local muc_name = "Chatrooms"; -local history_length = 20; - -local muc_new_room = require "util.muc".new_room; -local register_component = require "core.componentmanager".register_component; -local deregister_component = require "core.componentmanager".deregister_component; -local jid_split = require "util.jid".split; -local st = require "util.stanza"; - -local rooms = {}; -local component; -local host_room = muc_new_room(muc_host); -host_room.route_stanza = function(room, stanza) core_post_stanza(component, stanza); end; - -local function get_disco_info(stanza) - return st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info") - :tag("identity", {category='conference', type='text', name=muc_name}):up() - :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply -end -local function get_disco_items(stanza) - local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); - for jid, room in pairs(rooms) do - reply:tag("item", {jid=jid, name=jid}):up(); - end - return reply; -- TODO cache disco reply -end - -local function handle_to_domain(origin, stanza) - local type = stanza.attr.type; - if type == "error" or type == "result" then return; end - if stanza.name == "iq" and type == "get" then - local xmlns = stanza.tags[1].attr.xmlns; - if xmlns == "http://jabber.org/protocol/disco#info" then - origin.send(get_disco_info(stanza)); - elseif xmlns == "http://jabber.org/protocol/disco#items" then - origin.send(get_disco_items(stanza)); - else - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc - end - else - host_room:handle_stanza(origin, stanza); - --origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it")); - end -end - -component = register_component(muc_host, function(origin, stanza) - local to_node, to_host, to_resource = jid_split(stanza.attr.to); - if to_node then - local bare = to_node.."@"..to_host; - if to_host == muc_host or bare == muc_host then - local room = rooms[bare]; - if not room then - room = muc_new_room(bare); - room.route_stanza = function(room, stanza) core_post_stanza(component, stanza); end; - rooms[bare] = room; - end - room:handle_stanza(origin, stanza); - else --[[not for us?]] end - return; - end - -- to the main muc domain - handle_to_domain(origin, stanza); -end); - -prosody.hosts[module:get_host()].muc = { rooms = rooms }; - -module.unload = function() - deregister_component(muc_host); -end -module.save = function() - return {rooms = rooms}; -end -module.restore = function(data) - rooms = data.rooms or {}; - prosody.hosts[module:get_host()].muc = { rooms = rooms }; -end diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua index c9acf9e6..c74d011e 100644 --- a/plugins/mod_offline.lua +++ b/plugins/mod_offline.lua @@ -10,7 +10,8 @@ local datamanager = require "util.datamanager";
local st = require "util.stanza";
local datetime = require "util.datetime";
-local ipairs = ipairs;
+local ipairs = ipairs; +local jid_split = require "util.jid".split;
module:add_feature("msgoffline");
diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index e07759f0..bfe22867 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -25,10 +25,20 @@ local data = {}; local recipients = {}; local hash_map = {}; -module:add_identity("pubsub", "pep"); +module.save = function() + return { data = data, recipients = recipients, hash_map = hash_map }; +end +module.restore = function(state) + data = state.data or {}; + recipients = state.recipients or {}; + hash_map = state.hash_map or {}; +end + +module:add_identity("pubsub", "pep", "Prosody"); module:add_feature("http://jabber.org/protocol/pubsub#publish"); local function publish(session, node, item) + item.attr.xmlns = nil; local disable = #item.tags ~= 1 or #item.tags[1].tags == 0; if #item.tags == 0 then item.name = "retract"; end local bare = session.username..'@'..session.host; @@ -132,9 +142,9 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) if payload and (payload.name == 'publish' or payload.name == 'retract') and payload.attr.node then -- <publish node='http://jabber.org/protocol/tune'> local node = payload.attr.node; payload = payload.tags[1]; - if payload then -- <item> - publish(session, node, payload); + if payload and payload.name == "item" then -- <item> session.send(st.reply(stanza)); + publish(session, node, st.clone(payload)); return true; end end diff --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua index e0324c80..1dc9fbec 100644 --- a/plugins/mod_ping.lua +++ b/plugins/mod_ping.lua @@ -6,15 +6,16 @@ -- COPYING file in the source package for more information. -- - - local st = require "util.stanza"; module:add_feature("urn:xmpp:ping"); -module:add_iq_handler({"c2s", "s2sin"}, "urn:xmpp:ping", - function(session, stanza) - if stanza.attr.type == "get" then - session.send(st.reply(stanza)); - end - end); +local function ping_handler(event) + if event.stanza.attr.type == "get" then + event.origin.send(st.reply(event.stanza)); + return true; + end +end + +module:hook("iq/bare/urn:xmpp:ping:ping", ping_handler); +module:hook("iq/host/urn:xmpp:ping:ping", ping_handler); diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua index 0f46888d..b75b9610 100644 --- a/plugins/mod_posix.lua +++ b/plugins/mod_posix.lua @@ -17,19 +17,45 @@ if type(signal) == "string" then module:log("warn", "Couldn't load signal library, won't respond to SIGTERM"); end -local config_get = require "core.configmanager".get; local logger_set = require "util.logger".setwriter; local prosody = _G.prosody; module.host = "*"; -- we're a global module +-- Allow switching away from root, some people like strange ports. +module:add_event_hook("server-started", function () + local uid = module:get_option("setuid"); + local gid = module:get_option("setgid"); + if gid then + local success, msg = pposix.setgid(gid); + if success then + module:log("debug", "Changed group to "..gid.." successfully."); + else + module:log("error", "Failed to change group to "..gid..". Error: "..msg); + prosody.shutdown("Failed to change group to "..gid); + end + end + if uid then + local success, msg = pposix.setuid(uid); + if success then + module:log("debug", "Changed user to "..uid.." successfully."); + else + module:log("error", "Failed to change user to "..uid..". Error: "..msg); + prosody.shutdown("Failed to change user to "..uid); + end + end + end); + -- Don't even think about it! module:add_event_hook("server-starting", function () - if pposix.getuid() == 0 and not config_get("*", "core", "run_as_root") then - module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!"); - module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root"); - prosody.shutdown("Refusing to run as root"); + local suid = module:get_option("setuid"); + if not suid or suid == 0 or suid == "root" then + if pposix.getuid() == 0 and not module:get_option("run_as_root") then + module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!"); + module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root"); + prosody.shutdown("Refusing to run as root"); + end end end); @@ -46,7 +72,7 @@ local function write_pidfile() if pidfile_written then remove_pidfile(); end - local pidfile = config_get("*", "core", "pidfile"); + local pidfile = module:get_option("pidfile"); if pidfile then local pf, err = io.open(pidfile, "w+"); if not pf then @@ -76,7 +102,17 @@ function syslog_sink_maker(config) end require "core.loggingmanager".register_sink_type("syslog", syslog_sink_maker); -if not config_get("*", "core", "no_daemonize") then +local daemonize = module:get_option("daemonize"); +if daemonize == nil then + local no_daemonize = module:get_option("no_daemonize"); --COMPAT w/ 0.5 + daemonize = not no_daemonize; + if no_daemonize ~= nil then + module:log("warn", "The 'no_daemonize' option is now replaced by 'daemonize'"); + module:log("warn", "Update your config from 'no_daemonize = %s' to 'daemonize = %s'", tostring(no_daemonize), tostring(daemonize)); + end +end + +if daemonize then local function daemonize_server() local ok, ret = pposix.daemonize(); if not ok then diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua index 02ec6f79..f83e017b 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -29,7 +29,7 @@ function core_route_stanza(origin, stanza) if stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" and stanza.attr.type ~= "error" then local node, host = jid_split(stanza.attr.to); host = hosts[host]; - if host and host.type == "local" then + if node and host and host.type == "local" then handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); return; end @@ -142,7 +142,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza) stanza.attr.to = nil; -- reset it end -function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza) +function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza, stanza) local h = hosts[host]; local count = 0; if h and h.type == "local" then @@ -151,6 +151,7 @@ function send_presence_of_available_resources(user, host, jid, recipient_session for k, session in pairs(u.sessions) do local pres = session.presence; if pres then + if stanza then pres = stanza; pres.attr.from = session.full_jid; end pres.attr.to = jid; core_route_stanza(session, pres); pres.attr.to = nil; @@ -165,7 +166,7 @@ end function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza) local node, host = jid_split(from_bare); - if node == origin.username and host == origin.host then return; end -- No self contacts + if to_bare == origin.username.."@"..origin.host then return; end -- No self contacts local st_from, st_to = stanza.attr.from, stanza.attr.to; stanza.attr.from, stanza.attr.to = from_bare, to_bare; log("debug", "outbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare); @@ -199,6 +200,9 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_ rostermanager.roster_push(node, host, to_bare); end core_route_stanza(origin, stanza); + -- COMPAT: Some legacy clients keep displaying unsubscribed contacts as online unless an unavailable presence is sent: + send_presence_of_available_resources(node, host, to_bare, origin, core_route_stanza, + st.presence({ type="unavailable", from=from_bare, to=to_bare, id=stanza.attr.id })); end stanza.attr.from, stanza.attr.to = st_from, st_to; end diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua index 383ab811..22724130 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -9,7 +9,6 @@ local hosts = _G.hosts; local st = require "util.stanza"; -local config = require "core.configmanager"; local datamanager = require "util.datamanager"; local usermanager_user_exists = require "core.usermanager".user_exists; local usermanager_create_user = require "core.usermanager".create_user; @@ -90,16 +89,16 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza) end); local recent_ips = {}; -local min_seconds_between_registrations = config.get(module.host, "core", "min_seconds_between_registrations"); -local whitelist_only = config.get(module.host, "core", "whitelist_registration_only"); -local whitelisted_ips = config.get(module.host, "core", "registration_whitelist") or { "127.0.0.1" }; -local blacklisted_ips = config.get(module.host, "core", "registration_blacklist") or {}; +local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations"); +local whitelist_only = module:get_option("whitelist_registration_only"); +local whitelisted_ips = module:get_option("registration_whitelist") or { "127.0.0.1" }; +local blacklisted_ips = module:get_option("registration_blacklist") or {}; for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, stanza) - if config.get(module.host, "core", "allow_registration") == false then + if module:get_option("allow_registration") == false then session.send(st.error_reply(stanza, "cancel", "service-unavailable")); elseif stanza.tags[1].name == "query" then local query = stanza.tags[1]; @@ -119,19 +118,18 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s if username and password then -- Check that the user is not blacklisted or registering too often if blacklisted_ips[session.ip] or (whitelist_only and not whitelisted_ips[session.ip]) then - session.send(st.error_reply(stanza, "cancel", "not-acceptable")); - return; + session.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not allowed to register an account.")); + return; elseif min_seconds_between_registrations and not whitelisted_ips[session.ip] then if not recent_ips[session.ip] then recent_ips[session.ip] = { time = os_time(), count = 1 }; else - local ip = recent_ips[session.ip]; ip.count = ip.count + 1; if os_time() - ip.time < min_seconds_between_registrations then ip.time = os_time(); - session.send(st.error_reply(stanza, "cancel", "not-acceptable")); + session.send(st.error_reply(stanza, "wait", "not-acceptable")); return; end ip.time = os_time(); @@ -140,18 +138,21 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s -- FIXME shouldn't use table.concat username = nodeprep(table.concat(username)); password = table.concat(password); - if usermanager_user_exists(username, session.host) then - session.send(st.error_reply(stanza, "cancel", "conflict")); + local host = module.host; + if not username then + session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is invalid.")); + elseif usermanager_user_exists(username, host) then + session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists.")); else - if usermanager_create_user(username, password, session.host) then + if usermanager_create_user(username, password, host) then session.send(st.reply(stanza)); -- user created! - module:log("info", "User account created: %s@%s", username, session.host); + module:log("info", "User account created: %s@%s", username, host); module:fire_event("user-registered", { - username = username, host = session.host, source = "mod_register", + username = username, host = host, source = "mod_register", session = session }); else -- TODO unable to write file, file may be locked, etc, what's the correct error? - session.send(st.error_reply(stanza, "wait", "internal-server-error")); + session.send(st.error_reply(stanza, "wait", "internal-server-error", "Failed to write data to disk.")); end end else diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua index 8f25ed64..7ca22aa1 100644 --- a/plugins/mod_roster.lua +++ b/plugins/mod_roster.lua @@ -24,7 +24,7 @@ module:add_feature("jabber:iq:roster"); local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"}):tag("optional"):up(); module:add_event_hook("stream-features", - function (session, features) + function (session, features) if session.username then features:add_child(rosterver_stream_feature); end diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index ec3857b8..641b08f0 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -13,6 +13,7 @@ local sm_bind_resource = require "core.sessionmanager".bind_resource; local sm_make_authenticated = require "core.sessionmanager".make_authenticated; local base64 = require "util.encodings".base64; +local nodeprep = require "util.encodings".stringprep.nodeprep; local datamanager_load = require "util.datamanager".load; local usermanager_validate_credentials = require "core.usermanager".validate_credentials; local usermanager_get_supported_methods = require "core.usermanager".get_supported_methods; @@ -24,7 +25,7 @@ local jid_split = require "util.jid".split local md5 = require "util.hashes".md5; local config = require "core.configmanager"; -local secure_auth_only = config.get(module:get_host(), "core", "require_encryption"); +local secure_auth_only = config.get(module:get_host(), "core", "c2s_require_encryption") or config.get(module:get_host(), "core", "require_encryption"); local log = module._log; @@ -36,10 +37,25 @@ local new_sasl = require "util.sasl".new; default_authentication_profile = { plain = function(username, realm) - return usermanager_get_password(username, realm), true; + local prepped_username = nodeprep(username); + if not prepped_username then + log("debug", "NODEprep failed on username: %s", username); + return "", nil; + end + local password = usermanager_get_password(prepped_username, realm); + if not password then + return "", nil; + end + return password, true; end }; +anonymous_authentication_profile = { + anonymous = function(username, realm) + return true; -- for normal usage you should always return true here + end +} + local function build_reply(status, ret, err_msg) local reply = st.stanza(status, {xmlns = xmlns_sasl}); if status == "challenge" then @@ -61,7 +77,8 @@ local function handle_status(session, status) if status == "failure" then session.sasl_handler = nil; elseif status == "success" then - if not session.sasl_handler.username then -- TODO move this to sessionmanager + local username = nodeprep(session.sasl_handler.username); + if not username then -- TODO move this to sessionmanager module:log("warn", "SASL succeeded but we didn't get a username!"); session.sasl_handler = nil; session:reset_stream(); @@ -73,30 +90,6 @@ local function handle_status(session, status) end end -local function credentials_callback(mechanism, ...) - if mechanism == "PLAIN" then - local username, hostname, password = ...; - local response = usermanager_validate_credentials(hostname, username, password, mechanism); - if response == nil then - return false; - else - return response; - end - elseif mechanism == "DIGEST-MD5" then - function func(x) return x; end - local node, domain, realm, decoder = ...; - local password = usermanager_get_password(node, domain); - if password then - if decoder then - node, realm, password = decoder(node), decoder(realm), decoder(password); - end - return func, md5(node..":"..realm..":"..password); - else - return func, nil; - end - end -end - local function sasl_handler(session, stanza) if stanza.name == "auth" then -- FIXME ignoring duplicates because ejabberd does @@ -144,20 +137,20 @@ module:add_event_hook("stream-features", if secure_auth_only and not session.secure then return; end - session.sasl_handler = new_sasl(session.host, default_authentication_profile); + if config.get(session.host or "*", "core", "anonymous_login") then + session.sasl_handler = new_sasl(session.host, anonymous_authentication_profile); + else + session.sasl_handler = new_sasl(session.host, default_authentication_profile); + end features:tag("mechanisms", mechanisms_attr); -- TODO: Provide PLAIN only if TLS is active, this is a SHOULD from the introduction of RFC 4616. This behavior could be overridden via configuration but will issuing a warning or so. - if config.get(session.host or "*", "core", "anonymous_login") then - features:tag("mechanism"):text("ANONYMOUS"):up(); - else - for k, v in pairs(session.sasl_handler:mechanisms()) do - features:tag("mechanism"):text(v):up(); - end - end + for k, v in pairs(session.sasl_handler:mechanisms()) do + features:tag("mechanism"):text(v):up(); + end features:up(); else features:tag("bind", bind_attr):tag("required"):up():up(); - features:tag("session", xmpp_session_attr):up(); + features:tag("session", xmpp_session_attr):tag("optional"):up():up(); end end); diff --git a/plugins/mod_selftests.lua b/plugins/mod_selftests.lua index 6a26dfc3..1f413634 100644 --- a/plugins/mod_selftests.lua +++ b/plugins/mod_selftests.lua @@ -6,14 +6,13 @@ -- COPYING file in the source package for more information. -- - +module.host = "*" -- Global module local st = require "util.stanza"; local register_component = require "core.componentmanager".register_component; local core_route_stanza = core_route_stanza; local socket = require "socket"; -local config = require "core.configmanager"; -local ping_hosts = config.get("*", "mod_selftests", "ping_hosts") or { "coversant.interop.xmpp.org", "djabberd.interop.xmpp.org", "djabberd-trunk.interop.xmpp.org", "ejabberd.interop.xmpp.org", "openfire.interop.xmpp.org" }; +local ping_hosts = module:get_option("ping_hosts") or { "coversant.interop.xmpp.org", "djabberd.interop.xmpp.org", "djabberd-trunk.interop.xmpp.org", "ejabberd.interop.xmpp.org", "openfire.interop.xmpp.org" }; local open_pings = {}; diff --git a/plugins/mod_time.lua b/plugins/mod_time.lua index 26088396..7d900ae9 100644 --- a/plugins/mod_time.lua +++ b/plugins/mod_time.lua @@ -6,8 +6,6 @@ -- COPYING file in the source package for more information. -- - - local st = require "util.stanza"; local datetime = require "util.datetime".datetime; local legacy = require "util.datetime".legacy; @@ -16,23 +14,31 @@ local legacy = require "util.datetime".legacy; module:add_feature("urn:xmpp:time"); -module:add_iq_handler({"c2s", "s2sin"}, "urn:xmpp:time", - function(session, stanza) - if stanza.attr.type == "get" then - session.send(st.reply(stanza):tag("time", {xmlns="urn:xmpp:time"}) - :tag("tzo"):text("+00:00"):up() -- FIXME get the timezone in a platform independent fashion - :tag("utc"):text(datetime())); - end - end); +local function time_handler(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "get" then + origin.send(st.reply(stanza):tag("time", {xmlns="urn:xmpp:time"}) + :tag("tzo"):text("+00:00"):up() -- TODO get the timezone in a platform independent fashion + :tag("utc"):text(datetime())); + return true; + end +end + +module:hook("iq/bare/urn:xmpp:time:time", time_handler); +module:hook("iq/host/urn:xmpp:time:time", time_handler); -- XEP-0090: Entity Time (deprecated) module:add_feature("jabber:iq:time"); -module:add_iq_handler({"c2s", "s2sin"}, "jabber:iq:time", - function(session, stanza) - if stanza.attr.type == "get" then - session.send(st.reply(stanza):tag("query", {xmlns="jabber:iq:time"}) - :tag("utc"):text(legacy())); - end - end); +local function legacy_time_handler(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "get" then + origin.send(st.reply(stanza):tag("query", {xmlns="jabber:iq:time"}) + :tag("utc"):text(legacy())); + return true; + end +end + +module:hook("iq/bare/jabber:iq:time:query", legacy_time_handler); +module:hook("iq/host/jabber:iq:time:query", legacy_time_handler); diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index 8926edfc..8a450803 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -6,20 +6,22 @@ -- COPYING file in the source package for more information. -- - - local st = require "util.stanza"; -local xmlns_starttls ='urn:ietf:params:xml:ns:xmpp-tls'; +local xmlns_stream = 'http://etherx.jabber.org/streams'; +local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls'; -local config = require "core.configmanager"; -local secure_auth_only = config.get("*", "core", "require_encryption"); +local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption"); +local secure_s2s_only = module:get_option("s2s_require_encryption"); module:add_handler("c2s_unauthed", "starttls", xmlns_starttls, function (session, stanza) if session.conn.starttls then session.send(st.stanza("proceed", { xmlns = xmlns_starttls })); session:reset_stream(); + if session.host and hosts[session.host].ssl_ctx_in then + session.conn.set_sslctx(hosts[session.host].ssl_ctx_in); + end session.conn.starttls(); session.log("info", "TLS negotiation started..."); session.secure = false; @@ -29,9 +31,27 @@ module:add_handler("c2s_unauthed", "starttls", xmlns_starttls, end end); +module:add_handler("s2sin_unauthed", "starttls", xmlns_starttls, + function (session, stanza) + if session.conn.starttls then + session.sends2s(st.stanza("proceed", { xmlns = xmlns_starttls })); + session:reset_stream(); + if session.to_host and hosts[session.to_host].ssl_ctx_in then + session.conn.set_sslctx(hosts[session.to_host].ssl_ctx_in); + end + session.conn.starttls(); + session.log("info", "TLS negotiation started for incoming s2s..."); + session.secure = false; + else + -- FIXME: What reply? + session.log("warn", "Attempt to start TLS, but TLS is not available on this s2s connection"); + end + end); + + local starttls_attr = { xmlns = xmlns_starttls }; module:add_event_hook("stream-features", - function (session, features) + function (session, features) if session.conn.starttls then features:tag("starttls", starttls_attr); if secure_auth_only then @@ -41,3 +61,37 @@ module:add_event_hook("stream-features", end end end); + +module:hook("s2s-stream-features", + function (data) + local session, features = data.session, data.features; + if session.to_host and session.conn.starttls then + features:tag("starttls", starttls_attr):up(); + if secure_s2s_only then + features:tag("required"):up():up(); + else + features:up(); + end + end + end); + +-- For s2sout connections, start TLS if we can +module:hook_stanza(xmlns_stream, "features", + function (session, stanza) + module:log("debug", "Received features element"); + if session.conn.starttls and stanza:child_with_ns(xmlns_starttls) then + module:log("%s is offering TLS, taking up the offer...", session.to_host); + session.sends2s("<starttls xmlns='"..xmlns_starttls.."'/>"); + return true; + end + end, 500); + +module:hook_stanza(xmlns_starttls, "proceed", + function (session, stanza) + module:log("debug", "Proceeding with TLS on s2sout..."); + local format, to_host, from_host = string.format, session.to_host, session.from_host; + session:reset_stream(); + session.conn.starttls(true); + session.secure = false; + return true; + end); diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua index eb0ca7cc..cf6c6b64 100644 --- a/plugins/mod_uptime.lua +++ b/plugins/mod_uptime.lua @@ -6,30 +6,17 @@ -- COPYING file in the source package for more information. -- - - -local st = require "util.stanza" - -local jid_split = require "util.jid".split; -local t_concat = table.concat; +local st = require "util.stanza"; local start_time = prosody.start_time; - -prosody.events.add_handler("server-started", function () start_time = prosody.start_time end); +prosody.events.add_handler("server-started", function() start_time = prosody.start_time end); module:add_feature("jabber:iq:last"); -module:add_iq_handler({"c2s", "s2sin"}, "jabber:iq:last", - function (origin, stanza) - if stanza.tags[1].name == "query" then - if stanza.attr.type == "get" then - local node, host, resource = jid_split(stanza.attr.to); - if node or resource then - -- TODO - else - origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:last", seconds = tostring(os.difftime(os.time(), start_time))})); - return true; - end - end - end - end); +module:hook("iq/host/jabber:iq:last:query", function(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "get" then + origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:last", seconds = tostring(os.difftime(os.time(), start_time))})); + return true; + end +end); diff --git a/plugins/mod_vcard.lua b/plugins/mod_vcard.lua index db67d9e5..0efc1638 100644 --- a/plugins/mod_vcard.lua +++ b/plugins/mod_vcard.lua @@ -6,58 +6,53 @@ -- COPYING file in the source package for more information. -- - - -local hosts = _G.hosts; -local datamanager = require "util.datamanager" - local st = require "util.stanza" -local t_concat, t_insert = table.concat, table.insert; - -local jid = require "util.jid" -local jid_split = jid.split; +local jid_split = require "util.jid".split; +local datamanager = require "util.datamanager" module:add_feature("vcard-temp"); -module:add_iq_handler({"c2s", "s2sin"}, "vcard-temp", - function (session, stanza) - if stanza.tags[1].name == "vCard" then - local to = stanza.attr.to; - if stanza.attr.type == "get" then - local vCard; - if to then - local node, host = jid_split(to); - if hosts[host] and hosts[host].type == "local" then - vCard = st.deserialize(datamanager.load(node, host, "vcard")); -- load vCard for user or server - end - else - vCard = st.deserialize(datamanager.load(session.username, session.host, "vcard"));-- load user's own vCard - end - if vCard then - session.send(st.reply(stanza):add_child(vCard)); -- send vCard! - else - session.send(st.error_reply(stanza, "cancel", "item-not-found")); - end - elseif stanza.attr.type == "set" then - if not to or to == session.username.."@"..session.host then - if datamanager.store(session.username, session.host, "vcard", st.preserialize(stanza.tags[1])) then - session.send(st.reply(stanza)); - else - -- TODO unable to write file, file may be locked, etc, what's the correct error? - session.send(st.error_reply(stanza, "wait", "internal-server-error")); - end - else - session.send(st.error_reply(stanza, "auth", "forbidden")); - end - end - return true; +local function handle_vcard(event) + local session, stanza = event.origin, event.stanza; + local to = stanza.attr.to; + if stanza.attr.type == "get" then + local vCard; + if to then + local node, host = jid_split(to); + vCard = st.deserialize(datamanager.load(node, host, "vcard")); -- load vCard for user or server + else + vCard = st.deserialize(datamanager.load(session.username, session.host, "vcard"));-- load user's own vCard + end + if vCard then + session.send(st.reply(stanza):add_child(vCard)); -- send vCard! + else + session.send(st.error_reply(stanza, "cancel", "item-not-found")); + end + else + if not to then + if datamanager.store(session.username, session.host, "vcard", st.preserialize(stanza.tags[1])) then + session.send(st.reply(stanza)); + else + -- TODO unable to write file, file may be locked, etc, what's the correct error? + session.send(st.error_reply(stanza, "wait", "internal-server-error")); end - end); - -local feature_vcard_attr = { var='vcard-temp' }; -module:add_event_hook("stream-features", - function (session, features) - if session.type == "c2s" then - features:tag("feature", feature_vcard_attr):up(); - end - end); + else + session.send(st.error_reply(stanza, "auth", "forbidden")); + end + end + return true; +end + +module:hook("iq/bare/vcard-temp:vCard", handle_vcard); +module:hook("iq/host/vcard-temp:vCard", handle_vcard); + +-- COMPAT: https://support.process-one.net/browse/EJAB-1045 +if module:get_option("vcard_compatibility") then + module:hook("iq/full", function(data) + local stanza = data.stanza; + local payload = stanza.tags[1]; + if stanza.attr.type == "get" or stanza.attr.type == "set" and payload.name == "vCard" and payload.attr.xmlns == "vcard-temp" then + return handle_vcard(data); + end + end, 1); +end diff --git a/plugins/mod_version.lua b/plugins/mod_version.lua index 87bff5d9..9af830f8 100644 --- a/plugins/mod_version.lua +++ b/plugins/mod_version.lua @@ -6,17 +6,13 @@ -- COPYING file in the source package for more information. -- - -local prosody = prosody; local st = require "util.stanza"; -local xmlns_version = "jabber:iq:version" - -module:add_feature(xmlns_version); +module:add_feature("jabber:iq:version"); local version = "the best operating system ever!"; -if not require "core.configmanager".get("*", "core", "hide_os_type") then +if not module:get_option("hide_os_type") then if os.getenv("WINDIR") then version = "Windows"; else @@ -31,11 +27,15 @@ end version = version:match("^%s*(.-)%s*$") or version; -module:add_iq_handler({"c2s", "s2sin"}, xmlns_version, function(session, stanza) - if stanza.attr.type == "get" then - session.send(st.reply(stanza):query(xmlns_version) - :tag("name"):text("Prosody"):up() - :tag("version"):text(prosody.version):up() - :tag("os"):text(version)); +local query = st.stanza("query", {xmlns = "jabber:iq:version"}) + :tag("name"):text("Prosody"):up() + :tag("version"):text(prosody.version):up() + :tag("os"):text(version); + +module:hook("iq/host/jabber:iq:version:query", function(event) + local stanza = event.stanza; + if stanza.attr.type == "get" and stanza.attr.to == module.host then + event.origin.send(st.reply(stanza):add_child(query)); + return true; end end); diff --git a/plugins/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua index 9457313f..6a2af853 100644 --- a/plugins/mod_watchregistrations.lua +++ b/plugins/mod_watchregistrations.lua @@ -9,12 +9,10 @@ local host = module:get_host(); -local config = require "core.configmanager"; +local registration_watchers = module:get_option("registration_watchers") + or module:get_option("admins") or {}; -local registration_watchers = config.get(host, "core", "registration_watchers") - or config.get(host, "core", "admins") or {}; - -local registration_alert = config.get(host, "core", "registration_notification") or "User $username just registered on $host from $ip"; +local registration_alert = module:get_option("registration_notification") or "User $username just registered on $host from $ip"; local st = require "util.stanza"; diff --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua index 5c0da8b8..edcfbd8c 100644 --- a/plugins/mod_welcome.lua +++ b/plugins/mod_welcome.lua @@ -6,10 +6,8 @@ -- COPYING file in the source package for more information. -- -local config = require "core.configmanager"; - local host = module:get_host(); -local welcome_text = config.get("*", "core", "welcome_message") or "Hello $user, welcome to the $host IM server!"; +local welcome_text = module:get_option("welcome_message") or "Hello $username, welcome to the $host IM server!"; local st = require "util.stanza"; diff --git a/plugins/mod_xmlrpc.lua b/plugins/mod_xmlrpc.lua index 05c0b8b0..7165386a 100644 --- a/plugins/mod_xmlrpc.lua +++ b/plugins/mod_xmlrpc.lua @@ -83,7 +83,7 @@ local function handle_xmlrpc_request(jid, method, args) end return create_error_response(500, "Error in creating response: "..result); end - return create_error_response(0, (result and result:gmatch("[^:]*:[^:]*: (.*)")()) or "nil"); + return create_error_response(0, tostring(result):gsub("^[^:]+:%d+: ", "")); end local function handle_xmpp_request(origin, stanza) diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua new file mode 100644 index 00000000..856f3cba --- /dev/null +++ b/plugins/muc/mod_muc.lua @@ -0,0 +1,164 @@ +-- 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. +-- + + +if module:get_host_type() ~= "component" then + error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0); +end + +local muc_host = module:get_host(); +local muc_name = module:get_option("name"); +if type(muc_name) ~= "string" then muc_name = "Prosody Chatrooms"; end +local restrict_room_creation = module:get_option("restrict_room_creation"); +if restrict_room_creation and restrict_room_creation ~= true then restrict_room_creation = nil; end +local history_length = 20; + +local muc_new_room = module:require "muc".new_room; +local register_component = require "core.componentmanager".register_component; +local deregister_component = require "core.componentmanager".deregister_component; +local jid_split = require "util.jid".split; +local jid_bare = require "util.jid".bare; +local st = require "util.stanza"; +local uuid_gen = require "util.uuid".generate; +local datamanager = require "util.datamanager"; +local um_is_admin = require "core.usermanager".is_admin; + +local rooms = {}; +local persistent_rooms = datamanager.load(nil, muc_host, "persistent") or {}; +local component; + +local function is_admin(jid) + return um_is_admin(jid) or um_is_admin(jid, module.host); +end + +local function room_route_stanza(room, stanza) core_post_stanza(component, stanza); end +local function room_save(room, forced) + local node = jid_split(room.jid); + persistent_rooms[room.jid] = room._data.persistent; + if room._data.persistent then + local history = room._data.history; + room._data.history = nil; + local data = { + jid = room.jid; + _data = room._data; + _affiliations = room._affiliations; + }; + datamanager.store(node, muc_host, "config", data); + room._data.history = history; + elseif forced then + datamanager.store(node, muc_host, "config", nil); + end + if forced then datamanager.store(nil, muc_host, "persistent", persistent_rooms); end +end + +for jid in pairs(persistent_rooms) do + local node = jid_split(jid); + local data = datamanager.load(node, muc_host, "config") or {}; + local room = muc_new_room(jid); + room._data = data._data; + room._affiliations = data._affiliations; + room.route_stanza = room_route_stanza; + room.save = room_save; + rooms[jid] = room; +end + +local host_room = muc_new_room(muc_host); +host_room.route_stanza = room_route_stanza; +host_room.save = room_save; + +local function get_disco_info(stanza) + return st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info") + :tag("identity", {category='conference', type='text', name=muc_name}):up() + :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply +end +local function get_disco_items(stanza) + local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); + for jid, room in pairs(rooms) do + if not room._data.hidden then + reply:tag("item", {jid=jid, name=jid}):up(); + end + end + return reply; -- TODO cache disco reply +end + +local function handle_to_domain(origin, stanza) + local type = stanza.attr.type; + if type == "error" or type == "result" then return; end + if stanza.name == "iq" and type == "get" then + local xmlns = stanza.tags[1].attr.xmlns; + if xmlns == "http://jabber.org/protocol/disco#info" then + origin.send(get_disco_info(stanza)); + elseif xmlns == "http://jabber.org/protocol/disco#items" then + origin.send(get_disco_items(stanza)); + elseif xmlns == "http://jabber.org/protocol/muc#unique" then + origin.send(st.reply(stanza):tag("unique", {xmlns = xmlns}):text(uuid_gen())); -- FIXME Random UUIDs can theoretically have collisions + else + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc + end + else + host_room:handle_stanza(origin, stanza); + --origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it")); + end +end + +component = register_component(muc_host, function(origin, stanza) + local to_node, to_host, to_resource = jid_split(stanza.attr.to); + if to_node then + local bare = to_node.."@"..to_host; + if to_host == muc_host or bare == muc_host then + local room = rooms[bare]; + if not room then + if not(restrict_room_creation) or is_admin(stanza.attr.from) then + room = muc_new_room(bare); + room.route_stanza = room_route_stanza; + room.save = room_save; + rooms[bare] = room; + end + end + if room then + room:handle_stanza(origin, stanza); + if not next(room._occupants) and not persistent_rooms[room.jid] then -- empty, non-persistent room + rooms[bare] = nil; -- discard room + end + else + origin.send(st.error_reply(stanza, "cancel", "not-allowed")); + end + else --[[not for us?]] end + return; + end + -- to the main muc domain + handle_to_domain(origin, stanza); +end); +function component.send(stanza) -- FIXME do a generic fix + if stanza.attr.type == "result" or stanza.attr.type == "error" then + core_post_stanza(component, stanza); + else error("component.send only supports result and error stanzas at the moment"); end +end + +prosody.hosts[module:get_host()].muc = { rooms = rooms }; + +module.unload = function() + deregister_component(muc_host); +end +module.save = function() + return {rooms = rooms}; +end +module.restore = function(data) + rooms = {}; + for jid, oldroom in pairs(data.rooms or {}) do + local room = muc_new_room(jid); + room._jid_nick = oldroom._jid_nick; + room._occupants = oldroom._occupants; + room._data = oldroom._data; + room._affiliations = oldroom._affiliations; + room.route_stanza = room_route_stanza; + room.save = room_save; + rooms[jid] = room; + end + prosody.hosts[module:get_host()].muc = { rooms = rooms }; +end diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua new file mode 100644 index 00000000..3a185e17 --- /dev/null +++ b/plugins/muc/muc.lib.lua @@ -0,0 +1,735 @@ +-- Prosody IM +-- Copyright (C) 2008-2009 Matthew Wild +-- Copyright (C) 2008-2009 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local datamanager = require "util.datamanager"; +local datetime = require "util.datetime"; + +local jid_split = require "util.jid".split; +local jid_bare = require "util.jid".bare; +local jid_prep = require "util.jid".prep; +local st = require "util.stanza"; +local log = require "util.logger".init("mod_muc"); +local multitable_new = require "util.multitable".new; +local t_insert, t_remove = table.insert, table.remove; +local setmetatable = setmetatable; +local base64 = require "util.encodings".base64; +local md5 = require "util.hashes".md5; + +local muc_domain = nil; --module:get_host(); +local history_length = 20; + +------------ +local function filter_xmlns_from_array(array, filters) + local count = 0; + for i=#array,1,-1 do + local attr = array[i].attr; + if filters[attr and attr.xmlns] then + t_remove(array, i); + count = count + 1; + end + end + return count; +end +local function filter_xmlns_from_stanza(stanza, filters) + if filters then + if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then + return stanza, filter_xmlns_from_array(stanza, filters); + end + end + return stanza, 0; +end +local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; +local function get_filtered_presence(stanza) + return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters); +end +local kickable_error_conditions = { + ["gone"] = true; + ["internal-server-error"] = true; + ["item-not-found"] = true; + ["jid-malformed"] = true; + ["recipient-unavailable"] = true; + ["redirect"] = true; + ["remote-server-not-found"] = true; + ["remote-server-timeout"] = true; + ["service-unavailable"] = true; + ["malformed error"] = true; +}; +local function get_error_condition(stanza) + for _, tag in ipairs(stanza.tags) do + if tag.name == "error" and (not(tag.attr.xmlns) or tag.attr.xmlns == "jabber:client") then + for _, cond in ipairs(tag.tags) do + if cond.attr.xmlns == "urn:ietf:params:xml:ns:xmpp-stanzas" then + return cond.name; + end + end + return "malformed error"; + end + end + return "malformed error"; +end +local function is_kickable_error(stanza) + local cond = get_error_condition(stanza); + return kickable_error_conditions[cond] and cond; +end +local function getUsingPath(stanza, path, getText) + local tag = stanza; + for _, name in ipairs(path) do + if type(tag) ~= 'table' then return; end + tag = tag:child_with_name(name); + end + if tag and getText then tag = table.concat(tag); end + return tag; +end +local function getTag(stanza, path) return getUsingPath(stanza, path); end +local function getText(stanza, path) return getUsingPath(stanza, path, true); end +----------- + +--[[function get_room_disco_info(room, stanza) + return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info") + :tag("identity", {category='conference', type='text', name=room._data["name"]):up() + :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply +end +function get_room_disco_items(room, stanza) + return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); +end -- TODO allow non-private rooms]] + +-- + +local room_mt = {}; +room_mt.__index = room_mt; + +function room_mt:get_default_role(affiliation) + if affiliation == "owner" or affiliation == "admin" then + return "moderator"; + elseif affiliation == "member" or not affiliation then + return "participant"; + end +end + +function room_mt:broadcast_presence(stanza, sid, code, nick) + stanza = get_filtered_presence(stanza); + local occupant = self._occupants[stanza.attr.from]; + stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) + :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none", nick=nick}):up(); + if code then + stanza:tag("status", {code=code}):up(); + end + self:broadcast_except_nick(stanza, stanza.attr.from); + local me = self._occupants[stanza.attr.from]; + if me then + stanza:tag("status", {code='110'}); + stanza.attr.to = sid; + self:_route_stanza(stanza); + end +end +function room_mt:broadcast_message(stanza, historic) + for occupant, o_data in pairs(self._occupants) do + for jid in pairs(o_data.sessions) do + stanza.attr.to = jid; + self:_route_stanza(stanza); + end + end + if historic then -- add to history + local history = self._data['history']; + if not history then history = {}; self._data['history'] = history; end + -- stanza = st.clone(stanza); + stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203 + stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) + t_insert(history, st.clone(st.preserialize(stanza))); + while #history > history_length do t_remove(history, 1) end + end +end +function room_mt:broadcast_except_nick(stanza, nick) + for rnick, occupant in pairs(self._occupants) do + if rnick ~= nick then + for jid in pairs(occupant.sessions) do + stanza.attr.to = jid; + self:_route_stanza(stanza); + end + end + end +end + +function room_mt:send_occupant_list(to) + local current_nick = self._jid_nick[to]; + for occupant, o_data in pairs(self._occupants) do + if occupant ~= current_nick then + local pres = get_filtered_presence(o_data.sessions[o_data.jid]); + pres.attr.to, pres.attr.from = to, occupant; + pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) + :tag("item", {affiliation=o_data.affiliation or "none", role=o_data.role or "none"}):up(); + self:_route_stanza(pres); + end + end +end +function room_mt:send_history(to) + local history = self._data['history']; -- send discussion history + if history then + for _, msg in ipairs(history) do + msg = st.deserialize(msg); + msg.attr.to=to; + self:_route_stanza(msg); + end + end + if self._data['subject'] then + self:_route_stanza(st.message({type='groupchat', from=self.jid, to=to}):tag("subject"):text(self._data['subject'])); + end +end + +local function room_get_disco_info(self, stanza) + return st.reply(stanza):query("http://jabber.org/protocol/disco#info") + :tag("identity", {category="conference", type="text"}):up() + :tag("feature", {var="http://jabber.org/protocol/muc"}); +end +local function room_get_disco_items(self, stanza) + local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); + for room_jid in pairs(self._occupants) do + reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up(); + end + return reply; +end +function room_mt:set_subject(current_nick, subject) + -- TODO check nick's authority + if subject == "" then subject = nil; end + self._data['subject'] = subject; + if self.save then self:save(); end + local msg = st.message({type='groupchat', from=current_nick}) + :tag('subject'):text(subject):up(); + self:broadcast_message(msg, false); + return true; +end + +function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc + local from, to = stanza.attr.from, stanza.attr.to; + local room = jid_bare(to); + local current_nick = self._jid_nick[from]; + local type = stanza.attr.type; + log("debug", "room: %s, current_nick: %s, stanza: %s", room or "nil", current_nick or "nil", stanza:top_tag()); + if (select(2, jid_split(from)) == muc_domain) then error("Presence from the MUC itself!!!"); end + if stanza.name == "presence" then + local pr = get_filtered_presence(stanza); + pr.attr.from = current_nick; + if type == "error" then -- error, kick em out! + if current_nick then + log("debug", "kicking %s from %s", current_nick, room); + self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}) + :tag('status'):text('Kicked: '..get_error_condition(stanza))); -- send unavailable + end + elseif type == "unavailable" then -- unavailable + if current_nick then + log("debug", "%s leaving %s", current_nick, room); + local occupant = self._occupants[current_nick]; + local new_jid = next(occupant.sessions); + if new_jid == from then new_jid = next(occupant.sessions, new_jid); end + if new_jid then + local jid = occupant.jid; + occupant.jid = new_jid; + occupant.sessions[from] = nil; + pr.attr.to = from; + pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) + :tag("item", {affiliation=occupant.affiliation or "none", role='none'}):up() + :tag("status", {code='110'}); + self:_route_stanza(pr); + if jid ~= new_jid then + pr = st.clone(occupant.sessions[new_jid]) + :tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) + :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none"}); + pr.attr.from = current_nick; + self:broadcast_except_nick(pr, current_nick); + end + else + occupant.role = 'none'; + self:broadcast_presence(pr, from); + self._occupants[current_nick] = nil; + end + self._jid_nick[from] = nil; + end + elseif not type then -- available + if current_nick then + --if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence + if current_nick == to then -- simple presence + log("debug", "%s broadcasted presence", current_nick); + self._occupants[current_nick].sessions[from] = pr; + self:broadcast_presence(pr, from); + else -- change nick + local occupant = self._occupants[current_nick]; + local is_multisession = next(occupant.sessions, next(occupant.sessions)); + if self._occupants[to] or is_multisession then + log("debug", "%s couldn't change nick", current_nick); + local reply = st.error_reply(stanza, "cancel", "conflict"):up(); + reply.tags[1].attr.code = "409"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); + else + local data = self._occupants[current_nick]; + local to_nick = select(3, jid_split(to)); + if to_nick then + log("debug", "%s (%s) changing nick to %s", current_nick, data.jid, to); + local p = st.presence({type='unavailable', from=current_nick}); + self:broadcast_presence(p, from, '303', to_nick); + self._occupants[current_nick] = nil; + self._occupants[to] = data; + self._jid_nick[from] = to; + pr.attr.from = to; + self._occupants[to].sessions[from] = pr; + self:broadcast_presence(pr, from); + else + --TODO malformed-jid + end + end + end + --else -- possible rejoin + -- log("debug", "%s had connection replaced", current_nick); + -- self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}) + -- :tag('status'):text('Replaced by new connection'):up()); -- send unavailable + -- self:handle_to_occupant(origin, stanza); -- resend available + --end + else -- enter room + local new_nick = to; + local is_merge; + if self._occupants[to] then + if jid_bare(from) ~= jid_bare(self._occupants[to].jid) then + new_nick = nil; + end + is_merge = true; + end + if not new_nick then + log("debug", "%s couldn't join due to nick conflict: %s", from, to); + local reply = st.error_reply(stanza, "cancel", "conflict"):up(); + reply.tags[1].attr.code = "409"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); + else + log("debug", "%s joining as %s", from, to); + if not next(self._affiliations) then -- new room, no owners + self._affiliations[jid_bare(from)] = "owner"; + end + local affiliation = self:get_affiliation(from); + local role = self:get_default_role(affiliation) + if role then -- new occupant + if not is_merge then + self._occupants[to] = {affiliation=affiliation, role=role, jid=from, sessions={[from]=get_filtered_presence(stanza)}}; + else + self._occupants[to].sessions[from] = get_filtered_presence(stanza); + end + self._jid_nick[from] = to; + self:send_occupant_list(from); + pr.attr.from = to; + if not is_merge then + self:broadcast_presence(pr, from); + else + pr.attr.to = from; + self:_route_stanza(pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) + :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up() + :tag("status", {code='110'})); + end + self:send_history(from); + else -- banned + local reply = st.error_reply(stanza, "auth", "forbidden"):up(); + reply.tags[1].attr.code = "403"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); + end + end + end + elseif type ~= 'result' then -- bad type + if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences + origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error? + end + end + elseif not current_nick then -- not in room + if type == "error" or type == "result" then + local id = stanza.name == "iq" and stanza.attr.id and base64.decode(stanza.attr.id); + local _nick, _id, _hash = (id or ""):match("^(.+)%z(.*)%z(.+)$"); + local occupant = self._occupants[stanza.attr.to]; + if occupant and _nick and self._jid_nick[_nick] and _id and _hash then + local id, _to = stanza.attr.id; + for jid in pairs(occupant.sessions) do + if md5(jid) == _hash then + _to = jid; + break; + end + end + if _to then + stanza.attr.to, stanza.attr.from, stanza.attr.id = _to, self._jid_nick[_nick], _id; + self:_route_stanza(stanza); + stanza.attr.to, stanza.attr.from, stanza.attr.id = to, from, id; + end + end + else + origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); + end + elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM + origin.send(st.error_reply(stanza, "modify", "bad-request")); + elseif current_nick and stanza.name == "message" and type == "error" and is_kickable_error(stanza) then + log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid); + self:handle_to_occupant(origin, st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to}) + :tag('status'):text('Kicked: '..get_error_condition(stanza))); -- send unavailable + else -- private stanza + local o_data = self._occupants[to]; + if o_data then + log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid); + local jid = o_data.jid; + local bare = jid_bare(jid); + stanza.attr.to, stanza.attr.from = jid, current_nick; + local id = stanza.attr.id; + if stanza.name=='iq' and type=='get' and stanza.tags[1].attr.xmlns == 'vcard-temp' and bare ~= jid then + stanza.attr.to = bare; + stanza.attr.id = base64.encode(jid.."\0"..id.."\0"..md5(from)); + end + self:_route_stanza(stanza); + stanza.attr.to, stanza.attr.from, stanza.attr.id = to, from, id; + elseif type ~= "error" and type ~= "result" then -- recipient not in room + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room")); + end + end +end + +function room_mt:handle_form(origin, stanza) + if self:get_affiliation(stanza.attr.from) ~= "owner" then origin.send(st.error_reply(stanza, "auth", "forbidden")); return; end + if stanza.attr.type == "get" then + local title = "Configuration for "..self.jid; + origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") + :tag("x", {xmlns='jabber:x:data', type='form'}) + :tag("title"):text(title):up() + :tag("instructions"):text(title):up() + :tag("field", {type='hidden', var='FORM_TYPE'}):tag("value"):text("http://jabber.org/protocol/muc#roomconfig"):up():up() + :tag("field", {type='boolean', label='Make Room Persistent?', var='muc#roomconfig_persistentroom'}) + :tag("value"):text(self._data.persistent and "1" or "0"):up() + :up() + :tag("field", {type='boolean', label='Make Room Publicly Searchable?', var='muc#roomconfig_publicroom'}) + :tag("value"):text(self._data.hidden and "0" or "1"):up() + :up() + ); + elseif stanza.attr.type == "set" then + local query = stanza.tags[1]; + local form; + for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end + if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end + if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end + if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + local fields = {}; + for _, field in pairs(form.tags) do + if field.name == "field" and field.attr.var and field.tags[1].name == "value" and #field.tags[1].tags == 0 then + fields[field.attr.var] = field.tags[1][1] or ""; + end + end + if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + + local persistent = fields['muc#roomconfig_persistentroom']; + if persistent == "0" or persistent == "false" then persistent = nil; elseif persistent == "1" or persistent == "true" then persistent = true; + else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + self._data.persistent = persistent; + module:log("debug", "persistent=%s", tostring(persistent)); + + local public = fields['muc#roomconfig_publicroom']; + if public == "0" or public == "false" then public = nil; elseif public == "1" or public == "true" then public = true; + else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + self._data.hidden = not public and true or nil; + + if self.save then self:save(true); end + origin.send(st.reply(stanza)); + end +end + +function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc + local type = stanza.attr.type; + local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; + if stanza.name == "iq" then + if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" then + origin.send(room_get_disco_info(self, stanza)); + elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" then + origin.send(room_get_disco_items(self, stanza)); + elseif xmlns == "http://jabber.org/protocol/muc#admin" then + local actor = stanza.attr.from; + local affiliation = self:get_affiliation(actor); + local current_nick = self._jid_nick[actor]; + local role = current_nick and self._occupants[current_nick].role or self:get_default_role(affiliation); + local item = stanza.tags[1].tags[1]; + if item and item.name == "item" then + if type == "set" then + local callback = function() origin.send(st.reply(stanza)); end + if item.attr.jid then -- Validate provided JID + item.attr.jid = jid_prep(item.attr.jid); + if not item.attr.jid then + origin.send(st.error_reply(stanza, "modify", "jid-malformed")); + return; + end + end + if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation + local occupant = self._occupants[self.jid.."/"..item.attr.nick]; + if occupant then item.attr.jid = occupant.jid; end + end + local reason = item.tags[1] and item.tags[1].name == "reason" and #item.tags[1] == 1 and item.tags[1][1]; + if item.attr.affiliation and item.attr.jid and not item.attr.role then + local success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, callback, reason); + if not success then origin.send(st.error_reply(stanza, errtype, err)); end + elseif item.attr.role and item.attr.nick and not item.attr.affiliation then + local success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, callback, reason); + if not success then origin.send(st.error_reply(stanza, errtype, err)); end + else + origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + elseif type == "get" then + local _aff = item.attr.affiliation; + local _rol = item.attr.role; + if _aff and not _rol then + if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then + local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); + for jid, affiliation in pairs(self._affiliations) do + if affiliation == _aff then + reply:tag("item", {affiliation = _aff, jid = jid}):up(); + end + end + origin.send(reply); + else + origin.send(st.error_reply(stanza, "auth", "forbidden")); + end + elseif _rol and not _aff then + if role == "moderator" then + -- TODO allow admins and owners not in room? Provide read-only access to everyone who can see the participants anyway? + if _rol == "none" then _rol = nil; end + local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); + for nick, occupant in pairs(self._occupants) do + if occupant.role == _rol then + reply:tag("item", {nick = nick, role = _rol or "none", affiliation = occupant.affiliation or "none", jid = occupant.jid}):up(); + end + end + origin.send(reply); + else + origin.send(st.error_reply(stanza, "auth", "forbidden")); + end + else + origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + end + elseif type == "set" or type == "get" then + origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + elseif xmlns == "http://jabber.org/protocol/muc#owner" and (type == "get" or type == "set") and stanza.tags[1].name == "query" then + self:handle_form(origin, stanza); + elseif type == "set" or type == "get" then + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + end + elseif stanza.name == "message" and type == "groupchat" then + local from, to = stanza.attr.from, stanza.attr.to; + local room = jid_bare(to); + local current_nick = self._jid_nick[from]; + if not current_nick then -- not in room + origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); + else + local from = stanza.attr.from; + stanza.attr.from = current_nick; + local subject = getText(stanza, {"subject"}); + if subject then + self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza + else + self:broadcast_message(stanza, true); + end + end + elseif stanza.name == "message" and type == "error" and is_kickable_error(stanza) then + local current_nick = self._jid_nick[stanza.attr.from]; + log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid); + self:handle_to_occupant(origin, st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to}) + :tag('status'):text('Kicked: '..get_error_condition(stanza))); -- send unavailable + elseif stanza.name == "presence" then -- hack - some buggy clients send presence updates to the room rather than their nick + local to = stanza.attr.to; + local current_nick = self._jid_nick[stanza.attr.from]; + if current_nick then + stanza.attr.to = current_nick; + self:handle_to_occupant(origin, stanza); + stanza.attr.to = to; + elseif type ~= "error" and type ~= "result" then + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + end + elseif stanza.name == "message" and not stanza.attr.type and #stanza.tags == 1 and self._jid_nick[stanza.attr.from] + and stanza.tags[1].name == "x" and stanza.tags[1].attr.xmlns == "http://jabber.org/protocol/muc#user" then + local x = stanza.tags[1]; + local payload = (#x.tags == 1 and x.tags[1]); + if payload and payload.name == "invite" and payload.attr.to then + local _from, _to = stanza.attr.from, stanza.attr.to; + local _invitee = jid_prep(payload.attr.to); + if _invitee then + local _reason = payload.tags[1] and payload.tags[1].name == 'reason' and #payload.tags[1].tags == 0 and payload.tags[1][1]; + local invite = st.message({from = _to, to = _invitee, id = stanza.attr.id}) + :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) + :tag('invite', {from=_from}) + :tag('reason'):text(_reason or ""):up() + :up() + :up() + :tag('x', {xmlns="jabber:x:conference", jid=_to}) -- COMPAT: Some older clients expect this + :text(_reason or "") + :up() + :tag('body') -- Add a plain message for clients which don't support invites + :text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or "")) + :up(); + self:_route_stanza(invite); + else + origin.send(st.error_reply(stanza, "cancel", "jid-malformed")); + end + else + origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + else + if type == "error" or type == "result" then return; end + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + end +end + +function room_mt:handle_stanza(origin, stanza) + local to_node, to_host, to_resource = jid_split(stanza.attr.to); + if to_resource then + self:handle_to_occupant(origin, stanza); + else + self:handle_to_room(origin, stanza); + end +end + +function room_mt:route_stanza(stanza) end -- Replace with a routing function, e.g., function(room, stanza) core_route_stanza(origin, stanza); end + +function room_mt:get_affiliation(jid) + local node, host, resource = jid_split(jid); + local bare = node and node.."@"..host or host; + local result = self._affiliations[bare]; -- Affiliations are granted, revoked, and maintained based on the user's bare JID. + if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned + return result; +end +function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) + jid = jid_bare(jid); + if affiliation == "none" then affiliation = nil; end + if affiliation and affiliation ~= "outcast" and affiliation ~= "owner" and affiliation ~= "admin" and affiliation ~= "member" then + return nil, "modify", "not-acceptable"; + end + if self:get_affiliation(actor) ~= "owner" then return nil, "cancel", "not-allowed"; end + if jid_bare(actor) == jid then return nil, "cancel", "not-allowed"; end + self._affiliations[jid] = affiliation; + local role = self:get_default_role(affiliation); + local p = st.presence() + :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) + :tag("item", {affiliation=affiliation or "none", role=role or "none"}) + :tag("reason"):text(reason or ""):up() + :up(); + local x = p.tags[1]; + local item = x.tags[1]; + if not role then -- getting kicked + p.attr.type = "unavailable"; + if affiliation == "outcast" then + x:tag("status", {code="301"}):up(); -- banned + else + x:tag("status", {code="321"}):up(); -- affiliation change + end + end + local modified_nicks = {}; + for nick, occupant in pairs(self._occupants) do + if jid_bare(occupant.jid) == jid then + if not role then -- getting kicked + self._occupants[nick] = nil; + else + t_insert(modified_nicks, nick); + occupant.affiliation, occupant.role = affiliation, role; + end + p.attr.from = nick; + for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick + if not role then self._jid_nick[jid] = nil; end + p.attr.to = jid; + self:_route_stanza(p); + end + end + end + if self.save then self:save(); end + if callback then callback(); end + for _, nick in ipairs(modified_nicks) do + p.attr.from = nick; + self:broadcast_except_nick(p, nick); + end + return true; +end + +function room_mt:get_role(nick) + local session = self._occupants[nick]; + return session and session.role or nil; +end +function room_mt:set_role(actor, nick, role, callback, reason) + if role == "none" then role = nil; end + if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end + if self:get_affiliation(actor) ~= "owner" then return nil, "cancel", "not-allowed"; end + local occupant = self._occupants[nick]; + if not occupant then return nil, "modify", "not-acceptable"; end + if occupant.affiliation == "owner" or occupant.affiliation == "admin" then return nil, "cancel", "not-allowed"; end + local p = st.presence({from = nick}) + :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) + :tag("item", {affiliation=occupant.affiliation or "none", nick=nick, role=role or "none"}) + :tag("reason"):text(reason or ""):up() + :up(); + if not role then -- kick + p.attr.type = "unavailable"; + self._occupants[nick] = nil; + for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick + self._jid_nick[jid] = nil; + end + p:tag("status", {code = "307"}):up(); + else + occupant.role = role; + end + for jid in pairs(occupant.sessions) do -- send to all sessions of the nick + p.attr.to = jid; + self:_route_stanza(p); + end + if callback then callback(); end + self:broadcast_except_nick(p, nick); + return true; +end + +function room_mt:_route_stanza(stanza) + local muc_child; + local to_occupant = self._occupants[self._jid_nick[stanza.attr.to]]; + local from_occupant = self._occupants[stanza.attr.from]; + if stanza.name == "presence" then + if to_occupant and from_occupant then + if to_occupant.role == "moderator" or jid_bare(to_occupant.jid) == jid_bare(from_occupant.jid) then + for i=#stanza.tags,1,-1 do + local tag = stanza.tags[i]; + if tag.name == "x" and tag.attr.xmlns == "http://jabber.org/protocol/muc#user" then + muc_child = tag; + break; + end + end + end + end + end + if muc_child then + for _, item in pairs(muc_child.tags) do + if item.name == "item" then + if from_occupant == to_occupant then + item.attr.jid = stanza.attr.to; + else + item.attr.jid = from_occupant.jid; + end + end + end + end + self:route_stanza(stanza); + if muc_child then + for _, item in pairs(muc_child.tags) do + if item.name == "item" then + item.attr.jid = nil; + end + end + end +end + +local _M = {}; -- module "muc" + +function _M.new_room(jid) + return setmetatable({ + jid = jid; + _jid_nick = {}; + _occupants = {}; + _data = {}; + _affiliations = {}; + }, room_mt); +end + +return _M; @@ -33,6 +33,25 @@ end -- Required to be able to find packages installed with luarocks pcall(require, "luarocks.require") +-- Replace require with one that doesn't pollute _G +do + local _realG = _G; + local _real_require = require; + function require(...) + local curr_env = getfenv(2); + local curr_env_mt = getmetatable(getfenv(2)); + local _realG_mt = getmetatable(_realG); + if curr_env_mt and curr_env_mt.__index and not curr_env_mt.__newindex and _realG_mt then + local old_newindex + old_newindex, _realG_mt.__newindex = _realG_mt.__newindex, curr_env; + local ret = _real_require(...); + _realG_mt.__newindex = old_newindex; + return ret; + end + return _real_require(...); + end +end + config = require "core.configmanager" @@ -93,6 +112,17 @@ function init_global_state() prosody.events = require "util.events".new(); + prosody.platform = "unknown"; + if os.getenv("WINDIR") then + prosody.platform = "windows"; + elseif package.config:sub(1,1) == "/" then + prosody.platform = "posix"; + end + + prosody.installed = nil; + if CFG_SOURCEDIR and (prosody.platform == "windows" or CFG_SOURCEDIR:match("^/")) then + prosody.installed = true; + end -- Function to reload the config file function prosody.reload_config() @@ -151,11 +181,16 @@ function load_secondary_libraries() require "core.sessionmanager" require "core.stanza_router" + require "net.http" + require "util.array" + require "util.datetime" require "util.iterators" require "util.timer" require "util.helpers" + pcall(require, "util.signal") -- Not on Windows + -- Commented to protect us from -- the second kind of people --[[ @@ -186,7 +221,7 @@ function prepare_to_start() prosody.events.fire_event("server-starting"); -- Load SSL settings from config, and create a ctx table - local global_ssl_ctx = ssl and config.get("*", "core", "ssl"); + local global_ssl_ctx = rawget(_G, "ssl") and config.get("*", "core", "ssl"); if global_ssl_ctx then local default_ssl_ctx = { mode = "server", protocol = "sslv23", capath = "/etc/ssl/certs", verify = "none"; }; setmetatable(global_ssl_ctx, { __index = default_ssl_ctx }); @@ -194,7 +229,7 @@ function prepare_to_start() local cl = require "net.connlisteners"; -- start listening on sockets - function net_activate_ports(option, listener, default, conntype) + function prosody.net_activate_ports(option, listener, default, conntype) if not cl.get(listener) then return; end local ports = config.get("*", "core", option.."_ports") or default; if type(ports) == "number" then ports = {ports} end; @@ -219,11 +254,11 @@ function prepare_to_start() end end - net_activate_ports("c2s", "xmppclient", {5222}, (global_ssl_ctx and "tls") or "tcp"); - net_activate_ports("s2s", "xmppserver", {5269}, "tcp"); - net_activate_ports("component", "xmppcomponent", {}, "tcp"); - net_activate_ports("legacy_ssl", "xmppclient", {}, "ssl"); - net_activate_ports("console", "console", {5582}, "tcp"); + prosody.net_activate_ports("c2s", "xmppclient", {5222}, (global_ssl_ctx and "tls") or "tcp"); + prosody.net_activate_ports("s2s", "xmppserver", {5269}, (global_ssl_ctx and "tls") or "tcp"); + prosody.net_activate_ports("component", "xmppcomponent", {}, "tcp"); + prosody.net_activate_ports("legacy_ssl", "xmppclient", {}, "ssl"); + prosody.net_activate_ports("console", "console", {5582}, "tcp"); prosody.start_time = os.time(); end @@ -247,7 +282,7 @@ end function loop() -- Error handler for errors that make it this far local function catch_uncaught_error(err) - if err:match("%d*: interrupted!$") then + if type(err) == "string" and err:match("%d*: interrupted!$") then return "quitting"; end @@ -314,8 +349,8 @@ read_version(); log("info", "Hello and welcome to Prosody version %s", prosody.version); load_secondary_libraries(); init_data_store(); -prepare_to_start(); init_global_protection(); +prepare_to_start(); eventmanager.fire_event("server-started"); prosody.events.fire_event("server-started"); diff --git a/prosody.cfg.lua.dist b/prosody.cfg.lua.dist index 38618131..d660a9bd 100644 --- a/prosody.cfg.lua.dist +++ b/prosody.cfg.lua.dist @@ -1,104 +1,110 @@ --- Prosody Example Configuration File
---
--- If it wasn't already obvious, -- starts a comment, and all
--- text after it on a line is ignored by Prosody.
---
--- The config is split into sections, a global section, and one
--- for each defined host that we serve. You can add as many host
--- sections as you like.
---
--- Lists are written { "like", "this", "one" }
--- Lists can also be of { 1, 2, 3 } numbers, and other things.
--- Either commas, or semi-colons; may be used
--- as seperators.
---
--- A table is a list of values, except each value has a name. An
--- example table would be:
---
--- ssl = { key = "keyfile.key", certificate = "certificate.cert" }
---
--- Whitespace (that is tabs, spaces, line breaks) is mostly insignificant, so
--- can
--- be placed anywhere that you deem fitting.
---
--- Tip: You can check that the syntax of this file is correct when you have finished
--- by running: luac -p prosody.cfg.lua
--- If there are any errors, it will let you know what and where they are, otherwise it
--- will keep quiet.
---
--- The only thing left to do is rename this file to remove the .dist ending, and fill in the
--- blanks. Good luck, and happy Jabbering!
-
--- Server-wide settings go in this section
-Host "*"
-
- -- This is the list of modules Prosody will load on startup.
- -- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
- modules_enabled = {
- -- Generally required
- "roster"; -- Allow users to have a roster. Recommended ;)
- "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
- "tls"; -- Add support for secure TLS on c2s/s2s connections
- "dialback"; -- s2s dialback support
- "disco"; -- Service discovery
-
- -- Not essential, but recommended
- "private"; -- Private XML storage (for room bookmarks, etc.)
- "vcard"; -- Allow users to set vCards
-
- -- Nice to have
- "legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
- "version"; -- Replies to server version requests
- "uptime"; -- Report how long server has been running
- "time"; -- Let others know the time here on this server
- "ping"; -- Replies to XMPP pings with pongs
- "pep"; -- Enables users to publish their mood, activity, playing music and more
- "register"; -- Allow users to register on this server using a client and change passwords
-
- -- Other specific functionality
- --"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
- --"console"; -- telnet to port 5582 (needs console_enabled = true)
- --"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
- --"httpserver"; -- Serve static files from a directory over HTTP
- };
-
- -- These modules are auto-loaded, should you
- -- for (for some mad reason) want to disable
- -- them then uncomment them below
- modules_disabled = {
- -- "presence";
- -- "message";
- -- "iq";
- };
-
- -- Disable account creation by default, for security
- -- For more information see http://prosody.im/doc/creating_accounts
- allow_registration = false;
-
- -- These are the SSL/TLS-related settings. If you don't want
- -- to use SSL/TLS, you may comment or remove this
- ssl = {
- key = "certs/localhost.key";
- certificate = "certs/localhost.cert";
- }
-
--- This allows clients to connect to localhost. No harm in it.
-Host "localhost"
-
--- Section for example.com
--- (replace example.com with your domain name)
-Host "example.com"
-
- enabled = false -- This will disable the host, preserving the config, but denying connections
-
- -- Assign this host a certificate for TLS, otherwise it would use the one
- -- set in the global section (if any).
- -- Note that old-style SSL on port 5223 only supports one certificate, and will always
- -- use the global one.
- ssl = {
- key = "certs/example.com.key";
- certificate = "certs/example.com.crt";
- }
-
--- Set up a MUC (multi-user chat) room server on conference.example.com:
-Component "conference.example.com" "muc"
+-- Prosody Example Configuration File +-- +-- If it wasn't already obvious, -- starts a comment, and all +-- text after it on a line is ignored by Prosody. +-- +-- The config is split into sections, a global section, and one +-- for each defined host that we serve. You can add as many host +-- sections as you like. +-- +-- Lists are written { "like", "this", "one" } +-- Lists can also be of { 1, 2, 3 } numbers, and other things. +-- Either commas, or semi-colons; may be used +-- as seperators. +-- +-- A table is a list of values, except each value has a name. An +-- example table would be: +-- +-- ssl = { key = "keyfile.key", certificate = "certificate.cert" } +-- +-- Whitespace (that is tabs, spaces, line breaks) is mostly insignificant, so +-- can +-- be placed anywhere that you deem fitting. +-- +-- Tip: You can check that the syntax of this file is correct when you have finished +-- by running: luac -p prosody.cfg.lua +-- If there are any errors, it will let you know what and where they are, otherwise it +-- will keep quiet. +-- +-- The only thing left to do is rename this file to remove the .dist ending, and fill in the +-- blanks. Good luck, and happy Jabbering! + +-- Server-wide settings go in this section +Host "*" + + -- This is a (by default, empty) list of accounts that are admins + -- for the server. Note that you must create the accounts separately + -- (see http://prosody.im/doc/creating_accounts for info) + -- Example: admins = { "user1@example.com", "user2@example.net" } + admins = { } + + -- This is the list of modules Prosody will load on startup. + -- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too. + modules_enabled = { + -- Generally required + "roster"; -- Allow users to have a roster. Recommended ;) + "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in. + "tls"; -- Add support for secure TLS on c2s/s2s connections + "dialback"; -- s2s dialback support + "disco"; -- Service discovery + + -- Not essential, but recommended + "private"; -- Private XML storage (for room bookmarks, etc.) + "vcard"; -- Allow users to set vCards + + -- Nice to have + "legacyauth"; -- Legacy authentication. Only used by some old clients and bots. + "version"; -- Replies to server version requests + "uptime"; -- Report how long server has been running + "time"; -- Let others know the time here on this server + "ping"; -- Replies to XMPP pings with pongs + "pep"; -- Enables users to publish their mood, activity, playing music and more + "register"; -- Allow users to register on this server using a client and change passwords + + -- Other specific functionality + --"posix"; -- POSIX functionality, sends server to background, enables syslog, etc. + --"console"; -- telnet to port 5582 (needs console_enabled = true) + --"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP" + --"httpserver"; -- Serve static files from a directory over HTTP + }; + + -- These modules are auto-loaded, should you + -- for (for some mad reason) want to disable + -- them then uncomment them below + modules_disabled = { + -- "presence"; + -- "message"; + -- "iq"; + }; + + -- Disable account creation by default, for security + -- For more information see http://prosody.im/doc/creating_accounts + allow_registration = false; + + -- These are the SSL/TLS-related settings. If you don't want + -- to use SSL/TLS, you may comment or remove this + ssl = { + key = "certs/localhost.key"; + certificate = "certs/localhost.cert"; + } + +-- This allows clients to connect to localhost. No harm in it. +Host "localhost" + +-- Section for example.com +-- (replace example.com with your domain name) +Host "example.com" + + enabled = false -- This will disable the host, preserving the config, but denying connections + + -- Assign this host a certificate for TLS, otherwise it would use the one + -- set in the global section (if any). + -- Note that old-style SSL on port 5223 only supports one certificate, and will always + -- use the global one. + ssl = { + key = "certs/example.com.key"; + certificate = "certs/example.com.crt"; + } + +-- Set up a MUC (multi-user chat) room server on conference.example.com: +Component "conference.example.com" "muc" @@ -32,7 +32,6 @@ end -- Required to be able to find packages installed with luarocks pcall(require, "luarocks.require") - config = require "core.configmanager" do @@ -102,15 +101,20 @@ local error_messages = setmetatable({ ["no-password"] = "No password was supplied"; ["no-such-user"] = "The given user does not exist on the server"; ["unable-to-save-data"] = "Unable to store, perhaps you don't have permission?"; - ["no-pidfile"] = "There is no pidfile option in the configuration file, see http://prosody.im/doc/prosodyctl#pidfile for help"; + ["no-pidfile"] = "There is no 'pidfile' option in the configuration file, see http://prosody.im/doc/prosodyctl#pidfile for help"; ["no-such-method"] = "This module has no commands"; ["not-running"] = "Prosody is not running"; }, { __index = function (t,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end }); +local events = require "util.events".new(); + hosts = {}; +prosody = { hosts = hosts, events = events }; -require "core.hostmanager" -require "core.eventmanager".fire_event("server-starting"); +for hostname, config in pairs(config.getconfig()) do + hosts[hostname] = { events = events }; +end + require "core.modulemanager" require "util.prosodyctl" diff --git a/tests/test.lua b/tests/test.lua index f5bcb02e..f5976a02 100644 --- a/tests/test.lua +++ b/tests/test.lua @@ -11,6 +11,7 @@ function run_all_tests() dotest "util.jid" dotest "util.multitable" + dotest "core.modulemanager" dotest "core.stanza_router" dotest "core.s2smanager" dotest "core.configmanager" @@ -29,9 +30,11 @@ else package.cpath = package.cpath..";../?.so"; end +local _realG = _G; + require "util.import" -local env_mt = { __index = function (t,k) return rawget(_G, k) or print("WARNING: Attempt to access nil global '"..tostring(k).."'"); end }; +local env_mt = { __index = function (t,k) return rawget(_realG, k) or print("WARNING: Attempt to access nil global '"..tostring(k).."'"); end }; function testlib_new_env(t) return setmetatable(t or {}, env_mt); end @@ -65,7 +68,7 @@ end function dosingletest(testname, fname) - local tests = setmetatable({}, { __index = _G }); + local tests = setmetatable({}, { __index = _realG }); tests.__unit = testname; tests.__test = fname; local chunk, err = loadfile(testname); @@ -103,7 +106,7 @@ function dosingletest(testname, fname) end function dotest(unitname) - local tests = setmetatable({}, { __index = _G }); + local tests = setmetatable({}, { __index = _realG }); tests.__unit = unitname; local chunk, err = loadfile("test_"..unitname:gsub("%.", "_")..".lua"); if not chunk then @@ -118,8 +121,9 @@ function dotest(unitname) return; end - local unit = setmetatable({}, { __index = setmetatable({ module = function () end }, { __index = _G }) }); - + if tests.env then setmetatable(tests.env, { __index = _realG }); end + local unit = setmetatable({}, { __index = setmetatable({ _G = tests.env or _G }, { __index = tests.env or _G }) }); + unit._G = unit; _realG._G = unit; local fn = "../"..unitname:gsub("%.", "/")..".lua"; local chunk, err = loadfile(fn); if not chunk then diff --git a/tests/test_core_modulemanager.lua b/tests/test_core_modulemanager.lua new file mode 100644 index 00000000..82e9aa45 --- /dev/null +++ b/tests/test_core_modulemanager.lua @@ -0,0 +1,48 @@ +-- 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 config = require "core.configmanager"; +local helpers = require "util.helpers"; +local set = require "util.set"; + +function load_modules_for_host(load_modules_for_host, mm) + local test_num = 0; + local function test_load(global_modules_enabled, global_modules_disabled, host_modules_enabled, host_modules_disabled, expected_modules) + test_num = test_num + 1; + -- Prepare + hosts = { ["example.com"] = {} }; + config.set("*", "core", "modules_enabled", global_modules_enabled); + config.set("*", "core", "modules_disabled", global_modules_disabled); + config.set("example.com", "core", "modules_enabled", host_modules_enabled); + config.set("example.com", "core", "modules_disabled", host_modules_disabled); + + expected_modules = set.new(expected_modules); + expected_modules:add_list(helpers.get_upvalue(load_modules_for_host, "autoload_modules")); + + local loaded_modules = set.new(); + function mm.load(host, module) + assert_equal(host, "example.com", test_num..": Host isn't example.com but "..tostring(host)); + assert_equal(expected_modules:contains(module), true, test_num..": Loading unexpected module '"..tostring(module).."'"); + loaded_modules:add(module); + end + load_modules_for_host("example.com"); + assert_equal((expected_modules - loaded_modules):empty(), true, test_num..": Not all modules loaded: "..tostring(expected_modules - loaded_modules)); + end + + test_load({ "one", "two", "three" }, nil, nil, nil, { "one", "two", "three" }); + test_load({ "one", "two", "three" }, {}, nil, nil, { "one", "two", "three" }); + test_load({ "one", "two", "three" }, { "two" }, nil, nil, { "one", "three" }); + test_load({ "one", "two", "three" }, { "three" }, nil, nil, { "one", "two" }); + test_load({ "one", "two", "three" }, nil, nil, { "three" }, { "one", "two" }); + test_load({ "one", "two", "three" }, nil, { "three" }, { "three" }, { "one", "two", "three" }); + + test_load({ "one", "two" }, nil, { "three" }, nil, { "one", "two", "three" }); + test_load({ "one", "two", "three" }, nil, { "three" }, nil, { "one", "two", "three" }); + test_load({ "one", "two", "three" }, { "three" }, { "three" }, nil, { "one", "two", "three" }); + test_load({ "one", "two" }, { "three" }, { "three" }, nil, { "one", "two", "three" }); +end diff --git a/tests/test_core_stanza_router.lua b/tests/test_core_stanza_router.lua index a759ceec..59e68b91 100644 --- a/tests/test_core_stanza_router.lua +++ b/tests/test_core_stanza_router.lua @@ -6,17 +6,19 @@ -- COPYING file in the source package for more information. -- +_G.prosody = { full_sessions = {}; bare_sessions = {}; hosts = {}; }; - -function core_process_stanza(core_process_stanza) +function core_process_stanza(core_process_stanza, u) + local stanza = require "util.stanza"; local s2sout_session = { to_host = "remotehost", from_host = "localhost", type = "s2sout" } local s2sin_session = { from_host = "remotehost", to_host = "localhost", type = "s2sin", hosts = { ["remotehost"] = { authed = true } } } local local_host_session = { host = "localhost", type = "local", s2sout = { ["remotehost"] = s2sout_session } } local local_user_session = { username = "user", host = "localhost", resource = "resource", full_jid = "user@localhost/resource", type = "c2s" } - local hosts = { - ["localhost"] = local_host_session; - } - + + _G.prosody.hosts["localhost"] = local_host_session; + _G.prosody.full_sessions["user@localhost/resource"] = local_user_session; + _G.prosody.bare_sessions["user@localhost"] = { sessions = { resource = local_user_session } }; + -- Test message routing local function test_message_full_jid() local env = testlib_new_env(); @@ -24,12 +26,14 @@ function core_process_stanza(core_process_stanza) local target_routed; - function env.core_route_stanza(p_origin, p_stanza) + function env.core_post_stanza(p_origin, p_stanza) assert_equal(p_origin, local_user_session, "origin of routed stanza is not correct"); assert_equal(p_stanza, msg, "routed stanza is not correct one: "..p_stanza:pretty_print()); target_routed = true; end + env.hosts = hosts; + env.prosody = { hosts = hosts }; setfenv(core_process_stanza, env); assert_equal(core_process_stanza(local_user_session, msg), nil, "core_process_stanza returned incorrect value"); assert_equal(target_routed, true, "stanza was not routed successfully"); @@ -41,11 +45,12 @@ function core_process_stanza(core_process_stanza) local target_routed; - function env.core_route_stanza(p_origin, p_stanza) + function env.core_post_stanza(p_origin, p_stanza) assert_equal(p_origin, local_user_session, "origin of routed stanza is not correct"); assert_equal(p_stanza, msg, "routed stanza is not correct one: "..p_stanza:pretty_print()); target_routed = true; end + env.hosts = hosts; setfenv(core_process_stanza, env); assert_equal(core_process_stanza(local_user_session, msg), nil, "core_process_stanza returned incorrect value"); @@ -58,14 +63,12 @@ function core_process_stanza(core_process_stanza) local target_handled; - function env.core_route_stanza(p_origin, p_stanza) - end - - function env.core_handle_stanza(p_origin, p_stanza) + function env.core_post_stanza(p_origin, p_stanza) assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct"); assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print()); target_handled = true; end + env.hosts = hosts; setfenv(core_process_stanza, env); assert_equal(core_process_stanza(local_user_session, msg), nil, "core_process_stanza returned incorrect value"); @@ -84,6 +87,8 @@ function core_process_stanza(core_process_stanza) target_routed = true; end + function env.core_post_stanza(...) env.core_route_stanza(...); end + env.hosts = hosts; setfenv(core_process_stanza, env); assert_equal(core_process_stanza(local_user_session, msg), nil, "core_process_stanza returned incorrect value"); @@ -102,6 +107,10 @@ function core_process_stanza(core_process_stanza) target_routed = true; end + function env.core_post_stanza(...) + env.core_route_stanza(...); + end + env.hosts = hosts; setfenv(core_process_stanza, env); assert_equal(core_process_stanza(local_user_session, msg), nil, "core_process_stanza returned incorrect value"); @@ -113,7 +122,7 @@ function core_process_stanza(core_process_stanza) local function test_iq_to_remote_server() local env = testlib_new_env(); - local msg = stanza.stanza("iq", { to = "remotehost", type = "chat" }):tag("body"):text("Hello world"); + local msg = stanza.stanza("iq", { to = "remotehost", type = "get", id = "id" }):tag("body"):text("Hello world"); local target_routed; @@ -123,8 +132,8 @@ function core_process_stanza(core_process_stanza) target_routed = true; end - function env.core_handle_stanza(p_origin, p_stanza) - + function env.core_post_stanza(...) + env.core_route_stanza(...); end env.hosts = hosts; @@ -135,7 +144,7 @@ function core_process_stanza(core_process_stanza) local function test_iq_error_to_local_user() local env = testlib_new_env(); - local msg = stanza.stanza("iq", { to = "user@localhost/resource", from = "user@remotehost", type = "error" }):tag("error", { type = 'cancel' }):tag("item-not-found", { xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' }); + local msg = stanza.stanza("iq", { to = "user@localhost/resource", from = "user@remotehost", type = "error", id = "id" }):tag("error", { type = 'cancel' }):tag("item-not-found", { xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' }); local target_routed; @@ -145,8 +154,8 @@ function core_process_stanza(core_process_stanza) target_routed = true; end - function env.core_handle_stanza(p_origin, p_stanza) - + function env.core_post_stanza(...) + env.core_route_stanza(...); end env.hosts = hosts; @@ -157,20 +166,16 @@ function core_process_stanza(core_process_stanza) local function test_iq_to_local_bare() local env = testlib_new_env(); - local msg = stanza.stanza("iq", { to = "user@localhost", from = "user@localhost", type = "get" }):tag("ping", { xmlns = "urn:xmpp:ping:0" }); + local msg = stanza.stanza("iq", { to = "user@localhost", from = "user@localhost", type = "get", id = "id" }):tag("ping", { xmlns = "urn:xmpp:ping:0" }); local target_handled; - function env.core_handle_stanza(p_origin, p_stanza) + function env.core_post_stanza(p_origin, p_stanza) assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct"); assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print()); target_handled = true; end - function env.core_route_stanza(p_origin, p_stanza) - - end - env.hosts = hosts; setfenv(core_process_stanza, env); assert_equal(core_process_stanza(local_user_session, msg), nil, "core_process_stanza returned incorrect value"); @@ -189,6 +194,7 @@ function core_process_stanza(core_process_stanza) end function core_route_stanza(core_route_stanza) + local stanza = require "util.stanza"; local s2sout_session = { to_host = "remotehost", from_host = "localhost", type = "s2sout" } local s2sin_session = { from_host = "remotehost", to_host = "localhost", type = "s2sin", hosts = { ["remotehost"] = { authed = true } } } local local_host_session = { host = "localhost", type = "local", s2sout = { ["remotehost"] = s2sout_session }, sessions = {} } @@ -204,7 +210,7 @@ function core_route_stanza(core_route_stanza) --package.loaded["core.usermanager"] = { user_exists = function (user, host) print("RAR!") return true or user == "user" and host == "localhost" and true; end }; local target_handled, target_replied; - function env.core_handle_stanza(p_origin, p_stanza) + function env.core_post_stanza(p_origin, p_stanza) target_handled = true; end @@ -222,5 +228,5 @@ function core_route_stanza(core_route_stanza) package.loaded["core.usermanager"] = nil; end - runtest(test_iq_result_to_offline_user, "iq type=result|error to an offline user are not replied to"); + --runtest(test_iq_result_to_offline_user, "iq type=result|error to an offline user are not replied to"); end diff --git a/tools/ejabberd2prosody.lua b/tools/ejabberd2prosody.lua index 4fef3f3a..7b19260d 100755 --- a/tools/ejabberd2prosody.lua +++ b/tools/ejabberd2prosody.lua @@ -9,9 +9,14 @@ +package.path = package.path ..";../?.lua"; + +if arg[0]:match("^./") then + package.path = package.path .. ";"..arg[0]:gsub("/ejabberd2prosody.lua$", "/?.lua"); +end + require "erlparse"; -package.path = package.path ..";../?.lua"; local serialize = require "util.serialization".serialize; local st = require "util.stanza"; package.loaded["util.logger"] = {init = function() return function() end; end} @@ -86,13 +91,24 @@ local filters = { local name = tuple[5]; local subscription = tuple[6]; local ask = tuple[7]; local groups = tuple[8]; if type(name) ~= type("") then name = nil; end - if ask == "none" then ask = nil; elseif ask == "out" then ask = "subscribe" elseif ask == "in" then + if ask == "none" then + ask = nil; + elseif ask == "out" then + ask = "subscribe" + elseif ask == "in" then + roster_pending(node, host, contact); + ask = nil; + elseif ask == "both" then roster_pending(node, host, contact); - return; - else error(ask) end + ask = "subscribe"; + else error("Unknown ask type: "..ask); end if subscription ~= "both" and subscription ~= "from" and subscription ~= "to" and subscription ~= "none" then error(subscription) end local item = {name = name, ask = ask, subscription = subscription, groups = {}}; - for _, g in ipairs(groups) do item.groups[g] = true; end + for _, g in ipairs(groups) do + if type(g) == "string" then + item.groups[g] = true; + end + end roster(node, host, contact, item); end; private_storage = function(tuple) diff --git a/tools/ejabberdsql2prosody.lua b/tools/ejabberdsql2prosody.lua index 4aace085..f652af5b 100644 --- a/tools/ejabberdsql2prosody.lua +++ b/tools/ejabberdsql2prosody.lua @@ -136,8 +136,8 @@ local function readFile(filename) while true do local tname, tuples = readInsert(); if tname then - if t[name] then - local t_name = t[name]; + if t[tname] then + local t_name = t[tname]; for i=1,#tuples do table.insert(t_name, tuples[i]); end @@ -284,6 +284,12 @@ function private_storage(node, host, xmlns, stanza) local ret, err = dm.store(node, host, "private", private); print("["..(err or "success").."] private: " ..node.."@"..host.." - "..xmlns); end +function offline_msg(node, host, t, stanza) + stanza.attr.stamp = os.date("!%Y-%m-%dT%H:%M:%SZ", t); + stanza.attr.stamp_legacy = os.date("!%Y%m%dT%H:%M:%S", t); + local ret, err = dm.list_append(node, host, "offline", st.preserialize(stanza)); + print("["..(err or "success").."] offline: " ..node.."@"..host.." - "..os.date("!%Y-%m-%dT%H:%M:%SZ", t)); +end for i, row in ipairs(t["rosterusers"] or NULL) do local node, contact = row.username, row.jid; local name = row.nick; @@ -321,5 +327,20 @@ for i, row in ipairs(t["vcard"] or NULL) do print("["..(err or "success").."] vCard: "..row.username.."@"..host); end for i, row in ipairs(t["private_storage"] or NULL) do - private_storage(row.username, host, row.namespace, st.preserialize(parse_xml(row.data))); + private_storage(row.username, host, row.namespace, parse_xml(row.data)); +end +table.sort(t["spool"] or NULL, function(a,b) return a.seq < b.seq; end); -- sort by sequence number, just in case +local time_offset = os.difftime(os.time(os.date("!*t")), os.time(os.date("*t"))) -- to deal with timezones +local date_parse = function(s) + local year, month, day, hour, min, sec = s:match("(....)-?(..)-?(..)T(..):(..):(..)"); + return os.time({year=year, month=month, day=day, hour=hour, min=min, sec=sec-time_offset}); +end +for i, row in ipairs(t["spool"] or NULL) do + local stanza = parse_xml(row.xml); + local last_child = stanza.tags[#stanza.tags]; + if not last_child or last_child ~= stanza[#stanza] then error("Last child of offline message is not a tag"); end + if last_child.name ~= "x" and last_child.attr.xmlns ~= "jabber:x:delay" then error("Last child of offline message is not a timestamp"); end + stanza[#stanza], stanza.tags[#stanza.tags] = nil, nil; + local t = date_parse(last_child.attr.stamp); + offline_msg(row.username, host, t, stanza); end diff --git a/tools/erlparse.lua b/tools/erlparse.lua index f2d410a3..bfec3b4d 100644 --- a/tools/erlparse.lua +++ b/tools/erlparse.lua @@ -45,16 +45,26 @@ local function isSpace(ch) return ch <= _space; end +local escapes = {["\\b"]="\b", ["\\d"]="\d", ["\\e"]="\e", ["\\f"]="\f", ["\\n"]="\n", ["\\r"]="\r", ["\\s"]="\s", ["\\t"]="\t", ["\\v"]="\v", ["\\\""]="\"", ["\\'"]="'", ["\\\\"]="\\"}; local function readString() read("\""); -- skip quote local slash = nil; local str = ""; while true do local ch = read(); - if ch == "\"" and not slash then break; end - str = str..ch; + if slash then + slash = slash..ch; + if not escapes[slash] then error("Unknown escape sequence: "..slash); end + str = str..escapes[slash]; + slash = nil; + elseif ch == "\"" then + break; + elseif ch == "\\" then + slash = ch; + else + str = str..ch; + end end - str = str:gsub("\\.", {["\\b"]="\b", ["\\d"]="\d", ["\\e"]="\e", ["\\f"]="\f", ["\\n"]="\n", ["\\r"]="\r", ["\\s"]="\s", ["\\t"]="\t", ["\\v"]="\v", ["\\\""]="\"", ["\\'"]="'", ["\\\\"]="\\"}); return str; end local function readAtom1() diff --git a/util-src/Makefile b/util-src/Makefile index 3b7ca7bc..6cee457b 100644 --- a/util-src/Makefile +++ b/util-src/Makefile @@ -24,21 +24,28 @@ clean: encodings.o: encodings.c $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o encodings.o encodings.c encodings.so: encodings.o - export MACOSX_DEPLOYMENT_TARGET="10.3"; $(LD) $(LFLAGS) -o encodings.so encodings.o -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) -lidn + MACOSX_DEPLOYMENT_TARGET="10.3"; export MACOSX_DEPLOYMENT_TARGET; + $(LD) $(LFLAGS) -o encodings.so encodings.o -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) -lidn hashes.o: hashes.c $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o hashes.o hashes.c hashes.so: hashes.o - export MACOSX_DEPLOYMENT_TARGET="10.3"; $(LD) $(LFLAGS) -o hashes.so hashes.o -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) -lcrypto + MACOSX_DEPLOYMENT_TARGET="10.3"; + export MACOSX_DEPLOYMENT_TARGET; + $(LD) $(LFLAGS) -o hashes.so hashes.o -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) -lcrypto pposix.o: pposix.c $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o pposix.o pposix.c pposix.so: pposix.o - export MACOSX_DEPLOYMENT_TARGET="10.3"; $(LD) $(LFLAGS) -o pposix.so pposix.o -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) + MACOSX_DEPLOYMENT_TARGET="10.3"; + export MACOSX_DEPLOYMENT_TARGET; + $(LD) $(LFLAGS) -o pposix.so pposix.o -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) lsignal.o: lsignal.c $(CC) $(CFLAGS) -I$(LUA_INCDIR) -c -o lsignal.o lsignal.c signal.so: lsignal.o - export MACOSX_DEPLOYMENT_TARGET="10.3"; $(LD) $(LFLAGS) -o signal.so lsignal.o + MACOSX_DEPLOYMENT_TARGET="10.3"; + export MACOSX_DEPLOYMENT_TARGET; + $(LD) $(LFLAGS) -o signal.so lsignal.o -L$(LUA_LIBDIR) -llua$(LUA_SUFFIX) diff --git a/util-src/Makefile.win b/util-src/Makefile.win index d76aaccb..7b141097 100644 --- a/util-src/Makefile.win +++ b/util-src/Makefile.win @@ -1,7 +1,7 @@ LUA_PATH=$(LUA_DEV) -IDN_PATH=.\libidn-1.9 -OPENSSL_PATH=.\openssl-0.9.8i +IDN_PATH=..\..\libidn-1.15 +OPENSSL_PATH=..\..\openssl-0.9.8k LUA_INCLUDE=$(LUA_PATH)\include LUA_LIB=$(LUA_PATH)\lib\lua5.1.lib @@ -12,18 +12,27 @@ IDN_INCLUDE2=$(IDN_PATH)\win32\include OPENSSL_LIB=$(OPENSSL_PATH)\out32dll\libeay32.lib OPENSSL_INCLUDE=$(OPENSSL_PATH)\include -all: encodings.dll hashes.dll +CL=cl /LD /MD /nologo -install: encodings.dll hashes.dll +all: encodings.dll hashes.dll windows.dll + +install: encodings.dll hashes.dll windows.dll copy /Y *.dll ..\util\ clean: - del encodings.dll encodings.exp encodings.lib encodings.obj - del hashes.dll hashes.exp hashes.lib hashes.obj + del encodings.dll encodings.exp encodings.lib encodings.obj encodings.dll.manifest + del hashes.dll hashes.exp hashes.lib hashes.obj hashes.dll.manifest + del windows.dll windows.exp windows.lib windows.obj windows.dll.manifest encodings.dll: encodings.c - cl /LD /nologo encodings.c /I"$(LUA_INCLUDE)" /I"$(IDN_INCLUDE1)" /I"$(IDN_INCLUDE2)" /link "$(LUA_LIB)" "$(IDN_LIB)" /export:luaopen_util_encodings + $(CL) encodings.c /I"$(LUA_INCLUDE)" /I"$(IDN_INCLUDE1)" /I"$(IDN_INCLUDE2)" /link "$(LUA_LIB)" "$(IDN_LIB)" /export:luaopen_util_encodings + del encodings.exp encodings.lib encodings.obj encodings.dll.manifest hashes.dll: hashes.c - cl /LD /nologo hashes.c /I"$(LUA_INCLUDE)" /I"$(OPENSSL_INCLUDE)" /link "$(LUA_LIB)" "$(OPENSSL_LIB)" /export:luaopen_util_hashes + $(CL) hashes.c /I"$(LUA_INCLUDE)" /I"$(OPENSSL_INCLUDE)" /link "$(LUA_LIB)" "$(OPENSSL_LIB)" /export:luaopen_util_hashes + del hashes.exp hashes.lib hashes.obj hashes.dll.manifest + +windows.dll: windows.c + $(CL) windows.c /I"$(LUA_INCLUDE)" /link "$(LUA_LIB)" dnsapi.lib /export:luaopen_util_windows + del windows.exp windows.lib windows.obj windows.dll.manifest diff --git a/util-src/encodings.c b/util-src/encodings.c index d7aabc14..5147512f 100644 --- a/util-src/encodings.c +++ b/util-src/encodings.c @@ -108,7 +108,6 @@ static int Lbase64_decode(lua_State *L) /** decode(s) */ break; } } - return 0; } static const luaL_Reg Reg_base64[] = @@ -125,9 +124,14 @@ static const luaL_Reg Reg_base64[] = static int stringprep_prep(lua_State *L, const Stringprep_profile *profile) { size_t len; - const char *s = luaL_checklstring(L, 1, &len); + const char *s; char string[1024]; int ret; + if(!lua_isstring(L, 1)) { + lua_pushnil(L); + return 1; + } + s = lua_tolstring(L, 1, &len); if (len >= 1024) { lua_pushnil(L); return 1; // TODO return error message @@ -163,6 +167,7 @@ static const luaL_Reg Reg_stringprep[] = /***************** IDNA *****************/ #include <idna.h> +#include <idn-free.h> static int Lidna_to_ascii(lua_State *L) /** idna.to_ascii(s) */ { @@ -172,11 +177,11 @@ static int Lidna_to_ascii(lua_State *L) /** idna.to_ascii(s) */ int ret = idna_to_ascii_8z(s, &output, 0); if (ret == IDNA_SUCCESS) { lua_pushstring(L, output); - if (output) free(output); + idn_free(output); return 1; } else { lua_pushnil(L); - if (output) free(output); + idn_free(output); return 1; // TODO return error message } } @@ -189,11 +194,11 @@ static int Lidna_to_unicode(lua_State *L) /** idna.to_unicode(s) */ int ret = idna_to_unicode_8z8z(s, &output, 0); if (ret == IDNA_SUCCESS) { lua_pushstring(L, output); - if (output) free(output); + idn_free(output); return 1; } else { lua_pushnil(L); - if (output) free(output); + idn_free(output); return 1; // TODO return error message } } diff --git a/util-src/lsignal.c b/util-src/lsignal.c index 158efcd6..80799e4a 100644 --- a/util-src/lsignal.c +++ b/util-src/lsignal.c @@ -301,7 +301,7 @@ static int l_raise(lua_State *L) return 1; } -#ifdef _POSIX_SOURCE +#if defined _POSIX_SOURCE || (defined(sun) || defined(__sun)) /* define some posix only functions */ diff --git a/util-src/pposix.c b/util-src/pposix.c index d27a84b1..94086ed6 100644 --- a/util-src/pposix.c +++ b/util-src/pposix.c @@ -91,10 +91,14 @@ static int lc_daemonize(lua_State *L) const char * const facility_strings[] = { "auth", +#if !(defined(sun) || defined(__sun)) "authpriv", +#endif "cron", "daemon", +#if !(defined(sun) || defined(__sun)) "ftp", +#endif "kern", "local0", "local1", @@ -113,10 +117,14 @@ const char * const facility_strings[] = { }; int facility_constants[] = { LOG_AUTH, +#if !(defined(sun) || defined(__sun)) LOG_AUTHPRIV, +#endif LOG_CRON, LOG_DAEMON, +#if !(defined(sun) || defined(__sun)) LOG_FTP, +#endif LOG_KERN, LOG_LOCAL0, LOG_LOCAL1, @@ -365,11 +373,13 @@ int string2resource(const char *s) { if (!strcmp(s, "CPU")) return RLIMIT_CPU; if (!strcmp(s, "DATA")) return RLIMIT_DATA; if (!strcmp(s, "FSIZE")) return RLIMIT_FSIZE; - if (!strcmp(s, "MEMLOCK")) return RLIMIT_MEMLOCK; if (!strcmp(s, "NOFILE")) return RLIMIT_NOFILE; + if (!strcmp(s, "STACK")) return RLIMIT_STACK; +#if !(defined(sun) || defined(__sun)) + if (!strcmp(s, "MEMLOCK")) return RLIMIT_MEMLOCK; if (!strcmp(s, "NPROC")) return RLIMIT_NPROC; if (!strcmp(s, "RSS")) return RLIMIT_RSS; - if (!strcmp(s, "STACK")) return RLIMIT_STACK; +#endif return -1; } @@ -453,12 +463,20 @@ int lc_getrlimit(lua_State *L) { return 3; } +void lc_abort(lua_State* L) +{ + abort(); +} + /* Register functions */ int luaopen_util_pposix(lua_State *L) { lua_newtable(L); + lua_pushcfunction(L, lc_abort); + lua_setfield(L, -2, "abort"); + lua_pushcfunction(L, lc_daemonize); lua_setfield(L, -2, "daemonize"); diff --git a/util-src/windows.c b/util-src/windows.c new file mode 100644 index 00000000..7fb96312 --- /dev/null +++ b/util-src/windows.c @@ -0,0 +1,45 @@ +
+#include <stdio.h>
+#include <windows.h>
+#include <windns.h>
+
+#include "lua.h"
+#include "lauxlib.h"
+
+static int Lget_nameservers(lua_State *L) {
+ char stack_buffer[1024]; // stack allocated buffer
+ IP4_ARRAY* ips = (IP4_ARRAY*) stack_buffer;
+ DWORD len = sizeof(stack_buffer);
+ DNS_STATUS status;
+
+ status = DnsQueryConfig(DnsConfigDnsServerList, FALSE, NULL, NULL, ips, &len);
+ if (status == 0) {
+ DWORD i;
+ lua_createtable(L, ips->AddrCount, 0);
+ for (i = 0; i < ips->AddrCount; i++) {
+ DWORD ip = ips->AddrArray[i];
+ char ip_str[16] = "";
+ sprintf_s(ip_str, sizeof(ip_str), "%d.%d.%d.%d", (ip >> 0) & 255, (ip >> 8) & 255, (ip >> 16) & 255, (ip >> 24) & 255);
+ lua_pushstring(L, ip_str);
+ lua_rawseti(L, -2, i+1);
+ }
+ return 1;
+ } else {
+ luaL_error(L, "DnsQueryConfig returned %d", status);
+ return 0; // unreachable, but prevents a compiler warning
+ }
+}
+
+static const luaL_Reg Reg[] =
+{
+ { "get_nameservers", Lget_nameservers },
+ { NULL, NULL }
+};
+
+LUALIB_API int luaopen_util_windows(lua_State *L) {
+ luaL_register(L, "windows", Reg);
+ lua_pushliteral(L, "version"); /** version */
+ lua_pushliteral(L, "-3.14");
+ lua_settable(L,-3);
+ return 1;
+}
diff --git a/util/array.lua b/util/array.lua index ce5ce077..686f55b1 100644 --- a/util/array.lua +++ b/util/array.lua @@ -6,9 +6,14 @@ -- COPYING file in the source package for more information. -- +local t_insert, t_sort, t_remove, t_concat + = table.insert, table.sort, table.remove, table.concat; + local array = {}; +local array_base = {}; +local array_methods = {}; +local array_mt = { __index = array_methods, __tostring = function (array) return array:concat(", "); end }; -local array_mt = { __index = array, __tostring = function (array) return array:concat(", "); end }; local function new_array(_, t) return setmetatable(t or {}, array_mt); end @@ -20,36 +25,47 @@ end setmetatable(array, { __call = new_array }); -function array:map(func, t2) - local t2 = t2 or array{}; - for k,v in ipairs(self) do - t2[k] = func(v); +function array_base.map(outa, ina, func) + for k,v in ipairs(ina) do + outa[k] = func(v); end - return t2; + return outa; end -function array:filter(func, t2) - local t2 = t2 or array{}; - for k,v in ipairs(self) do +function array_base.filter(outa, ina, func) + local inplace, start_length = ina == outa, #ina; + local write = 1; + for read=1,start_length do + local v = ina[read]; if func(v) then - t2:push(v); + outa[write] = v; + write = write + 1; + end + end + + if inplace and write <= start_length then + for i=write,start_length do + outa[i] = nil; end end - return t2; + + return outa; end +function array_base.sort(outa, ina, ...) + if ina ~= outa then + outa:append(ina); + end + t_sort(outa, ...); + return outa; +end -array.push = table.insert; -array.pop = table.remove; -array.sort = table.sort; -array.concat = table.concat; -array.length = function (t) return #t; end - -function array:random() +--- These methods only mutate +function array_methods:random() return self[math.random(1,#self)]; end -function array:shuffle() +function array_methods:shuffle(outa, ina) local len = #self; for i=1,#self do local r = math.random(i,len); @@ -58,7 +74,7 @@ function array:shuffle() return self; end -function array:reverse() +function array_methods:reverse() local len = #self-1; for i=len,1,-1 do self:push(self[i]); @@ -67,7 +83,7 @@ function array:reverse() return self; end -function array:append(array) +function array_methods:append(array) local len,len2 = #self, #array; for i=1,len2 do self[len+i] = array[i]; @@ -75,6 +91,12 @@ function array:append(array) return self; end +array_methods.push = table.insert; +array_methods.pop = table.remove; +array_methods.concat = table.concat; +array_methods.length = function (t) return #t; end + +--- These methods always create a new array function array.collect(f, s, var) local t, var = {}; while true do @@ -85,6 +107,22 @@ function array.collect(f, s, var) return setmetatable(t, array_mt); end +--- + +-- Setup methods from array_base +for method, f in pairs(array_base) do + local base_method = f; + -- Setup global array method which makes new array + array[method] = function (old_a, ...) + local a = new_array(); + return base_method(a, old_a, ...); + end + -- Setup per-array (mutating) method + array_methods[method] = function (self, ...) + return base_method(self, self, ...); + end +end + _G.array = array; module("array"); diff --git a/util/dataforms.lua b/util/dataforms.lua index ed62f9b1..5626172e 100644 --- a/util/dataforms.lua +++ b/util/dataforms.lua @@ -10,7 +10,6 @@ local setmetatable = setmetatable; local pairs, ipairs = pairs, ipairs; local tostring, type = tostring, type; local t_concat = table.concat; - local st = require "util.stanza"; module "dataforms" @@ -37,34 +36,44 @@ function form_t.form(layout, data) -- Add field tag form:tag("field", { type = field_type, var = field.name, label = field.label }); - local value = data[field.name] or field.value; + local value = (data and data[field.name]) or field.value; - -- Add value, depending on type - if field_type == "hidden" then - if type(value) == "table" then - -- Assume an XML snippet - form:tag("value") - :add_child(value) - :up(); - elseif value then - form:tag("value"):text(tostring(value)):up(); - end - elseif field_type == "boolean" then - form:tag("value"):text((value and "1") or "0"):up(); - elseif field_type == "fixed" then - - elseif field_type == "jid-multi" then - for _, jid in ipairs(value) do - form:tag("value"):text(jid):up(); - end - elseif field_type == "jid-single" then - form:tag("value"):text(value):up(); - elseif field_type == "text-single" or field_type == "text-private" then - form:tag("value"):text(value):up(); - elseif field_type == "text-multi" then - -- Split into multiple <value> tags, one for each line - for line in value:gmatch("([^\r\n]+)\r?\n*") do - form:tag("value"):text(line):up(); + if value then + -- Add value, depending on type + if field_type == "hidden" then + if type(value) == "table" then + -- Assume an XML snippet + form:tag("value") + :add_child(value) + :up(); + else + form:tag("value"):text(tostring(value)):up(); + end + elseif field_type == "boolean" then + form:tag("value"):text((value and "1") or "0"):up(); + elseif field_type == "fixed" then + + elseif field_type == "jid-multi" then + for _, jid in ipairs(value) do + form:tag("value"):text(jid):up(); + end + elseif field_type == "jid-single" then + form:tag("value"):text(value):up(); + elseif field_type == "text-single" or field_type == "text-private" then + form:tag("value"):text(value):up(); + elseif field_type == "text-multi" then + -- Split into multiple <value> tags, one for each line + for line in value:gmatch("([^\r\n]+)\r?\n*") do + form:tag("value"):text(line):up(); + end + elseif field_type == "list-single" then + for _, val in ipairs(value) do + if type(val) == "table" then + form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up(); + else + form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up(); + end + end end end @@ -106,6 +115,20 @@ field_readers["text-single"] = field_readers["text-private"] = field_readers["text-single"]; +field_readers["jid-single"] = + field_readers["text-single"]; + +field_readers["jid-multi"] = + function (field_tag) + local result = {}; + for value_tag in field_tag:childtags() do + if value_tag.name == "value" then + result[#result+1] = value_tag[1]; + end + end + return result; + end + field_readers["text-multi"] = function (field_tag) local result = {}; @@ -117,6 +140,9 @@ field_readers["text-multi"] = return t_concat(result, "\n"); end +field_readers["list-single"] = + field_readers["text-single"]; + field_readers["boolean"] = function (field_tag) local value = field_tag:child_with_name("value"); diff --git a/util/datamanager.lua b/util/datamanager.lua index dcc35bb5..4d07d6cc 100644 --- a/util/datamanager.lua +++ b/util/datamanager.lua @@ -137,7 +137,7 @@ function store(username, host, datastore, data) append(f, data); f:close(); if next(data) == nil then -- try to delete empty datastore - log("debug", "Removing empty %s datastore for user %s@%s", datastore, username, host); + log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil"); os_remove(getpath(username, host, datastore)); end -- we write data even when we are deleting because lua doesn't have a @@ -179,7 +179,7 @@ function list_store(username, host, datastore, data) end f:close(); if next(data) == nil then -- try to delete empty datastore - log("debug", "Removing empty %s datastore for user %s@%s", datastore, username, host); + log("debug", "Removing empty %s datastore for user %s@%s", datastore, username or "nil", host or "nil"); os_remove(getpath(username, host, datastore, "list")); end -- we write data even when we are deleting because lua doesn't have a diff --git a/util/discohelper.lua b/util/discohelper.lua deleted file mode 100644 index 5d9bf287..00000000 --- a/util/discohelper.lua +++ /dev/null @@ -1,89 +0,0 @@ --- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - - - -local t_insert = table.insert; -local jid_split = require "util.jid".split; -local ipairs = ipairs; -local st = require "util.stanza"; - -module "discohelper"; - -local function addDiscoItemsHandler(self, jid, func) - if self.item_handlers[jid] then - t_insert(self.item_handlers[jid], func); - else - self.item_handlers[jid] = {func}; - end -end - -local function addDiscoInfoHandler(self, jid, func) - if self.info_handlers[jid] then - t_insert(self.info_handlers[jid], func); - else - self.info_handlers[jid] = {func}; - end -end - -local function handle(self, stanza) - if stanza.name == "iq" and stanza.tags[1].name == "query" then - local query = stanza.tags[1]; - local to = stanza.attr.to; - local from = stanza.attr.from - local node = query.attr.node or ""; - local to_node, to_host = jid_split(to); - - local reply = st.reply(stanza):query(query.attr.xmlns); - local handlers; - if query.attr.xmlns == "http://jabber.org/protocol/disco#info" then -- select handler set - handlers = self.info_handlers; - elseif query.attr.xmlns == "http://jabber.org/protocol/disco#items" then - handlers = self.item_handlers; - end - local handler; - local found; -- to keep track of any handlers found - if to_node then -- handlers which get called always - handler = handlers["*node"]; - else - handler = handlers["*host"]; - end - if handler then -- call always called handler - for _, h in ipairs(handler) do - if h(reply, to, from, node) then found = true; end - end - end - handler = handlers[to]; -- get the handler - if not handler then -- if not found then use default handler - if to_node then - handler = handlers["*defaultnode"]; - else - handler = handlers["*defaulthost"]; - end - end - if handler then - for _, h in ipairs(handler) do - if h(reply, to, from, node) then found = true; end - end - end - if found then return reply; end -- return the reply if there was one - return st.error_reply(stanza, "cancel", "service-unavailable"); - end -end - -function new() - return { - item_handlers = {}; - info_handlers = {}; - addDiscoItemsHandler = addDiscoItemsHandler; - addDiscoInfoHandler = addDiscoInfoHandler; - handle = handle; - }; -end - -return _M; diff --git a/util/helpers.lua b/util/helpers.lua index 80f72b3b..e69f1d98 100644 --- a/util/helpers.lua +++ b/util/helpers.lua @@ -1,3 +1,10 @@ +-- 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. +-- module("helpers", package.seeall); @@ -14,6 +21,7 @@ function log_events(events, name, logger) name = name or tostring(events); function events.fire_event(event, ...) logger("debug", "%s firing event: %s", name, event); + return f(event, ...); end events[events.fire_event] = f; return events; @@ -23,4 +31,13 @@ function revert_log_events(events) events.fire_event, events[events.fire_event] = events[events.fire_event], nil; -- :) end +function get_upvalue(f, get_name) + local i, name, value = 0; + repeat + i = i + 1; + name, value = debug.getupvalue(f, i); + until name == get_name or name == nil; + return value; +end + return _M; diff --git a/util/iterators.lua b/util/iterators.lua index 08bb729c..ba33bc80 100644 --- a/util/iterators.lua +++ b/util/iterators.lua @@ -78,6 +78,39 @@ function count(f, s, var) return x; end +-- Return the first n items an iterator returns +function head(n, f, s, var) + local c = 0; + return function (s, var) + if c >= n then + return nil; + end + c = c + 1; + return f(s, var); + end, s; +end + +function tail(n, f, s, var) + local results, count = {}, 0; + while true do + local ret = { f(s, var) }; + var = ret[1]; + if var == nil then break; end + results[(count%n)+1] = ret; + count = count + 1; + end + + if n > count then n = count; end + + local pos = 0; + return function () + pos = pos + 1; + if pos > n then return nil; end + return unpack(results[((count-1+pos)%n)+1]); + end + --return reverse(head(n, reverse(f, s, var))); +end + -- Convert the values returned by an iterator to an array function it2array(f, s, var) local t, var = {}; diff --git a/util/muc.lua b/util/muc.lua deleted file mode 100644 index badcffce..00000000 --- a/util/muc.lua +++ /dev/null @@ -1,418 +0,0 @@ --- Prosody IM --- Copyright (C) 2008-2009 Matthew Wild --- Copyright (C) 2008-2009 Waqas Hussain --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -local datamanager = require "util.datamanager"; -local datetime = require "util.datetime"; - -local jid_split = require "util.jid".split; -local jid_bare = require "util.jid".bare; -local st = require "util.stanza"; -local log = require "util.logger".init("mod_muc"); -local multitable_new = require "util.multitable".new; -local t_insert, t_remove = table.insert, table.remove; - -local muc_domain = nil; --module:get_host(); -local history_length = 20; - ------------- -local function filter_xmlns_from_array(array, filters) - local count = 0; - for i=#array,1,-1 do - local attr = array[i].attr; - if filters[attr and attr.xmlns] then - t_remove(array, i); - count = count + 1; - end - end - return count; -end -local function filter_xmlns_from_stanza(stanza, filters) - if filters then - if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then - return stanza, filter_xmlns_from_array(stanza, filters); - end - end - return stanza, 0; -end -local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; -local function get_filtered_presence(stanza) - return filter_xmlns_from_stanza(st.clone(stanza), presence_filters); -end -local kickable_error_conditions = { - ["gone"] = true; - ["internal-server-error"] = true; - ["item-not-found"] = true; - ["jid-malformed"] = true; - ["recipient-unavailable"] = true; - ["redirect"] = true; - ["remote-server-not-found"] = true; - ["remote-server-timeout"] = true; - ["service-unavailable"] = true; -}; -local function get_kickable_error(stanza) - for _, tag in ipairs(stanza.tags) do - if tag.name == "error" and tag.attr.xmlns == "jabber:client" then - for _, cond in ipairs(tag.tags) do - if cond.attr.xmlns == "urn:ietf:params:xml:ns:xmpp-stanzas" then - return kickable_error_conditions[cond.name] and cond.name; - end - end - return true; -- malformed error message - end - end - return true; -- malformed error message -end -local function getUsingPath(stanza, path, getText) - local tag = stanza; - for _, name in ipairs(path) do - if type(tag) ~= 'table' then return; end - tag = tag:child_with_name(name); - end - if tag and getText then tag = table.concat(tag); end - return tag; -end -local function getTag(stanza, path) return getUsingPath(stanza, path); end -local function getText(stanza, path) return getUsingPath(stanza, path, true); end ------------ - ---[[function get_room_disco_info(room, stanza) - return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info") - :tag("identity", {category='conference', type='text', name=room._data["name"]):up() - :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply -end -function get_room_disco_items(room, stanza) - return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); -end -- TODO allow non-private rooms]] - --- - -local function room_broadcast_presence(room, stanza, code, nick) - stanza = get_filtered_presence(stanza); - local data = room._participants[stanza.attr.from]; - stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) - :tag("item", {affiliation=data.affiliation, role=data.role, nick=nick}):up(); - if code then - stanza:tag("status", {code=code}):up(); - end - local me; - for occupant, o_data in pairs(room._participants) do - if occupant ~= stanza.attr.from then - for jid in pairs(o_data.sessions) do - stanza.attr.to = jid; - room:route_stanza(stanza); - end - else - me = o_data; - end - end - if me then - stanza:tag("status", {code='110'}); - for jid in pairs(me.sessions) do - stanza.attr.to = jid; - room:route_stanza(stanza); - end - end -end -local function room_broadcast_message(room, stanza, historic) - for occupant, o_data in pairs(room._participants) do - for jid in pairs(o_data.sessions) do - stanza.attr.to = jid; - room:route_stanza(stanza); - end - end - if historic then -- add to history - local history = room._data['history']; - if not history then history = {}; room._data['history'] = history; end - -- stanza = st.clone(stanza); - stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203 - stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) - t_insert(history, st.clone(st.preserialize(stanza))); - while #history > history_length do t_remove(history, 1) end - end -end - - -local function room_send_occupant_list(room, to) - local current_nick = room._jid_nick[to]; - for occupant, o_data in pairs(room._participants) do - if occupant ~= current_nick then - local pres = get_filtered_presence(o_data.sessions[o_data.jid]); - pres.attr.to, pres.attr.from = to, occupant; - pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) - :tag("item", {affiliation=o_data.affiliation, role=o_data.role}):up(); - room:route_stanza(pres); - end - end -end -local function room_send_history(room, to) - local history = room._data['history']; -- send discussion history - if history then - for _, msg in ipairs(history) do - msg = st.deserialize(msg); - msg.attr.to=to; - room:route_stanza(msg); - end - end - if room._data['subject'] then - room:route_stanza(st.message({type='groupchat', from=room.jid, to=to}):tag("subject"):text(room._data['subject'])); - end -end - -local function room_get_disco_info(self, stanza) end -local function room_get_disco_items(self, stanza) end -local function room_set_subject(room, current_nick, subject) - -- TODO check nick's authority - if subject == "" then subject = nil; end - room._data['subject'] = subject; - local msg = st.message({type='groupchat', from=current_nick}) - :tag('subject'):text(subject):up(); - room_broadcast_message(room, msg, false); - return true; -end - -local function room_handle_to_occupant(self, origin, stanza) -- PM, vCards, etc - local from, to = stanza.attr.from, stanza.attr.to; - local room = jid_bare(to); - local current_nick = self._jid_nick[from]; - local type = stanza.attr.type; - log("debug", "room: %s, current_nick: %s, stanza: %s", room or "nil", current_nick or "nil", stanza:top_tag()); - if (select(2, jid_split(from)) == muc_domain) then error("Presence from the MUC itself!!!"); end - if stanza.name == "presence" then - local pr = get_filtered_presence(stanza); - pr.attr.from = current_nick; - if type == "error" then -- error, kick em out! - if current_nick then - log("debug", "kicking %s from %s", current_nick, room); - room_handle_to_occupant(self, origin, st.presence({type='unavailable', from=from, to=to}):tag('status'):text('This participant is kicked from the room because he sent an error presence')); -- send unavailable - end - elseif type == "unavailable" then -- unavailable - if current_nick then - log("debug", "%s leaving %s", current_nick, room); - local data = self._participants[current_nick]; - data.role = 'none'; - room_broadcast_presence(self, pr); - self._participants[current_nick] = nil; - self._jid_nick[from] = nil; - end - elseif not type then -- available - if current_nick then - --if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence - if current_nick == to then -- simple presence - log("debug", "%s broadcasted presence", current_nick); - self._participants[current_nick].sessions[from] = pr; - room_broadcast_presence(self, pr); - else -- change nick - if self._participants[to] then - log("debug", "%s couldn't change nick", current_nick); - origin.send(st.error_reply(stanza, "cancel", "conflict"):tag("x", {xmlns = "http://jabber.org/protocol/muc"})); - else - local data = self._participants[current_nick]; - local to_nick = select(3, jid_split(to)); - if to_nick then - log("debug", "%s (%s) changing nick to %s", current_nick, data.jid, to); - local p = st.presence({type='unavailable', from=current_nick}); - room_broadcast_presence(self, p, '303', to_nick); - self._participants[current_nick] = nil; - self._participants[to] = data; - self._jid_nick[from] = to; - pr.attr.from = to; - self._participants[to].sessions[from] = pr; - room_broadcast_presence(self, pr); - else - --TODO malformed-jid - end - end - end - --else -- possible rejoin - -- log("debug", "%s had connection replaced", current_nick); - -- handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}):tag('status'):text('Replaced by new connection'):up()); -- send unavailable - -- handle_to_occupant(origin, stanza); -- resend available - --end - else -- enter room - local new_nick = to; - if self._participants[to] then - new_nick = nil; - end - if not new_nick then - log("debug", "%s couldn't join due to nick conflict: %s", from, to); - origin.send(st.error_reply(stanza, "cancel", "conflict"):tag("x", {xmlns = "http://jabber.org/protocol/muc"})); - else - log("debug", "%s joining as %s", from, to); - local data; --- if not rooms:get(room) and not rooms_info:get(room) then -- new room --- rooms_info:set(room, 'name', (jid_split(room))); --- data = {affiliation='owner', role='moderator', jid=from, sessions={[from]=get_filtered_presence(stanza)}}; --- end - if not data then -- new occupant - data = {affiliation='none', role='participant', jid=from, sessions={[from]=get_filtered_presence(stanza)}}; - end - self._participants[to] = data; - self._jid_nick[from] = to; - room_send_occupant_list(self, from); - pr.attr.from = to; - room_broadcast_presence(self, pr); - room_send_history(self, from); - end - end - elseif type ~= 'result' then -- bad type - origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error? - end - elseif not current_nick and type ~= "error" and type ~= "result" then -- not in room - origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); - elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM - origin.send(st.error_reply(stanza, "modify", "bad-request")); - elseif stanza.name == "message" and type == "error" and get_kickable_error(stanza) then - log("debug", "%s kicked from %s for sending an error message", current_nick, room); - room_handle_to_occupant(self, origin, st.presence({type='unavailable', from=from, to=to}):tag('status'):text('This participant is kicked from the room because he sent an error message to another occupant')); -- send unavailable - else -- private stanza - local o_data = self._participants[to]; - if o_data then - log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid); - local jid = o_data.jid; - -- TODO if stanza.name=='iq' and type=='get' and stanza.tags[1].attr.xmlns == 'vcard-temp' then jid = jid_bare(jid); end - stanza.attr.to, stanza.attr.from = jid, current_nick; - self:route_stanza(stanza); - elseif type ~= "error" and type ~= "result" then -- recipient not in room - origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room")); - end - end -end - -local function room_handle_to_room(self, origin, stanza) -- presence changes and groupchat messages, along with disco/etc - local type = stanza.attr.type; - if stanza.name == "iq" and type == "get" then -- disco requests - local xmlns = stanza.tags[1].attr.xmlns; - if xmlns == "http://jabber.org/protocol/disco#info" then - origin.send(room_get_disco_info(self, stanza)); - elseif xmlns == "http://jabber.org/protocol/disco#items" then - origin.send(room_get_disco_items(self, stanza)); - else - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); - end - elseif stanza.name == "message" and type == "groupchat" then - local from, to = stanza.attr.from, stanza.attr.to; - local room = jid_bare(to); - local current_nick = self._jid_nick[from]; - if not current_nick then -- not in room - origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); - else - local from = stanza.attr.from; - stanza.attr.from = current_nick; - local subject = getText(stanza, {"subject"}); - if subject then - self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza - else - room_broadcast_message(self, stanza, true); - end - end - elseif stanza.name == "presence" then -- hack - some buggy clients send presence updates to the room rather than their nick - local to = stanza.attr.to; - local current_nick = self._jid_nick[stanza.attr.from]; - if current_nick then - stanza.attr.to = current_nick; - room_handle_to_occupant(self, origin, stanza); - stanza.attr.to = to; - elseif type ~= "error" and type ~= "result" then - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); - end - elseif stanza.name == "message" and not stanza.attr.type and #stanza.tags == 1 and self._jid_nick[stanza.attr.from] - and stanza.tags[1].name == "x" and stanza.tags[1].attr.xmlns == "http://jabber.org/protocol/muc#user" and #stanza.tags[1].tags == 1 - and stanza.tags[1].tags[1].name == "invite" and stanza.tags[1].tags[1].attr.to then - local _from, _to = stanza.attr.from, stanza.attr.to; - local _invitee = stanza.tags[1].tags[1].attr.to; - stanza.attr.from, stanza.attr.to = _to, _invitee; - stanza.tags[1].tags[1].attr.from, stanza.tags[1].tags[1].attr.to = _from, nil; - self:route_stanza(stanza); - stanza.tags[1].tags[1].attr.from, stanza.tags[1].tags[1].attr.to = nil, _invitee; - stanza.attr.from, stanza.attr.to = _from, _to; - else - if type == "error" or type == "result" then return; end - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); - end -end - -local function room_handle_stanza(self, origin, stanza) - local to_node, to_host, to_resource = jid_split(stanza.attr.to); - if to_resource then - room_handle_to_occupant(self, origin, stanza); - else - room_handle_to_room(self, origin, stanza); - end -end - -module "muc" - -function new_room(jid) - return { - jid = jid; - handle_stanza = room_handle_stanza; - set_subject = room_set_subject; - route_stanza = function(room, stanza) end; -- Replace with a routing function, e.g., function(room, stanza) core_route_stanza(origin, stanza); end - _jid_nick = {}; - _participants = {}; - _data = {}; - } -end - -return _M; - ---[[function get_disco_info(stanza) - return st.iq({type='result', id=stanza.attr.id, from=muc_domain, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info") - :tag("identity", {category='conference', type='text', name=muc_name}):up() - :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply -end -function get_disco_items(stanza) - local reply = st.iq({type='result', id=stanza.attr.id, from=muc_domain, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); - for room in pairs(rooms_info:get()) do - reply:tag("item", {jid=room, name=rooms_info:get(room, "name")}):up(); - end - return reply; -- TODO cache disco reply -end]] - ---[[function handle_to_domain(origin, stanza) - local type = stanza.attr.type; - if type == "error" or type == "result" then return; end - if stanza.name == "iq" and type == "get" then - local xmlns = stanza.tags[1].attr.xmlns; - if xmlns == "http://jabber.org/protocol/disco#info" then - origin.send(get_disco_info(stanza)); - elseif xmlns == "http://jabber.org/protocol/disco#items" then - origin.send(get_disco_items(stanza)); - else - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc - end - else - origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it")); - end -end - -register_component(muc_domain, function(origin, stanza) - local to_node, to_host, to_resource = jid_split(stanza.attr.to); - if to_resource and not to_node then - if type == "error" or type == "result" then return; end - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- host/resource - elseif to_resource then - handle_to_occupant(origin, stanza); - elseif to_node then - handle_to_room(origin, stanza) - else -- to the main muc domain - if type == "error" or type == "result" then return; end - handle_to_domain(origin, stanza); - end -end);]] - ---[[module.unload = function() - deregister_component(muc_domain); -end -module.save = function() - return {rooms = rooms.data; jid_nick = jid_nick.data; rooms_info = rooms_info.data; persist_list = persist_list}; -end -module.restore = function(data) - rooms.data, jid_nick.data, rooms_info.data, persist_list = - data.rooms or {}, data.jid_nick or {}, data.rooms_info or {}, data.persist_list or {}; -end]] diff --git a/util/sasl.lua b/util/sasl.lua index c7aa050b..c8aa16a2 100644 --- a/util/sasl.lua +++ b/util/sasl.lua @@ -125,7 +125,7 @@ function method:process(message) end -- load the mechanisms -load_mechs = {"plain", "digest-md5"} +load_mechs = {"plain", "digest-md5", "anonymous"} for _, mech in ipairs(load_mechs) do local name = "util.sasl."..mech; local m = require(name); diff --git a/util/sasl/anonymous.lua b/util/sasl/anonymous.lua new file mode 100644 index 00000000..8f2a7708 --- /dev/null +++ b/util/sasl/anonymous.lua @@ -0,0 +1,35 @@ +-- sasl.lua v0.4 +-- Copyright (C) 2008-2009 Tobias Markmann +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +local s_match = string.match; + +local log = require "util.logger".init("sasl"); + +module "anonymous" + +--========================= +--SASL ANONYMOUS according to RFC 4505 +local function anonymous(self, message) + local username; + repeat + username = generate_uuid(); + until self.profile.anonymous(username, self.realm); + self["username"] = username; + return "success" +end + +function init(registerMechanism) + registerMechanism("ANONYMOUS", {"anonymous"}, anonymous); +end + +return _M;
\ No newline at end of file diff --git a/util/sasl/digest-md5.lua b/util/sasl/digest-md5.lua index ba042933..a4a4f811 100644 --- a/util/sasl/digest-md5.lua +++ b/util/sasl/digest-md5.lua @@ -175,7 +175,7 @@ local function digest(self, message) elseif state == false then return "failure", "account-disabled" end Y = md5(response["username"]..":"..response["realm"]..":"..password); elseif self.profile["digest-md5"] then - local Y, state = self.profile["digest-md5"](response["username"], self.realm, response["realm"] response["charset"]) + local Y, state = self.profile["digest-md5"](response["username"], self.realm, response["realm"], response["charset"]) if state == nil then return "failure", "not-authorized" elseif state == false then return "failure", "account-disabled" end elseif self.profile["digest-md5-test"] then @@ -186,12 +186,12 @@ local function digest(self, message) --elseif Y == false then return "failure", "account-disabled" end local A1 = ""; if response.authzid then - if response.authzid == self.username.."@"..self.realm then + if response.authzid == self.username or response.authzid == self.username.."@"..self.realm then -- COMPAT - log("warn", "Client is violating XMPP RFC. See section 6.1 of RFC 3920."); + log("warn", "Client is violating RFC 3920 (section 6.1, point 7)."); A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid; else - A1 = "?"; + return "failure", "invalid-authzid"; end else A1 = Y..":"..response["nonce"]..":"..response["cnonce"]; diff --git a/util/stanza.lua b/util/stanza.lua index e02136f2..d295d5cc 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -28,13 +28,19 @@ local s_find = string.find; local os = os; local do_pretty_printing = not os.getenv("WINDIR"); -local getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring; - -local log = require "util.logger".init("stanza"); +local getstyle, getstring; +if do_pretty_printing then + local ok, termcolours = pcall(require, "util.termcolours"); + if ok then + getstyle, getstring = termcolours.getstyle, termcolours.getstring; + else + do_pretty_printing = nil; + end +end module "stanza" -stanza_mt = {}; +stanza_mt = { __type = "stanza" }; stanza_mt.__index = stanza_mt; function stanza(name, attr) @@ -118,20 +124,23 @@ function stanza_mt:childtags() end -local xml_escape = (function() +local xml_escape +do local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; - return function(str) return (s_gsub(str, "['&<>\"]", escape_table)); end -end)(); -local function _dostring(t, buf, self, xml_escape) + function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end + _M.xml_escape = xml_escape; +end + +local function _dostring(t, buf, self, xml_escape, parentns) local nsid = 0; local name = t.name t_insert(buf, "<"..name); for k, v in pairs(t.attr) do - if s_find(k, "|", 1, true) then - local ns, attrk = s_match(k, "^([^|]+)|(.+)$"); + if s_find(k, "\1", 1, true) then + local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$"); nsid = nsid + 1; t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'"); - else + elseif not(k == "xmlns" and v == parentns) then t_insert(buf, " "..k.."='"..xml_escape(v).."'"); end end @@ -143,7 +152,7 @@ local function _dostring(t, buf, self, xml_escape) for n=1,len do local child = t[n]; if child.name then - self(child, buf, self, xml_escape); + self(child, buf, self, xml_escape, t.attr.xmlns); else t_insert(buf, xml_escape(child)); end @@ -153,7 +162,7 @@ local function _dostring(t, buf, self, xml_escape) end function stanza_mt.__tostring(t) local buf = {}; - _dostring(t, buf, _dostring, xml_escape); + _dostring(t, buf, _dostring, xml_escape, nil); return t_concat(buf); end @@ -201,6 +210,17 @@ function deserialize(stanza) if stanza then local attr = stanza.attr; for i=1,#attr do attr[i] = nil; end + local attrx = {}; + for att in pairs(attr) do + if s_find(att, "|", 1, true) and not s_find(k, "\1", 1, true) then + local ns,na = s_match(k, "^([^|]+)|(.+)$"); + attrx[ns.."\1"..na] = attr[att]; + attr[att] = nil; + end + end + for a,v in pairs(attrx) do + attr[x] = v; + end setmetatable(stanza, stanza_mt); for _, child in ipairs(stanza) do if type(child) == "table" then diff --git a/util/timer.lua b/util/timer.lua index 819016de..c0c7f25a 100644 --- a/util/timer.lua +++ b/util/timer.lua @@ -42,7 +42,7 @@ ns_addtimer(function() local t, func = d[1], d[2]; if t <= current_time then data[i] = nil; - local r = func(); + local r = func(current_time); if type(r) == "number" then _add_task(r, func); end end end |