diff options
Diffstat (limited to 'plugins')
26 files changed, 1541 insertions, 181 deletions
diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua new file mode 100644 index 00000000..3e3a705d --- /dev/null +++ b/plugins/adhoc/adhoc.lib.lua @@ -0,0 +1,85 @@ +-- Copyright (C) 2009-2010 Florian Zeitz +-- +-- This file is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local st, uuid = require "util.stanza", require "util.uuid"; + +local xmlns_cmd = "http://jabber.org/protocol/commands"; + +local states = {} + +local _M = {}; + +function _cmdtag(desc, status, sessionid, action) + local cmd = st.stanza("command", { xmlns = xmlns_cmd, node = desc.node, status = status }); + if sessionid then cmd.attr.sessionid = sessionid; end + if action then cmd.attr.action = action; end + + return cmd; +end + +function _M.new(name, node, handler, permission) + return { name = name, node = node, handler = handler, cmdtag = _cmdtag, permission = (permission or "user") }; +end + +function _M.handle_cmd(command, origin, stanza) + local sessionid = stanza.tags[1].attr.sessionid or uuid.generate(); + local dataIn = {}; + dataIn.to = stanza.attr.to; + dataIn.from = stanza.attr.from; + dataIn.action = stanza.tags[1].attr.action or "execute"; + dataIn.form = stanza.tags[1]:child_with_ns("jabber:x:data"); + + local data, state = command:handler(dataIn, states[sessionid]); + states[sessionid] = state; + local stanza = st.reply(stanza); + if data.status == "completed" then + states[sessionid] = nil; + cmdtag = command:cmdtag("completed", sessionid); + elseif data.status == "canceled" then + states[sessionid] = nil; + cmdtag = command:cmdtag("canceled", sessionid); + elseif data.status == "error" then + states[sessionid] = nil; + stanza = st.error_reply(stanza, data.error.type, data.error.condition, data.error.message); + origin.send(stanza); + return true; + else + cmdtag = command:cmdtag("executing", sessionid); + end + + for name, content in pairs(data) do + if name == "info" then + cmdtag:tag("note", {type="info"}):text(content):up(); + elseif name == "warn" then + cmdtag:tag("note", {type="warn"}):text(content):up(); + elseif name == "error" then + cmdtag:tag("note", {type="error"}):text(content.message):up(); + elseif name =="actions" then + local actions = st.stanza("actions"); + for _, action in ipairs(content) do + if (action == "prev") or (action == "next") or (action == "complete") then + actions:tag(action):up(); + else + module:log("error", 'Command "'..command.name.. + '" at node "'..command.node..'" provided an invalid action "'..action..'"'); + end + end + cmdtag:add_child(actions); + elseif name == "form" then + cmdtag:add_child(content:form()); + elseif name == "result" then + cmdtag:add_child(content.layout:form(content.data, "result")); + elseif name == "other" then + cmdtag:add_child(content); + end + end + stanza:add_child(cmdtag); + origin.send(stanza); + + return true; +end + +return _M; diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua new file mode 100644 index 00000000..6f91a61b --- /dev/null +++ b/plugins/adhoc/mod_adhoc.lua @@ -0,0 +1,76 @@ +-- Copyright (C) 2009 Thilo Cestonaro +-- +-- This file is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local st = require "util.stanza"; +local is_admin = require "core.usermanager".is_admin; +local adhoc_handle_cmd = module:require "adhoc".handle_cmd; +local xmlns_cmd = "http://jabber.org/protocol/commands"; +local xmlns_disco = "http://jabber.org/protocol/disco"; +local commands = {}; + +module:add_feature(xmlns_cmd); + +module:hook("iq/host/"..xmlns_disco.."#items:query", function (event) + local origin, stanza = event.origin, event.stanza; + -- TODO: Is this correct, or should is_admin be changed? + local privileged = is_admin(stanza.attr.from) + or is_admin(stanza.attr.from, stanza.attr.to); + if stanza.attr.type == "get" and stanza.tags[1].attr.node + and stanza.tags[1].attr.node == xmlns_cmd then + reply = st.reply(stanza); + reply:tag("query", { xmlns = xmlns_disco.."#items", + node = xmlns_cmd }); + for node, command in pairs(commands) do + if (command.permission == "admin" and privileged) + or (command.permission == "user") then + reply:tag("item", { name = command.name, + node = node, jid = module:get_host() }); + reply:up(); + end + end + origin.send(reply); + return true; + end +end, 500); + +module:hook("iq/host", function (event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "set" and stanza.tags[1] + and stanza.tags[1].name == "command" then + local node = stanza.tags[1].attr.node + -- TODO: Is this correct, or should is_admin be changed? + local privileged = is_admin(event.stanza.attr.from) + or is_admin(stanza.attr.from, stanza.attr.to); + if commands[node] then + if commands[node].permission == "admin" + and not privileged then + origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up() + :add_child(commands[node]:cmdtag("canceled") + :tag("note", {type="error"}):text("You don't have permission to execute this command"))); + return true + end + -- User has permission now execute the command + return adhoc_handle_cmd(commands[node], origin, stanza); + end + end +end, 500); + +local function handle_item_added(item) + commands[item.node] = item; +end + +module:hook("item-added/adhoc", function (event) + return handle_item_added(event.item); +end, 500); + +module:hook("item-removed/adhoc", function (event) + commands[event.item.node] = nil; +end, 500); + +-- Pick up any items that are already added +for _, item in ipairs(module:get_host_items("adhoc")) do + handle_item_added(item); +end diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua index d3017f6c..647c642a 100644 --- a/plugins/mod_announce.lua +++ b/plugins/mod_announce.lua @@ -11,6 +11,30 @@ local st, jid, set = require "util.stanza", require "util.jid", require "util.se local is_admin = require "core.usermanager".is_admin; local admins = set.new(config.get(module:get_host(), "core", "admins")); +function send_to_online(message, server) + if server then + sessions = { [server] = hosts[server] }; + else + sessions = hosts; + end + + local c = 0; + for hostname, host_session in pairs(sessions) do + if host_session.sessions then + message.attr.from = hostname; + for username in pairs(host_session.sessions) do + c = c + 1; + message.attr.to = username.."@"..hostname; + core_post_stanza(host_session, message); + end + end + end + + return c; +end + + +-- Old <message>-based jabberd-style announcement sending function handle_announcement(data) local origin, stanza = data.origin, data.stanza; local host, resource = select(2, jid.split(stanza.attr.to)); @@ -31,15 +55,47 @@ function handle_announcement(data) message.attr.type = "headline"; message.attr.from = host; - local c = 0; - for user in pairs(host_session.sessions) do - c = c + 1; - message.attr.to = user.."@"..host; - core_post_stanza(host_session, message); - end - + local c = send_to_online(message, host); module:log("info", "Announcement sent to %d online users", c); return true; end - module:hook("message/host", handle_announcement); + +-- Ad-hoc command (XEP-0133) +local dataforms_new = require "util.dataforms".new; +local announce_layout = dataforms_new{ + title = "Making an Announcement"; + instructions = "Fill out this form to make an announcement to all\nactive users of this service."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "subject", type = "text-single", label = "Subject" }; + { name = "announcement", type = "text-multi", required = true, label = "Announcement" }; +}; + +function announce_handler(self, data, state) + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + + local fields = announce_layout:data(data.form); + + module:log("info", "Sending server announcement to all online users"); + local message = st.message({type = "headline"}, fields.announcement):up() + :tag("subject"):text(fields.subject or "Announcement"); + + local count = send_to_online(message, data.to); + + module:log("info", "Announcement sent to %d online users", count); + return { status = "completed", info = ("Announcement sent to %d online users"):format(count) }; + else + return { status = "executing", form = announce_layout }, "executing"; + end + + return true; +end + +local adhoc_new = module:require "adhoc".new; +local announce_desc = adhoc_new("Send Announcement to Online Users", "http://jabber.org/protocol/admin#announce", announce_handler, "admin"); +module:add_item("adhoc", announce_desc); + diff --git a/plugins/mod_auth_anonymous.lua b/plugins/mod_auth_anonymous.lua new file mode 100644 index 00000000..214611d8 --- /dev/null +++ b/plugins/mod_auth_anonymous.lua @@ -0,0 +1,85 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local log = require "util.logger".init("usermanager"); +local type = type; +local ipairs = ipairs; +local jid_bare = require "util.jid".bare; +local config = require "core.configmanager"; +local new_sasl = require "util.sasl".new; +local datamanager = require "util.datamanager"; + +function new_default_provider(host) + local provider = { name = "anonymous" }; + + function provider.test_password(username, password) + return nil, "Password based auth not supported."; + end + + function provider.get_password(username) + return nil, "Password not available."; + end + + function provider.set_password(username, password) + return nil, "Password based auth not supported."; + end + + function provider.user_exists(username) + return nil, "Only anonymous users are supported."; -- FIXME check if anonymous user is connected? + end + + function provider.create_user(username, password) + return nil, "Account creation/modification not supported."; + end + + function provider.get_sasl_handler() + local realm = module:get_option("sasl_realm") or module.host; + local anonymous_authentication_profile = { + anonymous = function(username, realm) + return true; -- for normal usage you should always return true here + end + }; + return new_sasl(realm, anonymous_authentication_profile); + end + + function provider.is_admin(jid) + local admins = config.get(host, "core", "admins"); + if admins ~= config.get("*", "core", "admins") and type(admins) == "table" then + jid = jid_bare(jid); + for _,admin in ipairs(admins) do + if admin == jid then return true; end + end + elseif admins then + log("error", "Option 'admins' for host '%s' is not a table", host); + end + return is_admin(jid); -- Test whether it's a global admin instead + end + return provider; +end + +local function dm_callback(username, host, datastore, data) + if host == module.host then + return false; + end + return username, host, datastore, data; +end +local host = hosts[module.host]; +local _saved_disallow_s2s = host.disallow_s2s; +function module.load() + _saved_disallow_s2s = host.disallow_s2s; + host.disallow_s2s = module:get_option("disallow_s2s") ~= false; + datamanager.add_callback(dm_callback); +end +function module.unload() + host.disallow_s2s = _saved_disallow_s2s; + datamanager.remove_callback(dm_callback); +end + +module:add_item("auth-provider", new_default_provider(module.host)); + diff --git a/plugins/mod_auth_cyrus.lua b/plugins/mod_auth_cyrus.lua new file mode 100644 index 00000000..6b34a541 --- /dev/null +++ b/plugins/mod_auth_cyrus.lua @@ -0,0 +1,77 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local log = require "util.logger".init("usermanager"); +local type = type; +local ipairs = ipairs; +local jid_bare = require "util.jid".bare; +local config = require "core.configmanager"; + +local cyrus_service_realm = module:get_option("cyrus_service_realm"); +local cyrus_service_name = module:get_option("cyrus_service_name"); +local cyrus_application_name = module:get_option("cyrus_application_name"); + +prosody.unlock_globals(); --FIXME: Figure out why this is needed and + -- why cyrussasl isn't caught by the sandbox +local cyrus_new = require "util.sasl_cyrus".new; +prosody.lock_globals(); +local new_sasl = function(realm) + return cyrus_new( + cyrus_service_realm or realm, + cyrus_service_name or "xmpp", + cyrus_application_name or "prosody" + ); +end + +function new_default_provider(host) + local provider = { name = "cyrus" }; + log("debug", "initializing default authentication provider for host '%s'", host); + + function provider.test_password(username, password) + return nil, "Legacy auth not supported with Cyrus SASL."; + end + + function provider.get_password(username) + return nil, "Passwords unavailable for Cyrus SASL."; + end + + function provider.set_password(username, password) + return nil, "Passwords unavailable for Cyrus SASL."; + end + + function provider.user_exists(username) + return true; + end + + function provider.create_user(username, password) + return nil, "Account creation/modification not available with Cyrus SASL."; + end + + function provider.get_sasl_handler() + local realm = module:get_option("sasl_realm") or module.host; + return new_sasl(realm); + end + + function provider.is_admin(jid) + local admins = config.get(host, "core", "admins"); + if admins ~= config.get("*", "core", "admins") and type(admins) == "table" then + jid = jid_bare(jid); + for _,admin in ipairs(admins) do + if admin == jid then return true; end + end + elseif admins then + log("error", "Option 'admins' for host '%s' is not a table", host); + end + return is_admin(jid); -- Test whether it's a global admin instead + end + return provider; +end + +module:add_item("auth-provider", new_default_provider(module.host)); + diff --git a/plugins/mod_auth_internal.lua b/plugins/mod_auth_internal.lua new file mode 100644 index 00000000..42a792e2 --- /dev/null +++ b/plugins/mod_auth_internal.lua @@ -0,0 +1,110 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local datamanager = require "util.datamanager"; +local log = require "util.logger".init("usermanager"); +local type = type; +local error = error; +local ipairs = ipairs; +local hashes = require "util.hashes"; +local jid_bare = require "util.jid".bare; +local config = require "core.configmanager"; +local usermanager = require "core.usermanager"; +local new_sasl = require "util.sasl".new; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local hosts = hosts; + +local prosody = _G.prosody; + +local is_cyrus = usermanager.is_cyrus; + +function new_default_provider(host) + local provider = { name = "internal" }; + log("debug", "initializing default authentication provider for host '%s'", host); + + function provider.test_password(username, password) + log("debug", "test password '%s' for user %s at host %s", password, username, module.host); + if is_cyrus(host) then return nil, "Legacy auth not supported with Cyrus SASL."; end + local credentials = datamanager.load(username, host, "accounts") or {}; + + if password == credentials.password then + return true; + else + return nil, "Auth failed. Invalid username or password."; + end + end + + function provider.get_password(username) + log("debug", "get_password for username '%s' at host '%s'", username, module.host); + if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end + return (datamanager.load(username, host, "accounts") or {}).password; + end + + function provider.set_password(username, password) + if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end + local account = datamanager.load(username, host, "accounts"); + if account then + account.password = password; + return datamanager.store(username, host, "accounts", account); + end + return nil, "Account not available."; + end + + function provider.user_exists(username) + if is_cyrus(host) then return true; end + local account = datamanager.load(username, host, "accounts"); + if not account then + log("debug", "account not found for username '%s' at host '%s'", username, module.host); + return nil, "Auth failed. Invalid username"; + end + if account.password == nil or string.len(account.password) == 0 then + log("debug", "account password not set or zero-length for username '%s' at host '%s'", username, module.host); + return nil, "Auth failed. Password invalid."; + end + return true; + end + + function provider.create_user(username, password) + if is_cyrus(host) then return nil, "Account creation/modification not available with Cyrus SASL."; end + return datamanager.store(username, host, "accounts", {password = password}); + end + + function provider.get_sasl_handler() + local realm = module:get_option("sasl_realm") or module.host; + local getpass_authentication_profile = { + plain = function(username, realm) + 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 + }; + return new_sasl(realm, getpass_authentication_profile); + end + + function provider.is_admin(jid) + local admins = module:get_option_array("admins"); + if admins ~= config.get("*", "core", "admins") and type(admins) == "table" then + jid = jid_bare(jid); + for _,admin in ipairs(admins) do + if admin == jid then return true; end + end + end + end + return provider; +end + +module:add_item("auth-provider", new_default_provider(module.host)); + diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua new file mode 100644 index 00000000..3e5cb17a --- /dev/null +++ b/plugins/mod_auth_internal_hashed.lua @@ -0,0 +1,174 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local datamanager = require "util.datamanager"; +local log = require "util.logger".init("usermanager"); +local type = type; +local error = error; +local ipairs = ipairs; +local hashes = require "util.hashes"; +local jid_bare = require "util.jid".bare; +local getAuthenticationDatabaseSHA1 = require "util.sasl.scram".getAuthenticationDatabaseSHA1; +local config = require "core.configmanager"; +local usermanager = require "core.usermanager"; +local generate_uuid = require "util.uuid".generate; +local new_sasl = require "util.sasl".new; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local hosts = hosts; + +-- TODO: remove these two lines in near future +local hmac_sha1 = require "util.hmac".sha1; +local sha1 = require "util.hashes".sha1; + +local prosody = _G.prosody; + +local is_cyrus = usermanager.is_cyrus; + +-- Default; can be set per-user +local iteration_count = 4096; + +function new_hashpass_provider(host) + local provider = { name = "internal_hashed" }; + log("debug", "initializing hashpass authentication provider for host '%s'", host); + + function provider.test_password(username, password) + if is_cyrus(host) then return nil, "Legacy auth not supported with Cyrus SASL."; end + local credentials = datamanager.load(username, host, "accounts") or {}; + + if credentials.password ~= nil and string.len(credentials.password) ~= 0 then + if credentials.password ~= password then + return nil, "Auth failed. Provided password is incorrect."; + end + + if provider.set_password(username, credentials.password) == nil then + return nil, "Auth failed. Could not set hashed password from plaintext."; + else + return true; + end + end + + if credentials.iteration_count == nil or credentials.salt == nil or string.len(credentials.salt) == 0 then + return nil, "Auth failed. Stored salt and iteration count information is not complete."; + end + + -- convert hexpass to stored_key and server_key + -- TODO: remove this in near future + if credentials.hashpass then + local salted_password = credentials.hashpass:gsub("..", function(x) return string.char(tonumber(x, 16)); end); + credentials.stored_key = sha1(hmac_sha1(salted_password, "Client Key")):gsub(".", function (c) return ("%02x"):format(c:byte()); end); + credentials.server_key = hmac_sha1(salted_password, "Server Key"):gsub(".", function (c) return ("%02x"):format(c:byte()); end); + credentials.hashpass = nil + datamanager.store(username, host, "accounts", credentials); + end + + local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, credentials.salt, credentials.iteration_count); + + local stored_key_hex = stored_key:gsub(".", function (c) return ("%02x"):format(c:byte()); end); + local server_key_hex = server_key:gsub(".", function (c) return ("%02x"):format(c:byte()); end); + + if valid and stored_key_hex == credentials.stored_key and server_key_hex == credentials.server_key then + return true; + else + return nil, "Auth failed. Invalid username, password, or password hash information."; + end + end + + function provider.set_password(username, password) + if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end + local account = datamanager.load(username, host, "accounts"); + if account then + account.salt = account.salt or generate_uuid(); + account.iteration_count = account.iteration_count or iteration_count; + local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, account.salt, account.iteration_count); + local stored_key_hex = stored_key:gsub(".", function (c) return ("%02x"):format(c:byte()); end); + local server_key_hex = server_key:gsub(".", function (c) return ("%02x"):format(c:byte()); end); + + account.stored_key = stored_key_hex + account.server_key = server_key_hex + + account.password = nil; + return datamanager.store(username, host, "accounts", account); + end + return nil, "Account not available."; + end + + function provider.user_exists(username) + if is_cyrus(host) then return true; end + local account = datamanager.load(username, host, "accounts"); + if not account then + log("debug", "account not found for username '%s' at host '%s'", username, module.host); + return nil, "Auth failed. Invalid username"; + end + if (account.hashpass == nil or string.len(account.hashpass) == 0) and (account.password == nil or string.len(account.password) == 0) then + log("debug", "account password not set or zero-length for username '%s' at host '%s'", username, module.host); + return nil, "Auth failed. Password invalid."; + end + return true; + end + + function provider.create_user(username, password) + if is_cyrus(host) then return nil, "Account creation/modification not available with Cyrus SASL."; end + local salt = generate_uuid(); + local valid, stored_key, server_key = saltedPasswordSHA1(password, salt, iteration_count); + local stored_key_hex = stored_key:gsub(".", function (c) return ("%02x"):format(c:byte()); end); + local server_key_hex = server_key:gsub(".", function (c) return ("%02x"):format(c:byte()); end); + return datamanager.store(username, host, "accounts", {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = iteration_count}); + end + + function provider.get_sasl_handler() + local realm = module:get_option("sasl_realm") or module.host; + local testpass_authentication_profile = { + plain_test = function(username, password, realm) + local prepped_username = nodeprep(username); + if not prepped_username then + log("debug", "NODEprep failed on username: %s", username); + return "", nil; + end + return usermanager.test_password(prepped_username, password, realm), true; + end, + scram_sha_1 = function(username, realm) + local credentials = datamanager.load(username, host, "accounts") or {}; + if credentials.password then + usermanager.set_password(username, credentials.password, host); + credentials = datamanager.load(username, host, "accounts") or {}; + end + + -- convert hexpass to stored_key and server_key + -- TODO: remove this in near future + if credentials.hashpass then + local salted_password = credentials.hashpass:gsub("..", function(x) return string.char(tonumber(x, 16)); end); + credentials.stored_key = sha1(hmac_sha1(salted_password, "Client Key")):gsub(".", function (c) return ("%02x"):format(c:byte()); end); + credentials.server_key = hmac_sha1(salted_password, "Server Key"):gsub(".", function (c) return ("%02x"):format(c:byte()); end); + credentials.hashpass = nil + datamanager.store(username, host, "accounts", credentials); + end + + local stored_key, server_key, iteration_count, salt = credentials.stored_key, credentials.server_key, credentials.iteration_count, credentials.salt; + stored_key = stored_key and stored_key:gsub("..", function(x) return string.char(tonumber(x, 16)); end); + server_key = server_key and server_key:gsub("..", function(x) return string.char(tonumber(x, 16)); end); + return stored_key, server_key, iteration_count, salt, true; + end + }; + return new_sasl(realm, testpass_authentication_profile); + end + + function provider.is_admin(jid) + local admins = module:get_option_array("admins"); + if admins ~= config.get("*", "core", "admins") and type(admins) == "table" then + jid = jid_bare(jid); + for _,admin in ipairs(admins) do + if admin == jid then return true; end + end + end + end + return provider; +end + +module:add_item("auth-provider", new_hashpass_provider(module.host)); + diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua index 5ff4781a..9edee44a 100644 --- a/plugins/mod_compression.lua +++ b/plugins/mod_compression.lua @@ -14,6 +14,7 @@ local xmlns_compression_feature = "http://jabber.org/features/compress" local xmlns_compression_protocol = "http://jabber.org/protocol/compress" local xmlns_stream = "http://etherx.jabber.org/streams"; local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up(); +local add_filter = require "util.filters".add_filter; local compression_level = module:get_option("compression_level"); -- if not defined assume admin wants best compression @@ -94,43 +95,36 @@ end -- setup compression for a stream local function setup_compression(session, deflate_stream) - local old_send = (session.sends2s or session.send); - - local new_send = function(t) - --TODO: Better code injection in the sending process - 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", "%s", tostring(compressed)); - return; - end - session.conn:write(compressed); - end; - - if session.sends2s then session.sends2s = new_send - elseif session.send then session.send = new_send end + add_filter(session, "bytes/out", function(t) + local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync'); + if status == false then + module:log("warn", "%s", tostring(compressed)); + session:close({ + condition = "undefined-condition"; + text = compressed; + extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); + }); + return; + end + return compressed; + end); end -- setup decompression for a stream local function setup_decompression(session, inflate_stream) - 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", "%s", tostring(decompressed)); - return; - end - old_data(conn, decompressed); - end; + add_filter(session, "bytes/in", function(data) + local status, decompressed, eof = pcall(inflate_stream, data); + if status == false then + module:log("warn", "%s", tostring(decompressed)); + session:close({ + condition = "undefined-condition"; + text = decompressed; + extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); + }); + return; + end + return decompressed; + end); end module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compression_protocol, @@ -148,12 +142,6 @@ module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compressio -- setup decompression for session.data setup_decompression(session, inflate_stream); - local session_reset_stream = session.reset_stream; - session.reset_stream = function(session) - session_reset_stream(session); - setup_decompression(session, inflate_stream); - return true; - end; session:reset_stream(); local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams", ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host}; @@ -195,12 +183,6 @@ module:add_handler({"c2s_unauthed", "c2s", "s2sin_unauthed", "s2sin"}, "compress -- setup decompression for session.data setup_decompression(session, inflate_stream); - local session_reset_stream = session.reset_stream; - session.reset_stream = function(session) - session_reset_stream(session); - setup_decompression(session, inflate_stream); - return true; - end; session.compressed = true; elseif method then session.log("debug", "%s compression selected, but we don't support it.", tostring(method)); diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua index 5f821cbc..7a876f1d 100644 --- a/plugins/mod_groups.lua +++ b/plugins/mod_groups.lua @@ -29,6 +29,9 @@ function inject_roster_contacts(username, host, roster) if jid ~= bare_jid then if not roster[jid] then roster[jid] = {}; end roster[jid].subscription = "both"; + if groups[group_name][jid] then + roster[jid].name = groups[group_name][jid]; + end if not roster[jid].groups then roster[jid].groups = { [group_name] = true }; end @@ -100,10 +103,13 @@ function module.load() groups[curr_group] = groups[curr_group] or {}; else -- Add JID - local jid = jid_prep(line:match("%S+")); + local entryjid, name = line:match("([^=]*)=?(.*)"); + module:log("debug", "entryjid = '%s', name = '%s'", entryjid, name); + local jid; + jid = jid_prep(entryjid:match("%S+")); if jid then module:log("debug", "New member of %s: %s", tostring(curr_group), tostring(jid)); - groups[curr_group][jid] = true; + groups[curr_group][jid] = name or false; members[jid] = members[jid] or {}; members[jid][#members[jid]+1] = curr_group; end diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua index b3001fe5..e90af781 100644 --- a/plugins/mod_iq.lua +++ b/plugins/mod_iq.lua @@ -9,7 +9,6 @@ local st = require "util.stanza"; local jid_split = require "util.jid".split; -local user_exists = require "core.usermanager".user_exists; local full_sessions = full_sessions; local bare_sessions = bare_sessions; @@ -34,16 +33,6 @@ module:hook("iq/bare", function(data) -- IQ to bare JID recieved local origin, stanza = data.origin, data.stanza; - local to = stanza.attr.to; - if to and not bare_sessions[to] then -- quick check for account existance - local node, host = jid_split(to); - if not user_exists(node, host) then -- full check for account existance - if stanza.attr.type == "get" or stanza.attr.type == "set" then - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); - end - return true; - end - end -- TODO fire post processing events if stanza.attr.type == "get" or stanza.attr.type == "set" then return module:fire_event("iq/bare/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data); diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index 0134d736..50566fa6 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -50,7 +50,7 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:auth", username = nodeprep(username); resource = resourceprep(resource) local reply = st.reply(stanza); - if usermanager.validate_credentials(session.host, username, password) then + if usermanager.test_password(username, password, session.host) then -- Authentication successful! local success, err = sessionmanager.make_authenticated(session, username); if success then diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua new file mode 100644 index 00000000..f323e606 --- /dev/null +++ b/plugins/mod_motd.lua @@ -0,0 +1,25 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local host = module:get_host(); +local motd_text = module:get_option("motd_text") or "MOTD: (blank)"; +local motd_jid = module:get_option("motd_jid") or host; + +local st = require "util.stanza"; + +module:hook("resource-bind", + function (event) + local session = event.session; + local motd_stanza = + st.message({ to = session.username..'@'..session.host, from = motd_jid }) + :tag("body"):text(motd_text); + core_route_stanza(hosts[host], motd_stanza); + module:log("debug", "MOTD send to user %s@%s", session.username, session.host); + +end); diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua new file mode 100644 index 00000000..24aef9ed --- /dev/null +++ b/plugins/mod_offline.lua @@ -0,0 +1,56 @@ +-- 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 st = require "util.stanza"; +local datetime = require "util.datetime"; +local ipairs = ipairs; +local jid_split = require "util.jid".split; + +module:add_feature("msgoffline"); + +module:hook("message/offline/store", function(event) + local origin, stanza = event.origin, event.stanza; + local to = stanza.attr.to; + local node, host; + if to then + node, host = jid_split(to) + else + node, host = origin.username, origin.host; + end + + stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy(); + local result = datamanager.list_append(node, host, "offline", st.preserialize(stanza)); + stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil; + + return true; +end); + +module:hook("message/offline/broadcast", function(event) + local origin = event.origin; + local node, host = origin.username, origin.host; + + local data = datamanager.list_load(node, host, "offline"); + if not data then return true; end + for _, stanza in ipairs(data) do + stanza = st.deserialize(stanza); + stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = stanza.attr.stamp}):up(); -- XEP-0203 + stanza:tag("x", {xmlns = "jabber:x:delay", from = host, stamp = stanza.attr.stamp_legacy}):up(); -- XEP-0091 (deprecated) + stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil; + origin.send(stanza); + end + return true; +end); + +module:hook("message/offline/delete", function(event) + local origin = event.origin; + local node, host = origin.username, origin.host; + + return datamanager.list_store(node, host, "offline", nil); +end); diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index aa46d2d3..31546dff 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -16,7 +16,6 @@ local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed local pairs, ipairs = pairs, ipairs; local next = next; local type = type; -local load_roster = require "core.rostermanager".load_roster; local sha1 = require "util.hashes".sha1; local base64 = require "util.encodings".base64.encode; @@ -40,8 +39,8 @@ module:add_feature("http://jabber.org/protocol/pubsub#publish"); local function subscription_presence(user_bare, recipient) local recipient_bare = jid_bare(recipient); if (recipient_bare == user_bare) then return true end - local item = load_roster(jid_split(user_bare))[recipient_bare]; - return item and (item.subscription == 'from' or item.subscription == 'both'); + local username, host = jid_split(user_bare); + return is_contact_subscribed(username, host, recipient_bare); end local function publish(session, node, id, item) @@ -118,27 +117,32 @@ module:hook("presence/bare", function(event) -- inbound presence to bare JID recieved local origin, stanza = event.origin, event.stanza; local user = stanza.attr.to or (origin.username..'@'..origin.host); + local t = stanza.attr.type; - if not stanza.attr.to or subscription_presence(user, stanza.attr.from) then - local recipient = stanza.attr.from; - local current = recipients[user] and recipients[user][recipient]; - local hash = get_caps_hash_from_presence(stanza, current); - if current == hash then return; end - if not hash then - if recipients[user] then recipients[user][recipient] = nil; end - else - recipients[user] = recipients[user] or {}; - if hash_map[hash] then - recipients[user][recipient] = hash_map[hash]; - publish_all(user, recipient, origin); + if not t then -- available presence + if not stanza.attr.to or subscription_presence(user, stanza.attr.from) then + local recipient = stanza.attr.from; + local current = recipients[user] and recipients[user][recipient]; + local hash = get_caps_hash_from_presence(stanza, current); + if current == hash then return; end + if not hash then + if recipients[user] then recipients[user][recipient] = nil; end else - recipients[user][recipient] = hash; - origin.send( - st.stanza("iq", {from=stanza.attr.to, to=stanza.attr.from, id="disco", type="get"}) - :query("http://jabber.org/protocol/disco#info") - ); + recipients[user] = recipients[user] or {}; + if hash_map[hash] then + recipients[user][recipient] = hash_map[hash]; + publish_all(user, recipient, origin); + else + recipients[user][recipient] = hash; + origin.send( + st.stanza("iq", {from=stanza.attr.to, to=stanza.attr.from, id="disco", type="get"}) + :query("http://jabber.org/protocol/disco#info") + ); + end end end + elseif t == "unavailable" then + if recipients[user] then recipients[user][stanza.attr.from] = nil; end end end, 10); diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua index c38f7eba..52b1e0e6 100644 --- a/plugins/mod_posix.lua +++ b/plugins/mod_posix.lua @@ -54,16 +54,16 @@ module:add_event_hook("server-started", function () end); -- Don't even think about it! -module:add_event_hook("server-starting", function () - 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 +if not prosody.start_time then -- server-starting + 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 +end local pidfile; local pidfile_handle; @@ -141,7 +141,9 @@ if daemonize then write_pidfile(); end end - module:add_event_hook("server-starting", daemonize_server); + if not prosody.start_time then -- server-starting + daemonize_server(); + end else -- Not going to daemonize, so write the pid of this process write_pidfile(); diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua index 4fb8c3e4..3735cf1d 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -24,20 +24,6 @@ local rostermanager = require "core.rostermanager"; local sessionmanager = require "core.sessionmanager"; local offlinemanager = require "core.offlinemanager"; -local _core_route_stanza = core_route_stanza; -local core_route_stanza; -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 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 - end - _core_route_stanza(origin, stanza); -end - local function select_top_resources(user) local priority = 0; local recipients = {}; @@ -64,7 +50,7 @@ end local ignore_presence_priority = module:get_option("ignore_presence_priority"); -function handle_normal_presence(origin, stanza, core_route_stanza) +function handle_normal_presence(origin, stanza) if ignore_presence_priority then local priority = stanza:child_with_name("priority"); if priority and priority[1] ~= "0" then @@ -82,13 +68,13 @@ function handle_normal_presence(origin, stanza, core_route_stanza) for _, res in pairs(user and user.sessions or NULL) do -- broadcast to all resources if res ~= origin and res.presence then -- to resource stanza.attr.to = res.full_jid; - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza, true); end end for jid, item in pairs(roster) do -- broadcast to all interested contacts if item.subscription == "both" or item.subscription == "from" then stanza.attr.to = jid; - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza, true); end end if stanza.attr.type == nil and not origin.presence then -- initial presence @@ -97,13 +83,13 @@ function handle_normal_presence(origin, stanza, core_route_stanza) for jid, item in pairs(roster) do -- probe all contacts we are subscribed to if item.subscription == "both" or item.subscription == "to" then probe.attr.to = jid; - core_route_stanza(origin, probe); + core_post_stanza(origin, probe, true); end end for _, res in pairs(user and user.sessions or NULL) do -- broadcast from all available resources if res ~= origin and res.presence then res.presence.attr.to = origin.full_jid; - core_route_stanza(res, res.presence); + core_post_stanza(res, res.presence, true); res.presence.attr.to = nil; end end @@ -116,7 +102,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza) for jid, item in pairs(roster) do -- resend outgoing subscription requests if item.ask then request.attr.to = jid; - core_route_stanza(origin, request); + core_post_stanza(origin, request, true); end end local offline = offlinemanager.load(node, host); @@ -136,7 +122,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza) if origin.directed then for jid in pairs(origin.directed) do stanza.attr.to = jid; - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza, true); end origin.directed = nil; end @@ -159,7 +145,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, stanza) +function send_presence_of_available_resources(user, host, jid, recipient_session, stanza) local h = hosts[host]; local count = 0; if h and h.type == "local" then @@ -170,7 +156,7 @@ function send_presence_of_available_resources(user, host, jid, recipient_session if pres then if stanza then pres = stanza; pres.attr.from = session.full_jid; end pres.attr.to = jid; - core_route_stanza(session, pres); + core_post_stanza(session, pres, true); pres.attr.to = nil; count = count + 1; end @@ -181,26 +167,29 @@ function send_presence_of_available_resources(user, host, jid, recipient_session return count; end -function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza) +function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare) local node, host = jid_split(from_bare); 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); - if stanza.attr.type == "subscribe" then + if stanza.attr.type == "probe" then + stanza.attr.from, stanza.attr.to = st_from, st_to; + return; + elseif stanza.attr.type == "subscribe" then -- 1. route stanza -- 2. roster push (subscription = none, ask = subscribe) if rostermanager.set_contact_pending_out(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); end -- else file error - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza); elseif stanza.attr.type == "unsubscribe" then -- 1. route stanza -- 2. roster push (subscription = none or from) if rostermanager.unsubscribe(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed? end -- else file error - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza); elseif stanza.attr.type == "subscribed" then -- 1. route stanza -- 2. roster_push () @@ -208,20 +197,21 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_ if rostermanager.subscribed(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); end - core_route_stanza(origin, stanza); - send_presence_of_available_resources(node, host, to_bare, origin, core_route_stanza); + core_post_stanza(origin, stanza); + send_presence_of_available_resources(node, host, to_bare, origin); elseif stanza.attr.type == "unsubscribed" then -- 1. route stanza -- 2. roster push (subscription = none or to) if rostermanager.unsubscribed(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); end - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza); end stanza.attr.from, stanza.attr.to = st_from, st_to; + return true; end -function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza) +function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare) local node, host = jid_split(to_bare); local st_from, st_to = stanza.attr.from, stanza.attr.to; stanza.attr.from, stanza.attr.to = from_bare, to_bare; @@ -230,21 +220,21 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b if stanza.attr.type == "probe" then local result, err = rostermanager.is_contact_subscribed(node, host, from_bare); if result then - if 0 == send_presence_of_available_resources(node, host, st_from, origin, core_route_stanza) then - core_route_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"})); -- TODO send last activity + if 0 == send_presence_of_available_resources(node, host, st_from, origin) then + core_post_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"}), true); -- TODO send last activity end elseif not err then - core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"})); + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}), true); end elseif stanza.attr.type == "subscribe" then if rostermanager.is_contact_subscribed(node, host, from_bare) then - core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"})); -- already subscribed + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"}), true); -- already subscribed -- Sending presence is not clearly stated in the RFC, but it seems appropriate - if 0 == send_presence_of_available_resources(node, host, from_bare, origin, core_route_stanza) then - core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- TODO send last activity + if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- TODO send last activity end else - core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- acknowledging receipt + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt if not rostermanager.is_contact_pending_in(node, host, from_bare) then if rostermanager.set_contact_pending_in(node, host, from_bare) then sessionmanager.send_to_available_resources(node, host, stanza); @@ -268,6 +258,7 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b end end -- discard any other type stanza.attr.from, stanza.attr.to = st_from, st_to; + return true; end local outbound_presence_handler = function(data) @@ -278,12 +269,12 @@ local outbound_presence_handler = function(data) if to then local t = stanza.attr.type; if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes - handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); - return true; + return handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); end local to_bare = jid_bare(to); - if not(origin.roster[to_bare] and (origin.roster[to_bare].subscription == "both" or origin.roster[to_bare].subscription == "from")) then -- directed presence + local roster = origin.roster; + if roster and not(roster[to_bare] and (roster[to_bare].subscription == "both" or roster[to_bare].subscription == "from")) then -- directed presence origin.directed = origin.directed or {}; if t then -- removing from directed presence list on sending an error or unavailable origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to? @@ -306,8 +297,7 @@ module:hook("presence/bare", function(data) local t = stanza.attr.type; if to then if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID - handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); - return true; + return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); end local user = bare_sessions[to]; @@ -319,7 +309,7 @@ module:hook("presence/bare", function(data) end end -- no resources not online, discard elseif not t or t == "unavailable" then - handle_normal_presence(origin, stanza, core_route_stanza); + handle_normal_presence(origin, stanza); end return true; end); @@ -329,8 +319,7 @@ module:hook("presence/full", function(data) local t = stanza.attr.type; if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID - handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); - return true; + return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); end local session = full_sessions[stanza.attr.to]; @@ -347,10 +336,10 @@ module:hook("presence/host", function(data) local from_bare = jid_bare(stanza.attr.from); local t = stanza.attr.type; if t == "probe" then - core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); + core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); elseif t == "subscribe" then - core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" })); - core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); + core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" })); + core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); end return true; end); @@ -369,7 +358,7 @@ module:hook("resource-unbind", function(event) pres:tag("status"):text("Disconnected: "..err):up(); for jid in pairs(session.directed) do pres.attr.to = jid; - core_route_stanza(session, pres); + core_post_stanza(session, pres, true); end session.directed = nil; end diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua index ca5d51fa..8dbc2bc4 100644 --- a/plugins/mod_privacy.lua +++ b/plugins/mod_privacy.lua @@ -438,7 +438,9 @@ function preCheckOutgoing(e) e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource; end end - return checkIfNeedToBeBlocked(e, session); + if session.username then -- FIXME do properly + return checkIfNeedToBeBlocked(e, session); + end end module:hook("pre-message/full", preCheckOutgoing, 500); diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua index b8d142f7..7e150ac7 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -35,7 +35,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza) local username, host = session.username, session.host; --session.send(st.error_reply(stanza, "cancel", "not-allowed")); --return; - usermanager_set_password(username, host, nil); -- Disable account + usermanager_set_password(username, nil, host); -- Disable account -- FIXME the disabling currently allows a different user to recreate the account -- we should add an in-memory account block mode when we have threading session.send(st.reply(stanza)); @@ -70,7 +70,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza) username = nodeprep(table.concat(username)); password = table.concat(password); if username == session.username then - if usermanager_set_password(username, session.host, password) then + if usermanager_set_password(username, password, session.host) then session.send(st.reply(stanza)); else -- TODO unable to write file, file may be locked, etc, what's the correct error? diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index 9f940c37..99be8c34 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -15,10 +15,11 @@ 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; +local usermanager_get_provider = require "core.usermanager".get_provider; +local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; local usermanager_user_exists = require "core.usermanager".user_exists; local usermanager_get_password = require "core.usermanager".get_password; +local usermanager_test_password = require "core.usermanager".test_password; local t_concat, t_insert = table.concat, table.insert; local tostring = tostring; local jid_split = require "util.jid".split; @@ -66,21 +67,6 @@ else error("Unknown SASL backend"); end -local default_authentication_profile = { - plain = function(username, realm) - 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 -}; - local anonymous_authentication_profile = { anonymous = function(username, realm) return true; -- for normal usage you should always return true here @@ -183,7 +169,7 @@ module:hook("stream-features", function(event) if module:get_option("anonymous_login") then origin.sasl_handler = new_sasl(realm, anonymous_authentication_profile); else - origin.sasl_handler = new_sasl(realm, default_authentication_profile); + origin.sasl_handler = usermanager_get_sasl_handler(module.host); if not (module:get_option("allow_unencrypted_plain_auth")) and not origin.secure then origin.sasl_handler:forbidden({"PLAIN"}); end diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua index 24d10180..c3860af6 100644 --- a/plugins/mod_uptime.lua +++ b/plugins/mod_uptime.lua @@ -11,6 +11,7 @@ local st = require "util.stanza"; local start_time = prosody.start_time; prosody.events.add_handler("server-started", function() start_time = prosody.start_time end); +-- XEP-0012: Last activity module:add_feature("jabber:iq:last"); module:hook("iq/host/jabber:iq:last:query", function(event) @@ -20,3 +21,28 @@ module:hook("iq/host/jabber:iq:last:query", function(event) return true; end end); + +-- Ad-hoc command +local adhoc_new = module:require "adhoc".new; + +function uptime_text() + local t = os.time()-prosody.start_time; + local seconds = t%60; + t = (t - seconds)/60; + local minutes = t%60; + t = (t - minutes)/60; + local hours = t%24; + t = (t - hours)/24; + local days = t; + return string.format("This server has been running for %d day%s, %d hour%s and %d minute%s (since %s)", + days, (days ~= 1 and "s") or "", hours, (hours ~= 1 and "s") or "", + minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time)); +end + +function uptime_command_handler (self, data, state) + return { info = uptime_text(), status = "completed" }; +end + +local descriptor = adhoc_new("Get uptime", "uptime", uptime_command_handler); + +module:add_item ("adhoc", descriptor); diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index 18c80325..273e21ce 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -122,9 +122,13 @@ function room_mt:broadcast_message(stanza, historic) 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.attr.to = ""; + local stamp = datetime.datetime(); + local chars = #tostring(stanza); + stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203 stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) - t_insert(history, st.preserialize(stanza)); + local entry = { stanza = stanza, stamp = stamp }; + t_insert(history, entry); while #history > history_length do t_remove(history, 1) end end end @@ -151,12 +155,46 @@ function room_mt:send_occupant_list(to) end end end -function room_mt:send_history(to) +function room_mt:send_history(to, stanza) 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; + local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc"); + local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc"); + + local maxchars = history_tag and tonumber(history_tag.attr.maxchars); + if maxchars then maxchars = math.floor(maxchars); end + + local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history); + if not history_tag then maxstanzas = 20; end + + local seconds = history_tag and tonumber(history_tag.attr.seconds); + if seconds then seconds = datetime.datetime(os.time() - math.floor(seconds)); end + + local since = history_tag and history_tag.attr.since; + if since and not since:match("^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%dZ$") then since = nil; end -- FIXME timezone support + if seconds and (not since or since < seconds) then since = seconds; end + + local n = 0; + local charcount = 0; + local stanzacount = 0; + + for i=#history,1,-1 do + local entry = history[i]; + if maxchars then + if not entry.chars then + entry.stanza.attr.to = ""; + entry.chars = #tostring(entry.stanza); + end + charcount = charcount + entry.chars + #to; + if charcount > maxchars then break; end + end + if since and since > entry.stamp then break; end + if n + 1 > maxstanzas then break; end + n = n + 1; + end + for i=#history-n+1,#history do + local msg = history[i].stanza; + msg.attr.to = to; self:_route_stanza(msg); end end @@ -319,12 +357,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up() :tag("status", {code='110'})); end - if self._data.whois == 'anyone' then -- non-anonymous? - self:_route_stanza(st.stanza("message", {from=to, to=from, type='groupchat'}) - :tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) - :tag("status", {code='100'})); - end - self:send_history(from); + self:send_history(from, stanza); else -- banned local reply = st.error_reply(stanza, "auth", "forbidden"):up(); reply.tags[1].attr.code = "403"; @@ -751,7 +784,7 @@ end function room_mt:set_role(actor, occupant_jid, 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_role(self._jid_nick[actor]) ~= "moderator" then return nil, "cancel", "not-allowed"; end + if self:get_affiliation(actor) ~= "owner" then return nil, "cancel", "not-allowed"; end local occupant = self._occupants[occupant_jid]; 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 @@ -804,6 +837,9 @@ function room_mt:_route_stanza(stanza) end end end + if self._data.whois == 'anyone' then + muc_child:tag('status', { code = '100' }); + end end self:route_stanza(stanza); if muc_child then diff --git a/plugins/storage/ejabberdstore.lib.lua b/plugins/storage/ejabberdstore.lib.lua new file mode 100644 index 00000000..7e8592a8 --- /dev/null +++ b/plugins/storage/ejabberdstore.lib.lua @@ -0,0 +1,190 @@ +
+local handlers = {};
+
+handlers.accounts = {
+ get = function(self, user)
+ local select = self:query("select password from users where username=?", user);
+ local row = select and select:fetch();
+ if row then return { password = row[1] }; end
+ end;
+ set = function(self, user, data)
+ if data and data.password then
+ return self:modify("update users set password=? where username=?", data.password, user)
+ or self:modify("insert into users (username, password) values (?, ?)", user, data.password);
+ else
+ return self:modify("delete from users where username=?", user);
+ end
+ end;
+};
+handlers.vcard = {
+ get = function(self, user)
+ local select = self:query("select vcard from vcard where username=?", user);
+ local row = select and select:fetch();
+ if row then return parse_xml(row[1]); end
+ end;
+ set = function(self, user, data)
+ if data then
+ data = unparse_xml(data);
+ return self:modify("update vcard set vcard=? where username=?", data, user)
+ or self:modify("insert into vcard (username, vcard) values (?, ?)", user, data);
+ else
+ return self:modify("delete from vcard where username=?", user);
+ end
+ end;
+};
+handlers.private = {
+ get = function(self, user)
+ local select = self:query("select namespace,data from private_storage where username=?", user);
+ if select then
+ local data = {};
+ for row in select:rows() do
+ data[row[1]] = parse_xml(row[2]);
+ end
+ return data;
+ end
+ end;
+ set = function(self, user, data)
+ if data then
+ self:modify("delete from private_storage where username=?", user);
+ for namespace,text in pairs(data) do
+ self:modify("insert into private_storage (username, namespace, data) values (?, ?, ?)", user, namespace, unparse_xml(text));
+ end
+ return true;
+ else
+ return self:modify("delete from private_storage where username=?", user);
+ end
+ end;
+ -- TODO map_set, map_get
+};
+local subscription_map = { N = "none", B = "both", F = "from", T = "to" };
+local subscription_map_reverse = { none = "N", both = "B", from = "F", to = "T" };
+handlers.roster = {
+ get = function(self, user)
+ local select = self:query("select jid,nick,subscription,ask,server,subscribe,type from rosterusers where username=?", user);
+ if select then
+ local roster = { pending = {} };
+ for row in select:rows() do
+ local jid,nick,subscription,ask,server,subscribe,typ = unpack(row);
+ local item = { groups = {} };
+ if nick == "" then nick = nil; end
+ item.nick = nick;
+ item.subscription = subscription_map[subscription];
+ if ask == "N" then ask = nil;
+ elseif ask == "O" then ask = "subscribe"
+ elseif ask == "I" then roster.pending[jid] = true; ask = nil;
+ elseif ask == "B" then roster.pending[jid] = true; ask = "subscribe";
+ else module:log("debug", "bad roster_item.ask: %s", ask); ask = nil; end
+ item.ask = ask;
+ roster[jid] = item;
+ end
+
+ select = self:query("select jid,grp from rostergroups where username=?", user);
+ if select then
+ for row in select:rows() do
+ local jid,grp = unpack(rows);
+ if roster[jid] then roster[jid].groups[grp] = true; end
+ end
+ end
+ select = self:query("select version from roster_version where username=?", user);
+ local row = select and select:fetch();
+ if row then
+ roster[false] = { version = row[1]; };
+ end
+ return roster;
+ end
+ end;
+ set = function(self, user, data)
+ if data and next(data) ~= nil then
+ self:modify("delete from rosterusers where username=?", user);
+ self:modify("delete from rostergroups where username=?", user);
+ self:modify("delete from roster_version where username=?", user);
+ local done = {};
+ local pending = data.pending or {};
+ for jid,item in pairs(data) do
+ if jid and jid ~= "pending" then
+ local subscription = subscription_map_reverse[item.subscription];
+ local ask;
+ if pending[jid] then
+ if item.ask then ask = "B"; else ask = "I"; end
+ else
+ if item.ask then ask = "O"; else ask = "N"; end
+ end
+ local r = self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?, '', '', '')", user, jid, item.nick or "", subscription, ask);
+ if not r then module:log("debug", "--- :( %s", tostring(r)); end
+ done[jid] = true;
+ for group in pairs(item.groups) do
+ self:modify("insert into rostergroups (username,jid,grp) values (?, ?, ?)", user, jid, group);
+ end
+ end
+ end
+ for jid in pairs(pending) do
+ if not done[jid] then
+ self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?. ''. ''. '')", user, jid, "", "N", "I");
+ end
+ end
+ local version = data[false] and data[false].version;
+ if version then
+ self:modify("insert into roster_version (username,version) values (?, ?)", user, version);
+ end
+ return true;
+ else
+ self:modify("delete from rosterusers where username=?", user);
+ self:modify("delete from rostergroups where username=?", user);
+ self:modify("delete from roster_version where username=?", user);
+ end
+ end;
+};
+
+-----------------------------
+local driver = {};
+driver.__index = driver;
+
+function driver:prepare(sql)
+ module:log("debug", "query: %s", sql);
+ local err;
+ if not self.sqlcache then self.sqlcache = {}; end
+ local r = self.sqlcache[sql];
+ if r then return r; end
+ r, err = self.database:prepare(sql);
+ if not r then error("Unable to prepare SQL statement: "..err); end
+ self.sqlcache[sql] = r;
+ return r;
+end
+
+function driver:query(sql, ...)
+ local stmt = self:prepare(sql);
+ if stmt:execute(...) then return stmt; end
+end
+function driver:modify(sql, ...)
+ local stmt = self:query(sql, ...);
+ if stmt and stmt:affected() > 0 then return stmt; end
+end
+
+function driver:open(host, datastore, typ)
+ local cache_key = host.." "..datastore;
+ if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
+ local instance = setmetatable({}, self);
+ instance.host = host;
+ instance.datastore = datastore;
+ local handler = handlers[datastore];
+ if not handler then return nil; end
+ for key,val in pairs(handler) do
+ instance[key] = val;
+ end
+ if instance.init then instance:init(); end
+ self.ds_cache[cache_key] = instance;
+ return instance;
+end
+
+-----------------------------
+local _M = {};
+
+function _M.new(dbtype, dbname, ...)
+ local instance = setmetatable({}, driver);
+ instance.__index = instance;
+ instance.database = get_database(dbtype, dbname, ...);
+ instance.ds_cache = {};
+ return instance;
+end
+
+return _M;
diff --git a/plugins/storage/mod_storage.lua b/plugins/storage/mod_storage.lua new file mode 100644 index 00000000..e22de82c --- /dev/null +++ b/plugins/storage/mod_storage.lua @@ -0,0 +1,83 @@ + +module:set_global(); + +local cache = { data = {} }; +function cache:get(key) return self.data[key]; end +function cache:set(key, val) self.data[key] = val; return val; end + +local DBI = require "DBI"; +function get_database(driver, db, ...) + local uri = "dbi:"..driver..":"..db; + return cache:get(uri) or cache:set(uri, (function(...) + module:log("debug", "Opening database: %s", uri); + prosody.unlock_globals(); + local dbh = assert(DBI.Connect(...)); + prosody.lock_globals(); + dbh:autocommit(true) + return dbh; + end)(driver, db, ...)); +end + +local st = require "util.stanza"; +local _parse_xml = module:require("xmlparse"); +parse_xml_real = _parse_xml; +function parse_xml(str) + local s = _parse_xml(str); + if s and not s.gsub then + return st.preserialize(s); + end +end +function unparse_xml(s) + return tostring(st.deserialize(s)); +end + +local drivers = {}; + +--local driver = module:require("sqlbasic").new("SQLite3", "hello.sqlite"); +local option_datastore = module:get_option("datastore"); +local option_datastore_params = module:get_option("datastore_params") or {}; +if option_datastore then + local driver = module:require(option_datastore).new(unpack(option_datastore_params)); + table.insert(drivers, driver); +end + +local datamanager = require "util.datamanager"; +local olddm = {}; +local dm = {}; +for key,val in pairs(datamanager) do olddm[key] = val; end + +do -- driver based on old datamanager + local dmd = {}; + dmd.__index = dmd; + function dmd:open(host, datastore) + return setmetatable({ host = host, datastore = datastore }, dmd); + end + function dmd:get(user) return olddm.load(user, self.host, self.datastore); end + function dmd:set(user, data) return olddm.store(user, self.host, self.datastore, data); end + table.insert(drivers, dmd); +end + +local function open(...) + for _,driver in pairs(drivers) do + local ds = driver:open(...); + if ds then return ds; end + end +end + +local _data_path; +--function dm.set_data_path(path) _data_path = path; end +--function dm.add_callback(...) end +--function dm.remove_callback(...) end +--function dm.getpath(...) end +function dm.load(username, host, datastore) + local x = open(host, datastore); + return x:get(username); +end +function dm.store(username, host, datastore, data) + return open(host, datastore):set(username, data); +end +--function dm.list_append(...) return driver:list_append(...); end +--function dm.list_store(...) return driver:list_store(...); end +--function dm.list_load(...) return driver:list_load(...); end + +for key,val in pairs(dm) do datamanager[key] = val; end diff --git a/plugins/storage/sqlbasic.lib.lua b/plugins/storage/sqlbasic.lib.lua new file mode 100644 index 00000000..f1202287 --- /dev/null +++ b/plugins/storage/sqlbasic.lib.lua @@ -0,0 +1,97 @@ + +-- Basic SQL driver +-- This driver stores data as simple key-values + +local ser = require "util.serialization".serialize; +local deser = function(data) + module:log("debug", "deser: %s", tostring(data)); + if not data then return nil; end + local f = loadstring("return "..data); + if not f then return nil; end + setfenv(f, {}); + local s, d = pcall(f); + if not s then return nil; end + return d; +end; + +local driver = {}; +driver.__index = driver; + +driver.item_table = "item"; +driver.list_table = "list"; + +function driver:prepare(sql) + module:log("debug", "query: %s", sql); + local err; + if not self.sqlcache then self.sqlcache = {}; end + local r = self.sqlcache[sql]; + if r then return r; end + r, err = self.connection:prepare(sql); + if not r then error("Unable to prepare SQL statement: "..err); end + self.sqlcache[sql] = r; + return r; +end + +function driver:load(username, host, datastore) + local select = self:prepare("select data from "..self.item_table.." where username=? and host=? and datastore=?"); + select:execute(username, host, datastore); + local row = select:fetch(); + return row and deser(row[1]) or nil; +end + +function driver:store(username, host, datastore, data) + if not data or next(data) == nil then + local delete = self:prepare("delete from "..self.item_table.." where username=? and host=? and datastore=?"); + delete:execute(username, host, datastore); + return true; + else + local d = self:load(username, host, datastore); + if d then -- update + local update = self:prepare("update "..self.item_table.." set data=? where username=? and host=? and datastore=?"); + return update:execute(ser(data), username, host, datastore); + else -- insert + local insert = self:prepare("insert into "..self.item_table.." values (?, ?, ?, ?)"); + return insert:execute(username, host, datastore, ser(data)); + end + end +end + +function driver:list_append(username, host, datastore, data) + if not data then return; end + local insert = self:prepare("insert into "..self.list_table.." values (?, ?, ?, ?)"); + return insert:execute(username, host, datastore, ser(data)); +end + +function driver:list_store(username, host, datastore, data) + -- remove existing data + local delete = self:prepare("delete from "..self.list_table.." where username=? and host=? and datastore=?"); + delete:execute(username, host, datastore); + if data and next(data) ~= nil then + -- add data + for _, d in ipairs(data) do + self:list_append(username, host, datastore, ser(d)); + end + end + return true; +end + +function driver:list_load(username, host, datastore) + local select = self:prepare("select data from "..self.list_table.." where username=? and host=? and datastore=?"); + select:execute(username, host, datastore); + local r = {}; + for row in select:rows() do + table.insert(r, deser(row[1])); + end + return r; +end + +local _M = {}; +function _M.new(dbtype, dbname, ...) + local d = {}; + setmetatable(d, driver); + local dbh = get_database(dbtype, dbname, ...); + --d:set_connection(dbh); + d.connection = dbh; + return d; +end +return _M; diff --git a/plugins/storage/xep227store.lib.lua b/plugins/storage/xep227store.lib.lua new file mode 100644 index 00000000..5ef8df54 --- /dev/null +++ b/plugins/storage/xep227store.lib.lua @@ -0,0 +1,168 @@ +
+local st = require "util.stanza";
+
+local function getXml(user, host)
+ local jid = user.."@"..host;
+ local path = "data/"..jid..".xml";
+ local f = io.open(path);
+ if not f then return; end
+ local s = f:read("*a");
+ return parse_xml_real(s);
+end
+local function setXml(user, host, xml)
+ local jid = user.."@"..host;
+ local path = "data/"..jid..".xml";
+ if xml then
+ local f = io.open(path, "w");
+ if not f then return; end
+ local s = tostring(xml);
+ f:write(s);
+ f:close();
+ return true;
+ else
+ return os.remove(path);
+ end
+end
+local function getUserElement(xml)
+ if xml and xml.name == "server-data" then
+ local host = xml.tags[1];
+ if host and host.name == "host" then
+ local user = host.tags[1];
+ if user and user.name == "user" then
+ return user;
+ end
+ end
+ end
+end
+local function createOuterXml(user, host)
+ return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'})
+ :tag("host", {jid=host})
+ :tag("user", {name = user});
+end
+local function removeFromArray(array, value)
+ for i,item in ipairs(array) do
+ if item == value then
+ table.remove(array, i);
+ return;
+ end
+ end
+end
+local function removeStanzaChild(s, child)
+ removeFromArray(s.tags, child);
+ removeFromArray(s, child);
+end
+
+local handlers = {};
+
+handlers.accounts = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user and user.attr.password then
+ return { password = user.attr.password };
+ end
+ end;
+ set = function(self, user, data)
+ if data and data.password then
+ local xml = getXml(user, self.host);
+ if not xml then xml = createOuterXml(user, self.host); end
+ local usere = getUserElement(xml);
+ usere.attr.password = data.password;
+ return setXml(user, self.host, xml);
+ else
+ return setXml(user, self.host, nil);
+ end
+ end;
+};
+handlers.vcard = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user then
+ local vcard = user:get_child("vCard", 'vcard-temp');
+ if vcard then
+ return st.preserialize(vcard);
+ end
+ end
+ end;
+ set = function(self, user, data)
+ local xml = getXml(user, self.host);
+ local usere = xml and getUserElement(xml);
+ if usere then
+ local vcard = usere:get_child("vCard", 'vcard-temp');
+ if vcard then
+ removeStanzaChild(usere, vcard);
+ elseif not data then
+ return true;
+ end
+ if data then
+ vcard = st.deserialize(data);
+ usere:add_child(vcard);
+ end
+ return setXml(user, self.host, xml);
+ end
+ return true;
+ end;
+};
+handlers.private = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user then
+ local private = user:get_child("query", "jabber:iq:private");
+ if private then
+ local r = {};
+ for _, tag in ipairs(private.tags) do
+ r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag);
+ end
+ return r;
+ end
+ end
+ end;
+ set = function(self, user, data)
+ local xml = getXml(user, self.host);
+ local usere = xml and getUserElement(xml);
+ if usere then
+ local private = usere:get_child("query", 'jabber:iq:private');
+ if private then removeStanzaChild(usere, private); end
+ if data and next(data) ~= nil then
+ private = st.stanza("query", {xmlns='jabber:iq:private'});
+ for _,tag in pairs(data) do
+ private:add_child(st.deserialize(tag));
+ end
+ usere:add_child(private);
+ end
+ return setXml(user, self.host, xml);
+ end
+ return true;
+ end;
+};
+
+-----------------------------
+local driver = {};
+driver.__index = driver;
+
+function driver:open(host, datastore, typ)
+ local cache_key = host.." "..datastore;
+ if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
+ local instance = setmetatable({}, self);
+ instance.host = host;
+ instance.datastore = datastore;
+ local handler = handlers[datastore];
+ if not handler then return nil; end
+ for key,val in pairs(handler) do
+ instance[key] = val;
+ end
+ if instance.init then instance:init(); end
+ self.ds_cache[cache_key] = instance;
+ return instance;
+end
+
+-----------------------------
+local _M = {};
+
+function _M.new()
+ local instance = setmetatable({}, driver);
+ instance.__index = instance;
+ instance.ds_cache = {};
+ return instance;
+end
+
+return _M;
diff --git a/plugins/storage/xmlparse.lib.lua b/plugins/storage/xmlparse.lib.lua new file mode 100644 index 00000000..91063995 --- /dev/null +++ b/plugins/storage/xmlparse.lib.lua @@ -0,0 +1,56 @@ +
+local st = require "util.stanza";
+
+-- XML parser
+local parse_xml = (function()
+ local entity_map = setmetatable({
+ ["amp"] = "&";
+ ["gt"] = ">";
+ ["lt"] = "<";
+ ["apos"] = "'";
+ ["quot"] = "\"";
+ }, {__index = function(_, s)
+ if s:sub(1,1) == "#" then
+ if s:sub(2,2) == "x" then
+ return string.char(tonumber(s:sub(3), 16));
+ else
+ return string.char(tonumber(s:sub(2)));
+ end
+ end
+ end
+ });
+ local function xml_unescape(str)
+ return (str:gsub("&(.-);", entity_map));
+ end
+ local function parse_tag(s)
+ local name,sattr=(s):gmatch("([^%s]+)(.*)")();
+ local attr = {};
+ for a,b in (sattr):gmatch("([^=%s]+)=['\"]([^'\"]*)['\"]") do attr[a] = xml_unescape(b); end
+ return name, attr;
+ end
+ return function(xml)
+ local stanza = st.stanza("root");
+ local regexp = "<([^>]*)>([^<]*)";
+ for elem, text in xml:gmatch(regexp) do
+ if elem:sub(1,1) == "!" or elem:sub(1,1) == "?" then -- neglect comments and processing-instructions
+ elseif elem:sub(1,1) == "/" then -- end tag
+ elem = elem:sub(2);
+ stanza:up(); -- TODO check for start-end tag name match
+ elseif elem:sub(-1,-1) == "/" then -- empty tag
+ elem = elem:sub(1,-2);
+ local name,attr = parse_tag(elem);
+ stanza:tag(name, attr):up();
+ else -- start tag
+ local name,attr = parse_tag(elem);
+ stanza:tag(name, attr);
+ end
+ if #text ~= 0 then -- text
+ stanza:text(xml_unescape(text));
+ end
+ end
+ return stanza.tags[1];
+ end
+end)();
+-- end of XML parser
+
+return parse_xml;
|