diff options
Diffstat (limited to 'plugins')
44 files changed, 3743 insertions, 1108 deletions
diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua new file mode 100644 index 00000000..0cb4efe1 --- /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.layout or content):form(content.values)); + elseif name == "result" then + cmdtag:add_child((content.layout or content):form(content.values, "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..20c0f2be --- /dev/null +++ b/plugins/adhoc/mod_adhoc.lua @@ -0,0 +1,105 @@ +-- Copyright (C) 2009 Thilo Cestonaro +-- 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 = 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.."#info:query", function (event) + local origin, stanza = event.origin, event.stanza; + local node = stanza.tags[1].attr.node; + if stanza.attr.type == "get" and node then + if commands[node] then + local privileged = is_admin(stanza.attr.from, stanza.attr.to); + if (commands[node].permission == "admin" and privileged) + or (commands[node].permission == "user") then + reply = st.reply(stanza); + reply:tag("query", { xmlns = xmlns_disco.."#info", + node = node }); + reply:tag("identity", { name = commands[node].name, + category = "automation", type = "command-node" }):up(); + reply:tag("feature", { var = xmlns_cmd }):up(); + reply:tag("feature", { var = "jabber:x:data" }):up(); + else + reply = st.error_reply(stanza, "auth", "forbidden", "This item is not available to you"); + end + origin.send(reply); + return true; + elseif node == xmlns_cmd then + reply = st.reply(stanza); + reply:tag("query", { xmlns = xmlns_disco.."#info", + node = node }); + reply:tag("identity", { name = "Ad-Hoc Commands", + category = "automation", type = "command-list" }):up(); + origin.send(reply); + return true; + + end + end +end); + +module:hook("iq/host/"..xmlns_disco.."#items:query", function (event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "get" and stanza.tags[1].attr.node + and stanza.tags[1].attr.node == xmlns_cmd then + local privileged = is_admin(stanza.attr.from, stanza.attr.to); + 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/"..xmlns_cmd..":command", function (event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "set" then + local node = stanza.tags[1].attr.node + if commands[node] then + local privileged = is_admin(stanza.attr.from, stanza.attr.to); + 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_admin_adhoc.lua b/plugins/mod_admin_adhoc.lua new file mode 100644 index 00000000..984ae5ea --- /dev/null +++ b/plugins/mod_admin_adhoc.lua @@ -0,0 +1,609 @@ +-- 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 _G = _G; + +local prosody = _G.prosody; +local hosts = prosody.hosts; +local t_concat = table.concat; + +require "util.iterators"; +local usermanager_user_exists = require "core.usermanager".user_exists; +local usermanager_create_user = require "core.usermanager".create_user; +local usermanager_get_password = require "core.usermanager".get_password; +local usermanager_set_password = require "core.usermanager".set_password; +local is_admin = require "core.usermanager".is_admin; +local rm_load_roster = require "core.rostermanager".load_roster; +local st, jid, uuid = require "util.stanza", require "util.jid", require "util.uuid"; +local timer_add_task = require "util.timer".add_task; +local dataforms_new = require "util.dataforms".new; +local array = require "util.array"; +local modulemanager = require "modulemanager"; + +local adhoc_new = module:require "adhoc".new; + +function add_user_command_handler(self, data, state) + local add_user_layout = dataforms_new{ + title = "Adding a User"; + instructions = "Fill out this form to add a user."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for the account to be added" }; + { name = "password", type = "text-private", label = "The password for this account" }; + { name = "password-verify", type = "text-private", label = "Retype password" }; + }; + + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + local fields = add_user_layout:data(data.form); + if not fields.accountjid then + return { status = "completed", error = { message = "You need to specify a JID." } }; + end + local username, host, resource = jid.split(fields.accountjid); + if data.to ~= host then + return { status = "completed", error = { message = "Trying to add a user on " .. host .. " but command was sent to " .. data.to}}; + end + if (fields["password"] == fields["password-verify"]) and username and host then + if usermanager_user_exists(username, host) then + return { status = "completed", error = { message = "Account already exists" } }; + else + if usermanager_create_user(username, fields.password, host) then + module:log("info", "Created new account " .. username.."@"..host); + return { status = "completed", info = "Account successfully created" }; + else + return { status = "completed", error = { message = "Failed to write data to disk" } }; + end + end + else + module:log("debug", (fields.accountjid or "<nil>") .. " " .. (fields.password or "<nil>") .. " " + .. (fields["password-verify"] or "<nil>")); + return { status = "completed", error = { message = "Invalid data.\nPassword mismatch, or empty username" } }; + end + else + return { status = "executing", form = add_user_layout }, "executing"; + end +end + +function change_user_password_command_handler(self, data, state) + local change_user_password_layout = dataforms_new{ + title = "Changing a User Password"; + instructions = "Fill out this form to change a user's password."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for this account" }; + { name = "password", type = "text-private", required = true, label = "The password for this account" }; + }; + + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + local fields = change_user_password_layout:data(data.form); + if not fields.accountjid or fields.accountjid == "" or not fields.password then + return { status = "completed", error = { message = "Please specify username and password" } }; + end + local username, host, resource = jid.split(fields.accountjid); + if data.to ~= host then + return { status = "completed", error = { message = "Trying to change the password of a user on " .. host .. " but command was sent to " .. data.to}}; + end + if usermanager_user_exists(username, host) and usermanager_set_password(username, fields.password, host) then + return { status = "completed", info = "Password successfully changed" }; + else + return { status = "completed", error = { message = "User does not exist" } }; + end + else + return { status = "executing", form = change_user_password_layout }, "executing"; + end +end + +function delete_user_command_handler(self, data, state) + local delete_user_layout = dataforms_new{ + title = "Deleting a User"; + instructions = "Fill out this form to delete a user."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) to delete" }; + }; + + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + local fields = delete_user_layout:data(data.form); + local failed = {}; + local succeeded = {}; + for _, aJID in ipairs(fields.accountjids) do + local username, host, resource = jid.split(aJID); + if (host == data.to) and usermanager_user_exists(username, host) and disconnect_user(aJID) and usermanager_create_user(username, nil, host) then + module:log("debug", "User " .. aJID .. " has been deleted"); + succeeded[#succeeded+1] = aJID; + else + module:log("debug", "Tried to delete non-existant user "..aJID); + failed[#failed+1] = aJID; + end + end + return {status = "completed", info = (#succeeded ~= 0 and + "The following accounts were successfully deleted:\n"..t_concat(succeeded, "\n").."\n" or "").. + (#failed ~= 0 and + "The following accounts could not be deleted:\n"..t_concat(failed, "\n") or "") }; + else + return { status = "executing", form = delete_user_layout }, "executing"; + end +end + +function disconnect_user(match_jid) + local node, hostname, givenResource = jid.split(match_jid); + local host = hosts[hostname]; + local sessions = host.sessions[node] and host.sessions[node].sessions; + for resource, session in pairs(sessions or {}) do + if not givenResource or (resource == givenResource) then + module:log("debug", "Disconnecting "..node.."@"..hostname.."/"..resource); + session:close(); + end + end + return true; +end + +function end_user_session_handler(self, data, state) + local end_user_session_layout = dataforms_new{ + title = "Ending a User Session"; + instructions = "Fill out this form to end a user's session."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) for which to end sessions" }; + }; + + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + + local fields = end_user_session_layout:data(data.form); + local failed = {}; + local succeeded = {}; + for _, aJID in ipairs(fields.accountjids) do + local username, host, resource = jid.split(aJID); + if (host == data.to) and usermanager_user_exists(username, host) and disconnect_user(aJID) then + succeeded[#succeeded+1] = aJID; + else + failed[#failed+1] = aJID; + end + end + return {status = "completed", info = (#succeeded ~= 0 and + "The following accounts were successfully disconnected:\n"..t_concat(succeeded, "\n").."\n" or "").. + (#failed ~= 0 and + "The following accounts could not be disconnected:\n"..t_concat(failed, "\n") or "") }; + else + return { status = "executing", form = end_user_session_layout }, "executing"; + end +end + +local end_user_session_layout = dataforms_new{ + title = "Ending a User Session"; + instructions = "Fill out this form to end a user's session."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) for which to end sessions" }; +}; + + +function get_user_password_handler(self, data, state) + local get_user_password_layout = dataforms_new{ + title = "Getting User's Password"; + instructions = "Fill out this form to get a user's password."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for which to retrieve the password" }; + }; + + local get_user_password_result_layout = dataforms_new{ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", label = "JID" }; + { name = "password", type = "text-single", label = "Password" }; + }; + + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + local fields = get_user_password_layout:data(data.form); + if not fields.accountjid then + return { status = "completed", error = { message = "Please specify a JID." } }; + end + local user, host, resource = jid.split(fields.accountjid); + local accountjid = ""; + local password = ""; + if host ~= data.to then + return { status = "completed", error = { message = "Tried to get password for a user on " .. host .. " but command was sent to " .. data.to } }; + elseif usermanager_user_exists(user, host) then + accountjid = fields.accountjid; + password = usermanager_get_password(user, host); + else + return { status = "completed", error = { message = "User does not exist" } }; + end + return { status = "completed", result = { layout = get_user_password_result_layout, values = {accountjid = accountjid, password = password} } }; + else + return { status = "executing", form = get_user_password_layout }, "executing"; + end +end + +function get_user_roster_handler(self, data, state) + local get_user_roster_layout = dataforms_new{ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for which to retrieve the roster" }; + }; + + local get_user_roster_result_layout = dataforms_new{ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", label = "This is the roster for" }; + { name = "roster", type = "text-multi", label = "Roster XML" }; + }; + + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + + local fields = get_user_roster_layout:data(data.form); + + if not fields.accountjid then + return { status = "completed", error = { message = "Please specify a JID" } }; + end + + local user, host, resource = jid.split(fields.accountjid); + if host ~= data.to then + return { status = "completed", error = { message = "Tried to get roster for a user on " .. host .. " but command was sent to " .. data.to } }; + elseif not usermanager_user_exists(user, host) then + return { status = "completed", error = { message = "User does not exist" } }; + end + local roster = rm_load_roster(user, host); + + local query = st.stanza("query", { xmlns = "jabber:iq:roster" }); + for jid in pairs(roster) do + if jid ~= "pending" and jid then + query:tag("item", { + jid = jid, + subscription = roster[jid].subscription, + ask = roster[jid].ask, + name = roster[jid].name, + }); + for group in pairs(roster[jid].groups) do + query:tag("group"):text(group):up(); + end + query:up(); + end + end + + local query_text = query:__tostring(); -- TODO: Use upcoming pretty_print() function + query_text = query_text:gsub("><", ">\n<"); + + local result = get_user_roster_result_layout:form({ accountjid = user.."@"..host, roster = query_text }, "result"); + result:add_child(query); + return { status = "completed", other = result }; + else + return { status = "executing", form = get_user_roster_layout }, "executing"; + end +end + +function get_user_stats_handler(self, data, state) + local get_user_stats_layout = dataforms_new{ + title = "Get User Statistics"; + instructions = "Fill out this form to gather user statistics."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for statistics" }; + }; + + local get_user_stats_result_layout = dataforms_new{ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "ipaddresses", type = "text-multi", label = "IP Addresses" }; + { name = "rostersize", type = "text-single", label = "Roster size" }; + { name = "onlineresources", type = "text-multi", label = "Online Resources" }; + }; + + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + + local fields = get_user_stats_layout:data(data.form); + + if not fields.accountjid then + return { status = "completed", error = { message = "Please specify a JID." } }; + end + + local user, host, resource = jid.split(fields.accountjid); + if host ~= data.to then + return { status = "completed", error = { message = "Tried to get stats for a user on " .. host .. " but command was sent to " .. data.to } }; + elseif not usermanager_user_exists(user, host) then + return { status = "completed", error = { message = "User does not exist" } }; + end + local roster = rm_load_roster(user, host); + local rostersize = 0; + local IPs = ""; + local resources = ""; + for jid in pairs(roster) do + if jid ~= "pending" and jid then + rostersize = rostersize + 1; + end + end + for resource, session in pairs((hosts[host].sessions[user] and hosts[host].sessions[user].sessions) or {}) do + resources = resources .. "\n" .. resource; + IPs = IPs .. "\n" .. session.ip; + end + return { status = "completed", result = {layout = get_user_stats_result_layout, values = {ipaddresses = IPs, rostersize = tostring(rostersize), + onlineresources = resources}} }; + else + return { status = "executing", form = get_user_stats_layout }, "executing"; + end +end + +function get_online_users_command_handler(self, data, state) + local get_online_users_layout = dataforms_new{ + title = "Getting List of Online Users"; + instructions = "How many users should be returned at most?"; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "max_items", type = "list-single", label = "Maximum number of users", + value = { "25", "50", "75", "100", "150", "200", "all" } }; + { name = "details", type = "boolean", label = "Show details" }; + }; + + local get_online_users_result_layout = dataforms_new{ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "onlineuserjids", type = "text-multi", label = "The list of all online users" }; + }; + + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + + local fields = get_online_users_layout:data(data.form); + + local max_items = nil + if fields.max_items ~= "all" then + max_items = tonumber(fields.max_items); + end + local count = 0; + local users = {}; + for username, user in pairs(hosts[data.to].sessions or {}) do + if (max_items ~= nil) and (count >= max_items) then + break; + end + users[#users+1] = username.."@"..data.to; + count = count + 1; + if fields.details then + for resource, session in pairs(user.sessions or {}) do + local status, priority = "unavailable", tostring(session.priority or "-"); + if session.presence then + status = session.presence:child_with_name("show"); + if status then + status = status:get_text() or "[invalid!]"; + else + status = "available"; + end + end + users[#users+1] = " - "..resource..": "..status.."("..priority..")"; + end + end + end + return { status = "completed", result = {layout = get_online_users_result_layout, values = {onlineuserjids=t_concat(users, "\n")}} }; + else + return { status = "executing", form = get_online_users_layout }, "executing"; + end +end + +function list_modules_handler(self, data, state) + local result = dataforms_new { + title = "List of loaded modules"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#list" }; + { name = "modules", type = "text-multi", label = "The following modules are loaded:" }; + }; + + local modules = array.collect(keys(hosts[data.to].modules)):sort():concat("\n"); + + return { status = "completed", result = { layout = result; values = { modules = modules } } }; +end + +function load_module_handler(self, data, state) + local layout = dataforms_new { + title = "Load module"; + instructions = "Specify the module to be loaded"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#load" }; + { name = "module", type = "text-single", required = true, label = "Module to be loaded:"}; + }; + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + local fields = layout:data(data.form); + if (not fields.module) or (fields.module == "") then + return { status = "completed", error = { + message = "Please specify a module." + } }; + end + if modulemanager.is_loaded(data.to, fields.module) then + return { status = "completed", info = "Module already loaded" }; + end + local ok, err = modulemanager.load(data.to, fields.module); + if ok then + return { status = "completed", info = 'Module "'..fields.module..'" successfully loaded on host "'..data.to..'".' }; + else + return { status = "completed", error = { message = 'Failed to load module "'..fields.module..'" on host "'..data.to.. + '". Error was: "'..tostring(err or "<unspecified>")..'"' } }; + end + else + local modules = array.collect(keys(hosts[data.to].modules)):sort(); + return { status = "executing", form = layout }, "executing"; + end +end + +function reload_modules_handler(self, data, state) + local layout = dataforms_new { + title = "Reload modules"; + instructions = "Select the modules to be reloaded"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#reload" }; + { name = "modules", type = "list-multi", required = true, label = "Modules to be reloaded:"}; + }; + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + local fields = layout:data(data.form); + if #fields.modules == 0 then + return { status = "completed", error = { + message = "Please specify a module. (This means your client misbehaved, as this field is required)" + } }; + end + local ok_list, err_list = {}, {}; + for _, module in ipairs(fields.modules) do + local ok, err = modulemanager.reload(data.to, module); + if ok then + ok_list[#ok_list + 1] = module; + else + err_list[#err_list + 1] = module .. "(Error: " .. tostring(err) .. ")"; + end + end + local info = (#ok_list > 0 and ("The following modules were successfully reloaded on host "..data.to..":\n"..t_concat(ok_list, "\n")) or "").. + (#err_list > 0 and ("Failed to reload the following modules on host "..data.to..":\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; + else + local modules = array.collect(keys(hosts[data.to].modules)):sort(); + return { status = "executing", form = { layout = layout; values = { modules = modules } } }, "executing"; + end +end + +function send_to_online(message, server) + if server then + sessions = { [server] = hosts[server] }; + else + sessions = hosts; + end + + local c = 0; + for domain, session in pairs(sessions) do + for user in pairs(session.sessions or {}) do + c = c + 1; + message.attr.from = domain; + message.attr.to = user.."@"..domain; + core_post_stanza(session, message); + end + end + + return c; +end + +function shut_down_service_handler(self, data, state) + local shut_down_service_layout = dataforms_new{ + title = "Shutting Down the Service"; + instructions = "Fill out this form to shut down the service."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "delay", type = "list-single", label = "Time delay before shutting down", + value = { {label = "30 seconds", value = "30"}, + {label = "60 seconds", value = "60"}, + {label = "90 seconds", value = "90"}, + {label = "2 minutes", value = "120"}, + {label = "3 minutes", value = "180"}, + {label = "4 minutes", value = "240"}, + {label = "5 minutes", value = "300"}, + }; + }; + { name = "announcement", type = "text-multi", label = "Announcement" }; + }; + + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + + local fields = shut_down_service_layout:data(data.form); + + if fields.announcement and #fields.announcement > 0 then + local message = st.message({type = "headline"}, fields.announcement):up() + :tag("subject"):text("Server is shutting down"); + send_to_online(message); + end + + timer_add_task(tonumber(fields.delay or "5"), prosody.shutdown); + + return { status = "completed", info = "Server is about to shut down" }; + else + return { status = "executing", form = shut_down_service_layout }, "executing"; + end + + return true; +end + +function unload_modules_handler(self, data, state) + local layout = dataforms_new { + title = "Unload modules"; + instructions = "Select the modules to be unloaded"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#unload" }; + { name = "modules", type = "list-multi", required = true, label = "Modules to be unloaded:"}; + }; + if state then + if data.action == "cancel" then + return { status = "canceled" }; + end + local fields = layout:data(data.form); + if #fields.modules == 0 then + return { status = "completed", error = { + message = "Please specify a module. (This means your client misbehaved, as this field is required)" + } }; + end + local ok_list, err_list = {}, {}; + for _, module in ipairs(fields.modules) do + local ok, err = modulemanager.unload(data.to, module); + if ok then + ok_list[#ok_list + 1] = module; + else + err_list[#err_list + 1] = module .. "(Error: " .. tostring(err) .. ")"; + end + end + local info = (#ok_list > 0 and ("The following modules were successfully unloaded on host "..data.to..":\n"..t_concat(ok_list, "\n")) or "").. + (#err_list > 0 and ("Failed to unload the following modules on host "..data.to..":\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; + else + local modules = array.collect(keys(hosts[data.to].modules)):sort(); + return { status = "executing", form = { layout = layout; values = { modules = modules } } }, "executing"; + end +end + +local add_user_desc = adhoc_new("Add User", "http://jabber.org/protocol/admin#add-user", add_user_command_handler, "admin"); +local change_user_password_desc = adhoc_new("Change User Password", "http://jabber.org/protocol/admin#change-user-password", change_user_password_command_handler, "admin"); +local delete_user_desc = adhoc_new("Delete User", "http://jabber.org/protocol/admin#delete-user", delete_user_command_handler, "admin"); +local end_user_session_desc = adhoc_new("End User Session", "http://jabber.org/protocol/admin#end-user-session", end_user_session_handler, "admin"); +local get_user_password_desc = adhoc_new("Get User Password", "http://jabber.org/protocol/admin#get-user-password", get_user_password_handler, "admin"); +local get_user_roster_desc = adhoc_new("Get User Roster","http://jabber.org/protocol/admin#get-user-roster", get_user_roster_handler, "admin"); +local get_user_stats_desc = adhoc_new("Get User Statistics","http://jabber.org/protocol/admin#user-stats", get_user_stats_handler, "admin"); +local get_online_users_desc = adhoc_new("Get List of Online Users", "http://jabber.org/protocol/admin#get-online-users", get_online_users_command_handler, "admin"); +local list_modules_desc = adhoc_new("List loaded modules", "http://prosody.im/protocol/modules#list", list_modules_handler, "admin"); +local load_module_desc = adhoc_new("Load module", "http://prosody.im/protocol/modules#load", load_module_handler, "admin"); +local reload_modules_desc = adhoc_new("Reload modules", "http://prosody.im/protocol/modules#reload", reload_modules_handler, "admin"); +local shut_down_service_desc = adhoc_new("Shut Down Service", "http://jabber.org/protocol/admin#shutdown", shut_down_service_handler, "admin"); +local unload_modules_desc = adhoc_new("Unload modules", "http://prosody.im/protocol/modules#unload", unload_modules_handler, "admin"); + +module:add_item("adhoc", add_user_desc); +module:add_item("adhoc", change_user_password_desc); +module:add_item("adhoc", delete_user_desc); +module:add_item("adhoc", end_user_session_desc); +module:add_item("adhoc", get_user_password_desc); +module:add_item("adhoc", get_user_roster_desc); +module:add_item("adhoc", get_user_stats_desc); +module:add_item("adhoc", get_online_users_desc); +module:add_item("adhoc", list_modules_desc); +module:add_item("adhoc", load_module_desc); +module:add_item("adhoc", reload_modules_desc); +module:add_item("adhoc", shut_down_service_desc); +module:add_item("adhoc", unload_modules_desc); diff --git a/plugins/mod_console.lua b/plugins/mod_admin_telnet.lua index e87ef536..712e9eb7 100644 --- a/plugins/mod_console.lua +++ b/plugins/mod_admin_telnet.lua @@ -27,7 +27,13 @@ local default_env_mt = { __index = def_env }; prosody.console = { commands = commands, env = def_env }; local function redirect_output(_G, session) - return setmetatable({ print = session.print }, { __index = function (t, k) return rawget(_G, k); end, __newindex = function (t, k, v) rawset(_G, k, v); end }); + local env = setmetatable({ print = session.print }, { __index = function (t, k) return rawget(_G, k); end }); + env.dofile = function(name) + local f, err = loadfile(name); + if not f then return f, err; end + return setfenv(f, env)(); + end; + return env; end console = {}; @@ -36,7 +42,13 @@ function console:new_session(conn) local w = function(s) conn:write(s:gsub("\n", "\r\n")); end; local session = { conn = conn; send = function (t) w(tostring(t)); end; - print = function (t) w("| "..tostring(t).."\n"); end; + print = function (...) + local t = {}; + for i=1,select("#", ...) do + t[i] = tostring(select(i, ...)); + end + w("| "..table.concat(t, "\t").."\n"); + end; disconnect = function () conn:close(); end; }; session.env = setmetatable({}, default_env_mt); @@ -148,7 +160,7 @@ end commands.quit, commands.exit = commands.bye, commands.bye; commands["!"] = function (session, data) - if data:match("^!!") then + if data:match("^!!") and session.env._ then session.print("!> "..session.env._); return console_listener.onincoming(session.conn, session.env._); end @@ -165,6 +177,7 @@ commands["!"] = function (session, data) session.print("Sorry, not sure what you want"); end + function commands.help(session, data) local print = session.print; local section = data:match("^help (%w+)"); @@ -175,6 +188,7 @@ function commands.help(session, data) print [[c2s - Commands to manage local client-to-server sessions]] print [[s2s - Commands to manage sessions between this server and others]] print [[module - Commands to load/reload/unload modules/plugins]] + print [[host - Commands to activate, deactivate and list virtual hosts]] print [[server - Uptime, version, shutting down, etc.]] print [[config - Reloading the configuration, etc.]] print [[console - Help regarding the console itself]] @@ -191,6 +205,10 @@ function commands.help(session, data) 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 == "host" then + print [[host:activate(hostname) - Activates the specified host]] + print [[host:deactivate(hostname) - Disconnects all clients on this host and deactivates]] + print [[host:list() - List the currently-activated hosts]] elseif section == "server" then print [[server:version() - Show the server's version number]] print [[server:uptime() - Show how long the server has been running]] @@ -239,8 +257,8 @@ function def_env.server:uptime() local hours = t%24; t = (t - hours)/24; local days = t; - return true, 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 "", + return true, 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 @@ -508,11 +526,11 @@ function def_env.s2s:show(match_jid) end end end - local subhost_filter = function (h) + 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) + 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)) -- Pft! is what I say to list comprehensions or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then @@ -555,7 +573,7 @@ function def_env.s2s:close(from, to) if hosts[from] and not hosts[to] then -- Is an outgoing connection local session = hosts[from].s2sout[to]; - if not session then + if not session then print("No outgoing connection from "..from.." to "..to) else (session.close or s2smanager.destroy_session)(session); @@ -586,31 +604,12 @@ function def_env.s2s:close(from, to) 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"; + return hostmanager.activate(hostname, config); 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"; + return hostmanager.deactivate(hostname, reason); end function def_env.host:list() diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua index d3017f6c..77555bec 100644 --- a/plugins/mod_announce.lua +++ b/plugins/mod_announce.lua @@ -6,14 +6,38 @@ -- COPYING file in the source package for more information. -- -local st, jid, set = require "util.stanza", require "util.jid", require "util.set"; +local st, jid = require "util.stanza", require "util.jid"; local is_admin = require "core.usermanager".is_admin; -local admins = set.new(config.get(module:get_host(), "core", "admins")); -function handle_announcement(data) - local origin, stanza = data.origin, data.stanza; - local host, resource = select(2, jid.split(stanza.attr.to)); +function send_to_online(message, host) + local sessions; + if host then + sessions = { [host] = hosts[host] }; + 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(event) + local origin, stanza = event.origin, event.stanza; + local node, host, resource = jid.split(stanza.attr.to); if resource ~= "announce/online" then return; -- Not an announcement @@ -21,25 +45,56 @@ function handle_announcement(data) if not is_admin(stanza.attr.from) then -- Not an admin? Not allowed! - module:log("warn", "Non-admin %s tried to send server announcement", tostring(jid.bare(stanza.attr.from))); + module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from); return; end module:log("info", "Sending server announcement to all online users"); - local host_session = hosts[host]; local message = st.clone(stanza); 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..8d790508 --- /dev/null +++ b/plugins/mod_auth_anonymous.lua @@ -0,0 +1,67 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local log = require "util.logger".init("auth_anonymous"); +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 anonymous_authentication_profile = { + anonymous = function(sasl, username, realm) + return true; -- for normal usage you should always return true here + end + }; + return new_sasl(module.host, anonymous_authentication_profile); + 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..447fae51 --- /dev/null +++ b/plugins/mod_auth_cyrus.lua @@ -0,0 +1,83 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local log = require "util.logger".init("auth_cyrus"); + +local usermanager_user_exists = require "core.usermanager".user_exists; + +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"); +local require_provisioning = module:get_option("cyrus_require_provisioning") or false; + +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 + +do -- diagnostic + local list; + for mechanism in pairs(new_sasl(module.host):mechanisms()) do + list = (not(list) and mechanism) or (list..", "..mechanism); + end + if not list then + module:log("error", "No Cyrus SASL mechanisms available"); + else + module:log("debug", "Available Cyrus SASL mechanisms: %s", list); + end +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) + if require_provisioning then + return usermanager_user_exists(username, module.host); + end + 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 handler = new_sasl(module.host); + if require_provisioning then + function handler.require_provisioning(username) + return usermanager_user_exists(username, module.host); + end + end + return handler; + 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..ee810426 --- /dev/null +++ b/plugins/mod_auth_internal_hashed.lua @@ -0,0 +1,184 @@ +-- 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("auth_internal_hashed"); +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; + +-- COMPAT w/old trunk: remove these two lines before 0.8 release +local hmac_sha1 = require "util.hmac".sha1; +local sha1 = require "util.hashes".sha1; + +local to_hex; +do + local function replace_byte_with_hex(byte) + return ("%02x"):format(byte:byte()); + end + function to_hex(binary_string) + return binary_string:gsub(".", replace_byte_with_hex); + end +end + +local from_hex; +do + local function replace_hex_with_byte(hex) + return string.char(tonumber(hex, 16)); + end + function from_hex(hex_string) + return hex_string:gsub("..", replace_hex_with_byte); + end +end + + +local prosody = _G.prosody; + +-- 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) + 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 + -- COMPAT w/old trunk: remove before 0.8 release + if credentials.hashpass then + local salted_password = from_hex(credentials.hashpass); + credentials.stored_key = sha1(hmac_sha1(salted_password, "Client Key"), true); + credentials.server_key = to_hex(hmac_sha1(salted_password, "Server Key")); + 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 = to_hex(stored_key); + local server_key_hex = to_hex(server_key); + + 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) + 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 = to_hex(stored_key); + local server_key_hex = to_hex(server_key); + + 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) + 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 + return true; + end + + function provider.create_user(username, password) + if password == nil then + return datamanager.store(username, host, "accounts", {}); + end + local salt = generate_uuid(); + local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, iteration_count); + local stored_key_hex = to_hex(stored_key); + local server_key_hex = to_hex(server_key); + 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.delete_user(username) + return datamanager.store(username, host, "accounts", nil); + end + + function provider.get_sasl_handler() + local testpass_authentication_profile = { + plain_test = function(sasl, 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, realm, password), true; + end, + scram_sha_1 = function(sasl, username, realm) + local credentials = datamanager.load(username, host, "accounts"); + if not credentials then return; end + if credentials.password then + usermanager.set_password(username, credentials.password, host); + credentials = datamanager.load(username, host, "accounts"); + if not credentials then return; end + end + + -- convert hexpass to stored_key and server_key + -- COMPAT w/old trunk: remove before 0.8 release + if credentials.hashpass then + local salted_password = from_hex(credentials.hashpass); + credentials.stored_key = sha1(hmac_sha1(salted_password, "Client Key"), true); + credentials.server_key = to_hex(hmac_sha1(salted_password, "Server Key")); + 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 from_hex(stored_key); + server_key = server_key and from_hex(server_key); + return stored_key, server_key, iteration_count, salt, true; + end + }; + return new_sasl(module.host, testpass_authentication_profile); + end + + return provider; +end + +module:add_item("auth-provider", new_hashpass_provider(module.host)); + diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua new file mode 100644 index 00000000..784553ea --- /dev/null +++ b/plugins/mod_auth_internal_plain.lua @@ -0,0 +1,92 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local datamanager = require "util.datamanager"; +local log = require "util.logger".init("auth_internal_plain"); +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; + +function new_default_provider(host) + local provider = { name = "internal_plain" }; + 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); + 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); + return (datamanager.load(username, host, "accounts") or {}).password; + end + + function provider.set_password(username, password) + 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) + 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 + return true; + end + + function provider.create_user(username, password) + return datamanager.store(username, host, "accounts", {password = password}); + end + + function provider.delete_user(username) + return datamanager.store(username, host, "accounts", nil); + end + + function provider.get_sasl_handler() + local getpass_authentication_profile = { + plain = function(sasl, 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(module.host, getpass_authentication_profile); + end + + return provider; +end + +module:add_item("auth-provider", new_default_provider(module.host)); + diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua index 66a79785..a747f3cb 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -10,31 +10,33 @@ module.host = "*" -- Global module local hosts = _G.hosts; local lxp = require "lxp"; -local init_xmlhandlers = require "core.xmlhandlers" -local server = require "net.server"; +local new_xmpp_stream = require "util.xmppstream".new; local httpserver = require "net.httpserver"; local sm = require "core.sessionmanager"; local sm_destroy_session = sm.destroy_session; local new_uuid = require "util.uuid".generate; -local fire_event = require "core.eventmanager".fire_event; +local fire_event = prosody.events.fire_event; local core_process_stanza = core_process_stanza; local st = require "util.stanza"; local logger = require "util.logger"; local log = logger.init("mod_bosh"); +local timer = require "util.timer"; +local xmlns_streams = "http://etherx.jabber.org/streams"; +local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; local xmlns_bosh = "http://jabber.org/protocol/httpbind"; -- (hard-coded into a literal in session.send) -local stream_callbacks = { stream_ns = "http://jabber.org/protocol/httpbind", stream_tag = "body", default_ns = "jabber:client" }; + +local stream_callbacks = { + stream_ns = xmlns_bosh, stream_tag = "body", default_ns = "jabber:client" }; 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 consider_bosh_secure = module:get_option_boolean("consider_bosh_secure"); 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 cross_domain = module:get_option("cross_domain_bosh"); if cross_domain then @@ -52,6 +54,22 @@ if cross_domain then end end +local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items; + +local function get_ip_from_request(request) + local ip = request.handler:ip(); + local forwarded_for = request.headers["x-forwarded-for"]; + if forwarded_for then + forwarded_for = forwarded_for..", "..ip; + for forwarded_ip in forwarded_for:gmatch("[^%s,]+") do + if not trusted_proxies[forwarded_ip] then + ip = forwarded_ip; + end + end + end + return ip; +end + local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; local os_time = os.time; @@ -83,7 +101,10 @@ end function handle_request(method, body, request) if (not body) or request.method ~= "POST" then if request.method == "OPTIONS" then - return { headers = default_headers, body = "" }; + local headers = {}; + for k,v in pairs(default_headers) do headers[k] = v; end + headers["Content-Type"] = nil; + return { headers = headers, body = "" }; else return "<html><body>You really don't look like a BOSH client to me... what do you want?</body></html>"; end @@ -97,12 +118,19 @@ function handle_request(method, body, request) request.log = log; request.on_destroy = on_destroy_request; - local parser = lxp.new(init_xmlhandlers(request, stream_callbacks), "\1"); - - parser:parse(body); + local stream = new_xmpp_stream(request, stream_callbacks); + -- stream:feed() calls the stream_callbacks, so all stanzas in + -- the body are processed in this next line before it returns. + stream:feed(body); local session = sessions[request.sid]; if session then + -- Session was marked as inactive, since we have + -- a request open now, unmark it + if inactive_sessions[session] then + inactive_sessions[session] = nil; + end + local r = session.requests; log("debug", "Session %s has %d out of %d requests open", request.sid, #r, session.bosh_hold); log("debug", "and there are %d things in the send_buffer", #session.send_buffer); @@ -132,11 +160,6 @@ function handle_request(method, body, request) request.reply_before = os_time() + session.bosh_wait; waiting_requests[request] = true; end - if inactive_sessions[session] then - -- Session was marked as inactive, since we have - -- a request open now, unmark it - inactive_sessions[session] = nil; - end end return true; -- Inform httpserver we shall reply later @@ -146,11 +169,42 @@ end local function bosh_reset_stream(session) session.notopen = true; end +local stream_xmlns_attr = { xmlns = "urn:ietf:params:xml:ns:xmpp-streams" }; + local function bosh_close_stream(session, reason) (session.log or log)("info", "BOSH client disconnected"); - session_close_reply.attr.condition = reason; + + local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", + ["xmlns:streams"] = xmlns_streams }); + + + if reason then + close_reply.attr.condition = "remote-stream-error"; + if type(reason) == "string" then -- assume stream error + close_reply:tag("stream:error") + :tag(reason, {xmlns = xmlns_xmpp_streams}); + elseif type(reason) == "table" then + if reason.condition then + close_reply:tag("stream:error") + :tag(reason.condition, stream_xmlns_attr):up(); + if reason.text then + close_reply:tag("text", stream_xmlns_attr):text(reason.text):up(); + end + if reason.extra then + close_reply:add_child(reason.extra); + end + elseif reason.name then -- a stanza + close_reply = reason; + end + end + log("info", "Disconnecting client, <stream:error> is: %s", tostring(close_reply)); + end + + local session_close_response = { headers = default_headers, body = tostring(close_reply) }; + + --FIXME: Quite sure we shouldn't reply to all requests with the error for _, held_request in ipairs(session.requests) do - held_request:send(session_close_reply); + held_request:send(session_close_response); held_request:destroy(); end sessions[session.sid] = nil; @@ -168,28 +222,35 @@ function stream_callbacks.streamopened(request, attr) if not hosts[attr.to] then -- Unknown host log("debug", "BOSH client tried to connect to unknown host: %s", tostring(attr.to)); - session_close_reply.body.attr.condition = "host-unknown"; - request:send(session_close_reply); - request.notopen = nil + local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", + ["xmlns:streams"] = xmlns_streams, condition = "host-unknown" }); + request:send(tostring(close_reply)); return; end -- New session sid = new_uuid(); local session = { - type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid)-1, host = attr.to, + 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 = consider_bosh_secure or request.secure + log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure, + ip = get_ip_from_request(request); }; sessions[sid] = session; + session.log("debug", "BOSH session created for request from %s", session.ip); log("info", "New BOSH session, assigned it sid '%s'", sid); local r, send_buffer = session.requests, session.send_buffer; local response = { headers = default_headers } function session.send(s) + -- We need to ensure that outgoing stanzas have the jabber:client xmlns + if s.attr and not s.attr.xmlns then + s = st.clone(s); + s.attr.xmlns = "jabber:client"; + end --log("debug", "Sending BOSH data: %s", tostring(s)); local oldest_request = r[1]; if oldest_request then @@ -213,6 +274,7 @@ function stream_callbacks.streamopened(request, attr) t_insert(session.send_buffer, tostring(s)); log("debug", "There are now %d things in the send_buffer", #session.send_buffer); end + return true; end -- Send creation response @@ -222,9 +284,17 @@ function stream_callbacks.streamopened(request, attr) fire_event("stream-features", session, features); --xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh' local response = st.stanza("body", { xmlns = xmlns_bosh, - inactivity = tostring(BOSH_DEFAULT_INACTIVITY), polling = tostring(BOSH_DEFAULT_POLLING), requests = tostring(BOSH_DEFAULT_REQUESTS), hold = tostring(session.bosh_hold), maxpause = "120", - sid = sid, authid = sid, ver = '1.6', from = session.host, secure = 'true', ["xmpp:version"] = "1.0", - ["xmlns:xmpp"] = "urn:xmpp:xbosh", ["xmlns:stream"] = "http://etherx.jabber.org/streams" }):add_child(features); + wait = attr.wait, + inactivity = tostring(BOSH_DEFAULT_INACTIVITY), + polling = tostring(BOSH_DEFAULT_POLLING), + requests = tostring(BOSH_DEFAULT_REQUESTS), + hold = tostring(session.bosh_hold), + sid = sid, authid = sid, + ver = '1.6', from = session.host, + secure = 'true', ["xmpp:version"] = "1.0", + ["xmlns:xmpp"] = "urn:xmpp:xbosh", + ["xmlns:stream"] = "http://etherx.jabber.org/streams" + }):add_child(features); request:send{ headers = default_headers, body = tostring(response) }; request.sid = sid; @@ -249,6 +319,7 @@ function stream_callbacks.streamopened(request, attr) -- Repeated, ignore session.log("debug", "rid repeated (on request %s), ignoring: %s (diff %d)", request.id, session.rid, diff); request.notopen = nil; + request.ignore = true; request.sid = sid; t_insert(session.requests, request); return; @@ -277,17 +348,32 @@ function stream_callbacks.streamopened(request, attr) end function stream_callbacks.handlestanza(request, stanza) + if request.ignore then return; end log("debug", "BOSH stanza received: %s\n", stanza:top_tag()); local session = sessions[request.sid]; if session then if stanza.attr.xmlns == xmlns_bosh then stanza.attr.xmlns = nil; end - session.ip = request.handler:ip(); core_process_stanza(session, stanza); end end +function stream_callbacks.error(request, error) + log("debug", "Error parsing BOSH request payload; %s", error); + if not request.sid then + request:send({ headers = default_headers, status = "400 Bad Request" }); + return; + end + + local session = sessions[request.sid]; + if error == "stream-error" then -- Remote stream error, we close normally + session:close(); + else + session:close({ condition = "bad-format", text = "Error processing stream" }); + end +end + local dead_sessions = {}; function on_timer() -- log("debug", "Checking for requests soon to timeout..."); @@ -325,13 +411,14 @@ function on_timer() dead_sessions[i] = nil; sm_destroy_session(session, "BOSH client silent for over "..session.bosh_max_inactive.." seconds"); end + return 1; end local function setup() local ports = module:get_option("bosh_ports") or { 5280 }; httpserver.new_from_config(ports, handle_request, { base = "http-bind" }); - server.addtimer(on_timer); + timer.add_task(1, on_timer); end if prosody.start_time then -- already started setup(); diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua index 7efb4f9c..fda271dd 100644 --- a/plugins/mod_component.lua +++ b/plugins/mod_component.lua @@ -14,59 +14,87 @@ local hosts = _G.hosts; local t_concat = table.concat; -local config = require "core.configmanager"; -local cm_register_component = require "core.componentmanager".register_component; -local cm_deregister_component = require "core.componentmanager".deregister_component; local sha1 = require "util.hashes".sha1; local st = require "util.stanza"; local log = module._log; +local main_session, send; + +local function on_destroy(session, err) + if main_session == session then + main_session = nil; + send = nil; + session.on_destroy = nil; + end +end + +local function handle_stanza(event) + local stanza = event.stanza; + if send then + stanza.attr.xmlns = nil; + send(stanza); + else + 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 + event.origin.send(st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable")); + end + end + return true; +end + +module:hook("iq/bare", handle_stanza, -1); +module:hook("message/bare", handle_stanza, -1); +module:hook("presence/bare", handle_stanza, -1); +module:hook("iq/full", handle_stanza, -1); +module:hook("message/full", handle_stanza, -1); +module:hook("presence/full", handle_stanza, -1); +module:hook("iq/host", handle_stanza, -1); +module:hook("message/host", handle_stanza, -1); +module:hook("presence/host", handle_stanza, -1); + --- Handle authentication attempts by components -function handle_component_auth(session, stanza) - log("info", "Handling component auth"); +function handle_component_auth(event) + local session, stanza = event.origin, event.stanza; + + if session.type ~= "component" then return; end + if main_session == session then return; end + if (not session.host) or #stanza.tags > 0 then - (session.log or log)("warn", "Component handshake invalid"); + (session.log or log)("warn", "Invalid component handshake for host: %s", session.host); session:close("not-authorized"); - return; + return true; end - local secret = config.get(session.user, "core", "component_secret"); + local secret = module:get_option("component_secret"); if not secret then - (session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.user); + (session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.host); session:close("not-authorized"); - return; + return true; end local supplied_token = t_concat(stanza); local calculated_token = sha1(session.streamid..secret, true); if supplied_token:lower() ~= calculated_token:lower() then - log("info", "Component for %s authentication failed", session.host); + log("info", "Component authentication failed for %s", session.host); session:close{ condition = "not-authorized", text = "Given token does not match calculated token" }; - return; + return true; end - - -- Authenticated now - log("info", "Component authenticated: %s", session.host); - -- If component not already created for this host, create one now - if not hosts[session.host].connected then - local send = session.send; - session.component_session = cm_register_component(session.host, function (_, data) - if data.attr and data.attr.xmlns == "jabber:client" then - data.attr.xmlns = nil; - end - return send(data); - end); - hosts[session.host].connected = true; - log("info", "Component successfully registered"); - else - log("error", "Multiple components bound to the same address, first one wins (TODO: Implement stanza distribution)"); + if not main_session then + send = session.send; + main_session = session; + session.on_destroy = on_destroy; + session.component_validate_from = module:get_option_boolean("validate_from_addresses") ~= false; + log("info", "Component successfully authenticated: %s", session.host); + session.send(st.stanza("handshake")); + else -- TODO: Implement stanza distribution + log("error", "Multiple components bound to the same address, first one wins: %s", session.host); + session:close{ condition = "conflict", text = "Component already connected" }; end - -- Signal successful authentication - session.send(st.stanza("handshake")); + return true; end -module:add_handler("component", "handshake", "jabber:component:accept", handle_component_auth); +module:hook("stanza/jabber:component:accept:handshake", handle_component_auth); diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua index 53341492..82403016 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 @@ -38,7 +39,7 @@ end); module:hook("s2s-stream-features", function(event) local origin, features = event.origin, event.features; -- FIXME only advertise compression support when TLS layer has no compression enabled - if not origin.compressed then + if not origin.compressed then features:add_child(compression_stream_feature); end end); @@ -94,121 +95,108 @@ 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, - function(session ,stanza) - session.log("debug", "Activating compression...") +module:hook("stanza/http://jabber.org/protocol/compress:compressed", function(event) + local session = event.origin; + + if session.type == "s2sout_unauthed" or session.type == "s2sout" then + session.log("debug", "Activating compression...") + -- create deflate and inflate streams + local deflate_stream = get_deflate_stream(session); + if not deflate_stream then return true; end + + local inflate_stream = get_inflate_stream(session); + if not inflate_stream then return true; end + + -- setup compression for session.w + setup_compression(session, deflate_stream); + + -- setup decompression for session.data + setup_decompression(session, inflate_stream); + 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}; + session.sends2s("<?xml version='1.0'?>"); + session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag()); + session.compressed = true; + return true; + end +end); + +module:hook("stanza/http://jabber.org/protocol/compress:compress", function(event) + local session, stanza = event.origin, event.stanza; + + if session.type == "c2s" or session.type == "s2sin" or session.type == "c2s_unauthed" or session.type == "s2sin_unauthed" then + -- fail if we are already compressed + if session.compressed then + local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); + (session.sends2s or session.send)(error_st); + session.log("debug", "Client tried to establish another compression layer."); + return true; + end + + -- checking if the compression method is supported + local method = stanza:child_with_name("method"); + method = method and (method[1] or ""); + if method == "zlib" then + session.log("debug", "zlib compression enabled."); + -- create deflate and inflate streams local deflate_stream = get_deflate_stream(session); - if not deflate_stream then return end + if not deflate_stream then return true; end local inflate_stream = get_inflate_stream(session); - if not inflate_stream then return end + if not inflate_stream then return true; end + + (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol})); + session:reset_stream(); -- setup compression for session.w setup_compression(session, deflate_stream); -- 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}; - session.sends2s("<?xml version='1.0'?>"); - session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag()); - session.compressed = true; - end -); - -module:add_handler({"c2s_unauthed", "c2s", "s2sin_unauthed", "s2sin"}, "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("setup-failed"); - (session.sends2s or session.send)(error_st); - session.log("debug", "Client tried to establish another compression layer."); - return; - end - -- checking if the compression method is supported - local method = stanza:child_with_name("method"); - method = method and (method[1] or ""); - if method == "zlib" then - session.log("debug", "zlib compression enabled."); - - -- create deflate and inflate streams - local deflate_stream = get_deflate_stream(session); - if not deflate_stream then return end - - local inflate_stream = get_inflate_stream(session); - if not inflate_stream then return end - - (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol})); - session:reset_stream(); - - -- setup compression for session.w - setup_compression(session, deflate_stream); - - -- 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)); - local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method"); - (session.sends2s or session.send)(error_st); - else - (session.sends2s or session.send)(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed")); - end + session.compressed = true; + elseif method then + session.log("debug", "%s compression selected, but we don't support it.", tostring(method)); + local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method"); + (session.sends2s or session.send)(error_st); + else + (session.sends2s or session.send)(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed")); end -); + return true; + end +end); diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua index 189aeb36..8c80dce6 100644 --- a/plugins/mod_dialback.lua +++ b/plugins/mod_dialback.lua @@ -12,7 +12,6 @@ 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; local log = module._log; @@ -23,8 +22,10 @@ local xmlns_dialback = "jabber:server:dialback"; local dialback_requests = setmetatable({}, { __mode = 'v' }); -module:add_handler({"s2sin_unauthed", "s2sin"}, "verify", xmlns_dialback, - function (origin, stanza) +module:hook("stanza/jabber:server:dialback:verify", function(event) + local origin, stanza = event.origin, event.stanza; + + if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then -- We are being asked to verify the key, to ensure it was generated by us origin.log("debug", "verifying that dialback key is ours..."); local attr = stanza.attr; @@ -39,10 +40,14 @@ module:add_handler({"s2sin_unauthed", "s2sin"}, "verify", xmlns_dialback, end origin.log("debug", "verified dialback key... it is %s", type); origin.sends2s(st.stanza("db:verify", { from = attr.to, to = attr.from, id = attr.id, type = type }):text(stanza[1])); - end); + return true; + end +end); -module:add_handler({ "s2sin_unauthed", "s2sin" }, "result", xmlns_dialback, - function (origin, stanza) +module:hook("stanza/jabber:server:dialback:result", function(event) + local origin, stanza = event.origin, event.stanza; + + if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then -- he wants to be identified through dialback -- We need to check the key with the Authoritative server local attr = stanza.attr; @@ -52,7 +57,7 @@ module:add_handler({ "s2sin_unauthed", "s2sin" }, "result", xmlns_dialback, -- Not a host that we serve origin.log("info", "%s tried to connect to %s, which we don't serve", attr.from, attr.to); origin:close("host-unknown"); - return; + return true; end dialback_requests[attr.from] = origin; @@ -69,10 +74,14 @@ module:add_handler({ "s2sin_unauthed", "s2sin" }, "result", xmlns_dialback, origin.log("debug", "asking %s if key %s belongs to them", attr.from, stanza[1]); send_s2s(attr.to, attr.from, st.stanza("db:verify", { from = attr.to, to = attr.from, id = origin.streamid }):text(stanza[1])); - end); + return true; + end +end); -module:add_handler({ "s2sout_unauthed", "s2sout" }, "verify", xmlns_dialback, - function (origin, stanza) +module:hook("stanza/jabber:server:dialback:verify", function(event) + local origin, stanza = event.origin, event.stanza; + + if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then local attr = stanza.attr; local dialback_verifying = dialback_requests[attr.from]; if dialback_verifying then @@ -94,34 +103,40 @@ module:add_handler({ "s2sout_unauthed", "s2sout" }, "verify", xmlns_dialback, end dialback_requests[attr.from] = nil; end - end); + return true; + end +end); -module:add_handler({ "s2sout_unauthed", "s2sout" }, "result", xmlns_dialback, - function (origin, stanza) +module:hook("stanza/jabber:server:dialback:result", function(event) + local origin, stanza = event.origin, event.stanza; + + if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then -- Remote server is telling us whether we passed dialback local attr = stanza.attr; if not hosts[attr.to] then origin:close("host-unknown"); - return; + return true; elseif hosts[attr.to].s2sout[attr.from] ~= origin then -- This isn't right origin:close("invalid-id"); - return; + return true; end if stanza.attr.type == "valid" then s2s_make_authenticated(origin, attr.from); else - s2s_destroy_session(origin) + origin:close("not-authorized", "dialback authentication failed"); end - end); + return true; + end +end); module:hook_stanza(xmlns_stream, "features", function (origin, stanza) - s2s_initiate_dialback(origin); - return true; - end, 100); + 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); + 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 ee0043f1..907ca753 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -6,11 +6,12 @@ -- COPYING file in the source package for more information. -- -local componentmanager_get_children = require "core.componentmanager".get_children; +local get_children = require "core.hostmanager".get_children; local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; local jid_split = require "util.jid".split; local jid_bare = require "util.jid".bare; local st = require "util.stanza" +local calculate_hash = require "util.caps".calculate_hash; local disco_items = module:get_option("disco_items") or {}; do -- validate disco_items @@ -35,27 +36,63 @@ module:add_identity("server", "im", "Prosody"); -- FIXME should be in the non-ex module:add_feature("http://jabber.org/protocol/disco#info"); module:add_feature("http://jabber.org/protocol/disco#items"); -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"); +-- Generate and cache disco result and caps hash +local _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash; +local function build_server_disco_info() + local query = st.stanza("query", { xmlns = "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(); + query: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(); + query:tag("feature", {var=feature}):up(); done[feature] = true; end end + _cached_server_disco_info = query; + _cached_server_caps_hash = calculate_hash(query); + _cached_server_caps_feature = st.stanza("c", { + xmlns = "http://jabber.org/protocol/caps"; + hash = "sha-1"; + node = "http://prosody.im"; + ver = _cached_server_caps_hash; + }); +end +local function clear_disco_cache() + _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash = nil, nil, nil; +end +local function get_server_disco_info() + if not _cached_server_disco_info then build_server_disco_info(); end + return _cached_server_disco_info; +end +local function get_server_caps_feature() + if not _cached_server_caps_feature then build_server_disco_info(); end + return _cached_server_caps_feature; +end +local function get_server_caps_hash() + if not _cached_server_caps_hash then build_server_disco_info(); end + return _cached_server_caps_hash; +end + +module:hook("item-added/identity", clear_disco_cache); +module:hook("item-added/feature", clear_disco_cache); +module:hook("item-removed/identity", clear_disco_cache); +module:hook("item-removed/feature", clear_disco_cache); + +-- Handle disco requests to the server +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 ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then return; end -- TODO fire event? + local reply_query = get_server_disco_info(); + reply_query.node = node; + local reply = st.reply(stanza):add_child(reply_query); origin.send(reply); return true; end); @@ -66,7 +103,7 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve 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 + for jid in pairs(get_children(module.host)) do reply:tag("item", {jid = jid}):up(); end for _, item in ipairs(disco_items) do @@ -75,6 +112,15 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve origin.send(reply); return true; end); + +-- Handle caps stream feature +module:hook("stream-features", function (event) + if event.origin.type == "c2s" then + event.features:add_child(get_server_caps_feature()); + end +end); + +-- Handle disco requests to user accounts module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(event) local origin, stanza = event.origin, event.stanza; if stanza.attr.type ~= "get" then return; end @@ -84,7 +130,7 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(even if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account - module:fire_event("account-disco-info", { session = origin, stanza = reply }); + module:fire_event("account-disco-info", { origin = origin, stanza = reply }); origin.send(reply); return true; end @@ -98,7 +144,7 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(eve if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items'}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account - module:fire_event("account-disco-items", { session = origin, stanza = reply }); + module:fire_event("account-disco-items", { origin = origin, stanza = reply }); origin.send(reply); return true; end 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_httpserver.lua b/plugins/mod_httpserver.lua index c55bd20f..654aff06 100644 --- a/plugins/mod_httpserver.lua +++ b/plugins/mod_httpserver.lua @@ -8,9 +8,11 @@ local httpserver = require "net.httpserver"; +local lfs = require "lfs"; local open = io.open; local t_concat = table.concat; +local stat = lfs.attributes; local http_base = config.get("*", "core", "http_path") or "www_files"; @@ -48,7 +50,14 @@ local function preprocess_path(path) end function serve_file(path) - local f, err = open(http_base..path, "rb"); + local full_path = http_base..path; + if stat(full_path, "mode") == "directory" then + if stat(full_path.."/index.html", "mode") == "file" then + return serve_file(path.."/index.html"); + end + return response_403; + end + local f, err = open(full_path, "rb"); if not f then return response_404; end local data = f:read("*a"); f:close(); diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua index b3001fe5..484a1f8f 100644 --- a/plugins/mod_iq.lua +++ b/plugins/mod_iq.lua @@ -9,70 +9,70 @@ 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; -module:hook("iq/full", function(data) - -- IQ to full JID recieved - local origin, stanza = data.origin, data.stanza; +if module:get_host_type() == "local" then + module:hook("iq/full", function(data) + -- IQ to full JID recieved + local origin, stanza = data.origin, data.stanza; - local session = full_sessions[stanza.attr.to]; - if session then - -- TODO fire post processing event - session.send(stanza); - else -- resource not online - if stanza.attr.type == "get" or stanza.attr.type == "set" then - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + local session = full_sessions[stanza.attr.to]; + if session then + -- TODO fire post processing event + session.send(stanza); + else -- resource not online + if stanza.attr.type == "get" or stanza.attr.type == "set" then + origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); + end end - end - return true; -end); + return true; + end); +end module:hook("iq/bare", function(data) -- IQ to bare JID recieved local origin, stanza = data.origin, data.stanza; + local type = stanza.attr.type; - 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); + if type == "get" or type == "set" then + local child = stanza.tags[1]; + local ret = module:fire_event("iq/bare/"..child.attr.xmlns..":"..child.name, data); + if ret ~= nil then return ret; end + return module:fire_event("iq-"..type.."/bare/"..child.attr.xmlns..":"..child.name, data); else - module:fire_event("iq/bare/"..stanza.attr.id, data); - return true; + return module:fire_event("iq-"..type.."/bare/"..stanza.attr.id, data); end end); module:hook("iq/self", function(data) - -- IQ to bare JID recieved + -- IQ to self JID recieved local origin, stanza = data.origin, data.stanza; + local type = stanza.attr.type; - if stanza.attr.type == "get" or stanza.attr.type == "set" then - return module:fire_event("iq/self/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data); + if type == "get" or type == "set" then + local child = stanza.tags[1]; + local ret = module:fire_event("iq/self/"..child.attr.xmlns..":"..child.name, data); + if ret ~= nil then return ret; end + return module:fire_event("iq-"..type.."/self/"..child.attr.xmlns..":"..child.name, data); else - module:fire_event("iq/self/"..stanza.attr.id, data); - return true; + return module:fire_event("iq-"..type.."/self/"..stanza.attr.id, data); end end); module:hook("iq/host", function(data) -- IQ to a local host recieved local origin, stanza = data.origin, data.stanza; + local type = stanza.attr.type; - if stanza.attr.type == "get" or stanza.attr.type == "set" then - return module:fire_event("iq/host/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data); + if type == "get" or type == "set" then + local child = stanza.tags[1]; + local ret = module:fire_event("iq/host/"..child.attr.xmlns..":"..child.name, data); + if ret ~= nil then return ret; end + return module:fire_event("iq-"..type.."/host/"..child.attr.xmlns..":"..child.name, data); else - module:fire_event("iq/host/"..stanza.attr.id, data); - return true; + return module:fire_event("iq-"..type.."/host/"..stanza.attr.id, data); end end); diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index 0134d736..a47f0223 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -11,7 +11,9 @@ local st = require "util.stanza"; local t_concat = table.concat; -local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption"); +local secure_auth_only = module:get_option("c2s_require_encryption") + or module:get_option("require_encryption") + or not(module:get_option("allow_unencrypted_plain_auth")); local sessionmanager = require "core.sessionmanager"; local usermanager = require "core.usermanager"; @@ -29,47 +31,53 @@ module:hook("stream-features", function(event) end end); -module:add_iq_handler("c2s_unauthed", "jabber:iq:auth", - function (session, stanza) - if secure_auth_only and not session.secure then - session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server")); - return true; - end - - local username = stanza.tags[1]:child_with_name("username"); - local password = stanza.tags[1]:child_with_name("password"); - local resource = stanza.tags[1]:child_with_name("resource"); - if not (username and password and resource) then - local reply = st.reply(stanza); - session.send(reply:query("jabber:iq:auth") - :tag("username"):up() - :tag("password"):up() - :tag("resource"):up()); - else - username, password, resource = t_concat(username), t_concat(password), t_concat(resource); - username = nodeprep(username); - resource = resourceprep(resource) - local reply = st.reply(stanza); - if usermanager.validate_credentials(session.host, username, password) then - -- Authentication successful! - local success, err = sessionmanager.make_authenticated(session, username); - if success then - local err_type, err_msg; - 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)); - else - session.send(st.error_reply(stanza, "auth", "not-authorized")); +module:hook("stanza/iq/jabber:iq:auth:query", function(event) + local session, stanza = event.origin, event.stanza; + + if session.type ~= "c2s_unauthed" then + session.send(st.error_reply(stanza, "cancel", "service-unavailable", "Legacy authentication is only allowed for unauthenticated client connections.")); + return true; + end + + if secure_auth_only and not session.secure then + session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server")); + return true; + end + + local username = stanza.tags[1]:child_with_name("username"); + local password = stanza.tags[1]:child_with_name("password"); + local resource = stanza.tags[1]:child_with_name("resource"); + if not (username and password and resource) then + local reply = st.reply(stanza); + session.send(reply:query("jabber:iq:auth") + :tag("username"):up() + :tag("password"):up() + :tag("resource"):up()); + else + username, password, resource = t_concat(username), t_concat(password), t_concat(resource); + username = nodeprep(username); + resource = resourceprep(resource) + local reply = st.reply(stanza); + if usermanager.test_password(username, session.host, password) then + -- Authentication successful! + local success, err = sessionmanager.make_authenticated(session, username); + if success then + local err_type, err_msg; + 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 - return true; - end); + session.send(st.reply(stanza)); + else + session.send(st.error_reply(stanza, "auth", "not-authorized")); + end + end + return true; +end); diff --git a/plugins/mod_message.lua b/plugins/mod_message.lua index d5b40ed5..df317532 100644 --- a/plugins/mod_message.lua +++ b/plugins/mod_message.lua @@ -14,7 +14,6 @@ local st = require "util.stanza"; local jid_bare = require "util.jid".bare; local jid_split = require "util.jid".split; local user_exists = require "core.usermanager".user_exists; -local offlinemanager = require "core.offlinemanager"; local t_insert = table.insert; local function process_to_bare(bare, origin, stanza) @@ -26,7 +25,7 @@ local function process_to_bare(bare, origin, stanza) elseif t == "groupchat" then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); elseif t == "headline" then - if user then + if user and stanza.attr.to == bare then for _, session in pairs(user.sessions) do if session.presence and session.priority >= 0 then session.send(stanza); @@ -45,10 +44,17 @@ local function process_to_bare(bare, origin, stanza) end -- no resources are online local node, host = jid_split(bare); + local ok if user_exists(node, host) then -- TODO apply the default privacy list - offlinemanager.store(node, host, stanza); - else + + ok = module:fire_event('message/offline/handle', { + origin = origin, + stanza = stanza, + }); + end + + if not ok then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end end diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua new file mode 100644 index 00000000..462670e6 --- /dev/null +++ b/plugins/mod_motd.lua @@ -0,0 +1,27 @@ +-- 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"; + +motd_text = motd_text:gsub("^%s*(.-)%s*$", "%1"):gsub("\n%s+", "\n"); -- Strip indentation from the config + +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..1ac62f94 --- /dev/null +++ b/plugins/mod_offline.lua @@ -0,0 +1,51 @@ +-- 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/handle", 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 result; +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 + datamanager.list_store(node, host, "offline", nil); + return true; +end); diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index aa46d2d3..bd6f4b29 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -16,9 +16,7 @@ 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; +local calculate_hash = require "util.caps".calculate_hash; local NULL = {}; local data = {}; @@ -40,8 +38,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,25 +116,44 @@ 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; + local self = not stanza.attr.to; - 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 self 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 or (current and current == hash_map[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; + local from_bare = origin.type == "c2s" and origin.username.."@"..origin.host; + if self or origin.type ~= "c2s" or (recipients[from_bare] and recipients[from_bare][origin.full_jid]) ~= hash then + 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 + end + elseif t == "unavailable" then + if recipients[user] then recipients[user][stanza.attr.from] = nil; end + elseif not self and t == "unsubscribe" then + local from = jid_bare(stanza.attr.from); + local subscriptions = recipients[user]; + if subscriptions then + for subscriber in pairs(subscriptions) do + if jid_bare(subscriber) == from then + recipients[user][subscriber] = nil; + end end end end @@ -205,56 +222,13 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) end end); -local function calculate_hash(disco_info) - local identities, features, extensions = {}, {}, {}; - for _, tag in pairs(disco_info) do - if tag.name == "identity" then - table.insert(identities, (tag.attr.category or "").."\0"..(tag.attr.type or "").."\0"..(tag.attr["xml:lang"] or "").."\0"..(tag.attr.name or "")); - elseif tag.name == "feature" then - table.insert(features, tag.attr.var or ""); - elseif tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then - local form = {}; - local FORM_TYPE; - for _, field in pairs(tag.tags) do - if field.name == "field" and field.attr.var then - local values = {}; - for _, val in pairs(field.tags) do - val = #val.tags == 0 and table.concat(val); -- FIXME use get_text? - if val then table.insert(values, val); end - end - table.sort(values); - if field.attr.var == "FORM_TYPE" then - FORM_TYPE = values[1]; - elseif #values > 0 then - table.insert(form, field.attr.var.."\0"..table.concat(values, "<")); - else - table.insert(form, field.attr.var); - end - end - end - table.sort(form); - form = table.concat(form, "<"); - if FORM_TYPE then form = FORM_TYPE.."\0"..form; end - table.insert(extensions, form); - end - end - table.sort(identities); - table.sort(features); - table.sort(extensions); - if #identities > 0 then identities = table.concat(identities, "<"):gsub("%z", "/").."<"; else identities = ""; end - if #features > 0 then features = table.concat(features, "<").."<"; else features = ""; end - if #extensions > 0 then extensions = table.concat(extensions, "<"):gsub("%z", "<").."<"; else extensions = ""; end - local S = identities..features..extensions; - local ver = base64(sha1(S)); - return ver, S; -end - -module:hook("iq/bare/disco", function(event) +module:hook("iq-result/bare/disco", function(event) local session, stanza = event.origin, event.stanza; if stanza.attr.type == "result" then local disco = stanza.tags[1]; if disco and disco.name == "query" and disco.attr.xmlns == "http://jabber.org/protocol/disco#info" then -- Process disco response + local self = not stanza.attr.to; local user = stanza.attr.to or (session.username..'@'..session.host); local contact = stanza.attr.from; local current = recipients[user] and recipients[user][contact]; @@ -271,6 +245,15 @@ module:hook("iq/bare/disco", function(event) end end hash_map[ver] = notify; -- update hash map + if self then + for jid, item in pairs(session.roster) do -- for all interested contacts + if item.subscription == "both" or item.subscription == "from" then + if not recipients[jid] then recipients[jid] = {}; end + recipients[jid][contact] = notify; + publish_all(jid, contact, session); + end + end + end recipients[user][contact] = notify; -- set recipient's data to calculated data -- send messages to recipient publish_all(user, contact, session); @@ -285,8 +268,8 @@ module:hook("account-disco-info", function(event) end); module:hook("account-disco-items", function(event) - local session, stanza = event.session, event.stanza; - local bare = session.username..'@'..session.host; + local stanza = event.stanza; + local bare = stanza.attr.to; local user_data = data[bare]; if user_data then diff --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua index 61b717a2..0bfcac66 100644 --- a/plugins/mod_ping.lua +++ b/plugins/mod_ping.lua @@ -19,3 +19,17 @@ end module:hook("iq/bare/urn:xmpp:ping:ping", ping_handler); module:hook("iq/host/urn:xmpp:ping:ping", ping_handler); + +-- Ad-hoc command + +local datetime = require "util.datetime".datetime; + +function ping_command_handler (self, data, state) + local now = datetime(); + return { info = "Pong\n"..now, status = "completed" }; +end + +local adhoc_new = module:require "adhoc".new; +local descriptor = adhoc_new("Ping", "ping", ping_command_handler); +module:add_item ("adhoc", descriptor); + diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua index c38f7eba..d229c1b8 100644 --- a/plugins/mod_posix.lua +++ b/plugins/mod_posix.lua @@ -7,7 +7,7 @@ -- -local want_pposix_version = "0.3.3"; +local want_pposix_version = "0.3.5"; local pposix = assert(require "util.pposix"); if pposix._VERSION ~= want_pposix_version then module:log("warn", "Unknown version (%s) of binary pposix module, expected %s", tostring(pposix._VERSION), want_pposix_version); end @@ -17,8 +17,6 @@ if type(signal) == "string" then module:log("warn", "Couldn't load signal library, won't respond to SIGTERM"); end -local logger_set = require "util.logger".setwriter; - local lfs = require "lfs"; local stat = lfs.attributes; @@ -30,7 +28,7 @@ local umask = module:get_option("umask") or "027"; pposix.umask(umask); -- Allow switching away from root, some people like strange ports. -module:add_event_hook("server-started", function () +module:hook("server-started", function () local uid = module:get_option("setuid"); local gid = module:get_option("setgid"); if gid then @@ -54,16 +52,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; @@ -95,14 +93,23 @@ local function write_pidfile() pidfile_handle = nil; prosody.shutdown("Prosody already running"); else - pidfile_handle:write(tostring(pposix.getpid())); - pidfile_handle:flush(); + pidfile_handle:close(); + pidfile_handle, err = io.open(pidfile, "w+"); + if not pidfile_handle then + module:log("error", "Couldn't write pidfile at %s; %s", pidfile, err); + prosody.shutdown("Couldn't write pidfile"); + else + if lfs.lock(pidfile_handle, "w") then + pidfile_handle:write(tostring(pposix.getpid())); + pidfile_handle:flush(); + end + end end end end end -local syslog_opened +local syslog_opened; function syslog_sink_maker(config) if not syslog_opened then pposix.syslog_open("prosody"); @@ -110,12 +117,12 @@ function syslog_sink_maker(config) end local syslog, format = pposix.syslog_log, string.format; return function (name, level, message, ...) - if ... then - syslog(level, format(message, ...)); - else - syslog(level, message); - end - end; + if ... then + syslog(level, format(message, ...)); + else + syslog(level, message); + end + end; end require "core.loggingmanager".register_sink_type("syslog", syslog_sink_maker); @@ -141,13 +148,15 @@ 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(); end -module:add_event_hook("server-stopped", remove_pidfile); +module:hook("server-stopped", remove_pidfile); -- Set signal handlers if signal.signal then diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua index 4fb8c3e4..6d039d83 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -22,21 +22,6 @@ local NULL = {}; 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; @@ -64,7 +49,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 @@ -73,6 +58,15 @@ function handle_normal_presence(origin, stanza, core_route_stanza) priority[1] = "0"; end end + local priority = stanza:child_with_name("priority"); + if priority and #priority > 0 then + priority = t_concat(priority); + if s_find(priority, "^[+-]?[0-9]+$") then + priority = tonumber(priority); + if priority < -128 then priority = -128 end + if priority > 127 then priority = 127 end + else priority = 0; end + else priority = 0; end if full_sessions[origin.full_jid] then -- if user is still connected origin.send(stanza); -- reflect their presence back to them end @@ -82,13 +76,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 +91,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,15 +110,13 @@ 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); - if offline then - for _, msg in ipairs(offline) do - origin.send(msg); -- FIXME do we need to modify to/from in any way? - end - offlinemanager.deleteAll(node, host); + + if priority >= 0 then + local event = { origin = origin } + module:fire_event('message/offline/broadcast', event); end end if stanza.attr.type == "unavailable" then @@ -136,21 +128,12 @@ 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 else origin.presence = stanza; - local priority = stanza:child_with_name("priority"); - if priority and #priority > 0 then - priority = t_concat(priority); - if s_find(priority, "^[+-]?[0-9]+$") then - priority = tonumber(priority); - if priority < -128 then priority = -128 end - if priority > 127 then priority = 127 end - else priority = 0; end - else priority = 0; end if origin.priority ~= priority then origin.priority = priority; recalc_resource_map(user); @@ -159,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, 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 +153,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 +164,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 + if to_bare == from_bare 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 +194,23 @@ 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); + else + origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type")); 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 +219,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); @@ -266,8 +255,11 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b sessionmanager.send_to_interested_resources(node, host, stanza); rostermanager.roster_push(node, host, from_bare); end - end -- discard any other type + else + origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type")); + end stanza.attr.from, stanza.attr.to = st_from, st_to; + return true; end local outbound_presence_handler = function(data) @@ -278,12 +270,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 +298,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 +310,9 @@ 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); + else + origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type")); end return true; end); @@ -329,8 +322,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 +339,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 +361,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 aa953310..2d696154 100644 --- a/plugins/mod_privacy.lua +++ b/plugins/mod_privacy.lua @@ -45,28 +45,6 @@ function isAnotherSessionUsingDefaultList(origin) end end -function sendUnavailable(origin, to, from) ---[[ example unavailable presence stanza -<presence from="node@host/resource" type="unavailable" to="node@host" > - <status>Logged out</status> -</presence> -]]-- - local presence = st.presence({from=from, type="unavailable"}); - presence:tag("status"):text("Logged out"); - - local node, host = jid_bare(to); - local bare = node .. "@" .. host; - - local user = bare_sessions[bare]; - if user then - for resource, session in pairs(user.sessions) do - presence.attr.to = session.full_jid; - module:log("debug", "send unavailable to: %s; from: %s", tostring(presence.attr.to), tostring(presence.attr.from)); - origin.send(presence); - end - end -end - function declineList(privacy_lists, origin, stanza, which) if which == "default" then if isAnotherSessionUsingDefaultList(origin) then @@ -123,7 +101,7 @@ function deleteList(privacy_lists, origin, stanza, name) return {"modify", "bad-request", "Not existing list specifed to be deleted."}; end -function createOrReplaceList (privacy_lists, origin, stanza, name, entries, roster) +function createOrReplaceList (privacy_lists, origin, stanza, name, entries) local bare_jid = origin.username.."@"..origin.host; if privacy_lists.lists == nil then @@ -203,7 +181,7 @@ function getList(privacy_lists, origin, stanza, name) if name == nil then if privacy_lists.lists then - if origin.ActivePrivacyList then + if origin.activePrivacyList then reply:tag("active", {name=origin.activePrivacyList}):up(); end if privacy_lists.default then @@ -323,7 +301,6 @@ function checkIfNeedToBeBlocked(e, session) return; -- from one of a user's resource to another => HANDS OFF! end - local item; local listname = session.activePrivacyList; if listname == nil then listname = privacy_lists.default; -- no active list selected, use default list @@ -414,7 +391,6 @@ function preCheckIncoming(e) end if resource == nil then local prio = 0; - local session_; if bare_sessions[node.."@"..host] ~= nil then for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do if session_.priority ~= nil and session_.priority > prio then @@ -442,7 +418,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_private.lua b/plugins/mod_private.lua index abf1ec03..f1ebe786 100644 --- a/plugins/mod_private.lua +++ b/plugins/mod_private.lua @@ -7,7 +7,6 @@ -- - local st = require "util.stanza" local jid_split = require "util.jid".split; @@ -15,47 +14,40 @@ local datamanager = require "util.datamanager" module:add_feature("jabber:iq:private"); -module:add_iq_handler("c2s", "jabber:iq:private", - function (session, stanza) - local type = stanza.attr.type; - local query = stanza.tags[1]; - if (type == "get" or type == "set") and query.name == "query" then - local node, host = jid_split(stanza.attr.to); - if not(node or host) or (node == session.username and host == session.host) then - node, host = session.username, session.host; - if #query.tags == 1 then - local tag = query.tags[1]; - local key = tag.name..":"..tag.attr.xmlns; - local data, err = datamanager.load(node, host, "private"); - if err then - session.send(st.error_reply(stanza, "wait", "internal-server-error")); - return true; - end - if stanza.attr.type == "get" then - if data and data[key] then - session.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"}):add_child(st.deserialize(data[key]))); - else - session.send(st.reply(stanza):add_child(stanza.tags[1])); - end - else -- set - if not data then data = {}; end; - if #tag == 0 then - data[key] = nil; - else - data[key] = st.preserialize(tag); - end - -- TODO delete datastore if empty - if datamanager.store(node, host, "private", data) then - session.send(st.reply(stanza)); - else - session.send(st.error_reply(stanza, "wait", "internal-server-error")); - end - end - else - session.send(st.error_reply(stanza, "modify", "bad-format")); - end +module:hook("iq/self/jabber:iq:private:query", function(event) + local origin, stanza = event.origin, event.stanza; + local type = stanza.attr.type; + local query = stanza.tags[1]; + if #query.tags == 1 then + local tag = query.tags[1]; + local key = tag.name..":"..tag.attr.xmlns; + local data, err = datamanager.load(origin.username, origin.host, "private"); + if err then + origin.send(st.error_reply(stanza, "wait", "internal-server-error")); + return true; + end + if stanza.attr.type == "get" then + if data and data[key] then + origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"}):add_child(st.deserialize(data[key]))); + else + origin.send(st.reply(stanza):add_child(stanza.tags[1])); + end + else -- set + if not data then data = {}; end; + if #tag == 0 then + data[key] = nil; + else + data[key] = st.preserialize(tag); + end + -- TODO delete datastore if empty + if datamanager.store(origin.username, origin.host, "private", data) then + origin.send(st.reply(stanza)); else - session.send(st.error_reply(stanza, "cancel", "forbidden")); + origin.send(st.error_reply(stanza, "wait", "internal-server-error")); end end - end); + else + origin.send(st.error_reply(stanza, "modify", "bad-format")); + end + return true; +end); diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua index 190d30be..5b490730 100644 --- a/plugins/mod_proxy65.lua +++ b/plugins/mod_proxy65.lua @@ -10,25 +10,22 @@ module:unload("proxy65"); module:load("proxy65", <proxy65_jid>); ]]-- -if module:get_host_type() ~= "component" then - error("proxy65 should be loaded as a component, please see http://prosody.im/doc/components", 0); -end -local jid_split, jid_join = require "util.jid".split, require "util.jid".join; +local module = module; +local tostring = tostring; +local jid_split, jid_join, jid_compare = require "util.jid".split, require "util.jid".join, require "util.jid".compare; local st = require "util.stanza"; -local componentmanager = require "core.componentmanager"; -local config_get = require "core.configmanager".get; local connlisteners = require "net.connlisteners"; local sha1 = require "util.hashes".sha1; local server = require "net.server"; local host, name = module:get_host(), "SOCKS5 Bytestreams Service"; -local sessions, transfers, component, replies_cache = {}, {}, nil, {}; +local sessions, transfers, replies_cache = {}, {}, {}; -local proxy_port = config_get(host, "core", "proxy65_port") or 5000; -local proxy_interface = config_get(host, "core", "proxy65_interface") or "*"; -local proxy_address = config_get(host, "core", "proxy65_address") or (proxy_interface ~= "*" and proxy_interface) or host; -local proxy_acl = config_get(host, "core", "proxy65_acl"); +local proxy_port = module:get_option("proxy65_port") or 5000; +local proxy_interface = module:get_option("proxy65_interface") or "*"; +local proxy_address = module:get_option("proxy65_address") or (proxy_interface ~= "*" and proxy_interface) or host; +local proxy_acl = module:get_option("proxy65_acl"); local max_buffer_size = 4096; local connlistener = { default_port = proxy_port, default_interface = proxy_interface, default_mode = "*a" }; @@ -36,12 +33,12 @@ local connlistener = { default_port = proxy_port, default_interface = proxy_inte function connlistener.onincoming(conn, data) local session = sessions[conn] or {}; - if session.setup == nil and data ~= nil and data:sub(1):byte() == 0x05 and data:len() > 2 then - local nmethods = data:sub(2):byte(); + if session.setup == nil and data ~= nil and data:byte(1) == 0x05 and #data > 2 then + local nmethods = data:byte(2); local methods = data:sub(3); local supported = false; for i=1, nmethods, 1 do - if(methods:sub(i):byte() == 0x00) then -- 0x00 == method: NO AUTH + if(methods:byte(i) == 0x00) then -- 0x00 == method: NO AUTH supported = true; break; end @@ -66,14 +63,14 @@ function connlistener.onincoming(conn, data) return; end end - if data ~= nil and data:len() == 0x2F and -- 40 == length of SHA1 HASH, and 7 other bytes => 47 => 0x2F - data:sub(1):byte() == 0x05 and -- SOCKS5 has 5 in first byte - data:sub(2):byte() == 0x01 and -- CMD must be 1 - data:sub(3):byte() == 0x00 and -- RSV must be 0 - data:sub(4):byte() == 0x03 and -- ATYP must be 3 - data:sub(5):byte() == 40 and -- SHA1 HASH length must be 40 (0x28) - data:sub(-2):byte() == 0x00 and -- PORT must be 0, size 2 byte - data:sub(-1):byte() == 0x00 + if data ~= nil and #data == 0x2F and -- 40 == length of SHA1 HASH, and 7 other bytes => 47 => 0x2F + data:byte(1) == 0x05 and -- SOCKS5 has 5 in first byte + data:byte(2) == 0x01 and -- CMD must be 1 + data:byte(3) == 0x00 and -- RSV must be 0 + data:byte(4) == 0x03 and -- ATYP must be 3 + data:byte(5) == 40 and -- SHA1 HASH length must be 40 (0x28) + data:byte(-2) == 0x00 and -- PORT must be 0, size 2 byte + data:byte(-1) == 0x00 then local sha = data:sub(6, 45); -- second param is not count! it's the ending index (included!) if transfers[sha] == nil then @@ -89,7 +86,7 @@ function connlistener.onincoming(conn, data) server.link(conn, transfers[sha].target, max_buffer_size); server.link(transfers[sha].target, conn, max_buffer_size); end - conn:write(string.char(5, 0, 0, 3, sha:len()) .. sha .. string.char(0, 0)); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte) + conn:write(string.char(5, 0, 0, 3, #sha) .. sha .. string.char(0, 0)); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte) conn:lock_read(true) else module:log("warn", "Neither data transfer nor initial connect of a participator of a transfer.") @@ -120,7 +117,11 @@ function connlistener.ondisconnect(conn, err) end end -local function get_disco_info(stanza) +module:add_identity("proxy", "bytestreams", name); +module:add_feature("http://jabber.org/protocol/bytestreams"); + +module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function(event) + local origin, stanza = event.origin, event.stanza; local reply = replies_cache.disco_info; if reply == nil then reply = st.iq({type='result', from=host}):query("http://jabber.org/protocol/disco#info") @@ -131,10 +132,12 @@ local function get_disco_info(stanza) reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; - return reply; -end + origin.send(reply); + return true; +end, -1); -local function get_disco_items(stanza) +module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function(event) + local origin, stanza = event.origin, event.stanza; local reply = replies_cache.disco_items; if reply == nil then reply = st.iq({type='result', from=host}):query("http://jabber.org/protocol/disco#items"); @@ -143,32 +146,21 @@ local function get_disco_items(stanza) reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; - return reply; -end + origin.send(reply); + return true; +end, -1); -local function get_stream_host(origin, stanza) +module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event) + local origin, stanza = event.origin, event.stanza; local reply = replies_cache.stream_host; local err_reply = replies_cache.stream_host_err; local sid = stanza.tags[1].attr.sid; local allow = false; - local jid_node, jid_host, jid_resource = jid_split(stanza.attr.from); - - if stanza.attr.from == nil then - jid_node = origin.username; - jid_host = origin.host; - jid_resource = origin.resource; - end + local jid = stanza.attr.from; if proxy_acl and #proxy_acl > 0 then - if host ~= nil then -- at least a domain is needed. - for _, acl in ipairs(proxy_acl) do - local acl_node, acl_host, acl_resource = jid_split(acl); - if ((acl_node ~= nil and acl_node == jid_node) or acl_node == nil) and - ((acl_host ~= nil and acl_host == jid_host) or acl_host == nil) and - ((acl_resource ~= nil and acl_resource == jid_resource) or acl_resource == nil) then - allow = true; - end - end + for _, acl in ipairs(proxy_acl) do + if jid_compare(jid, acl) then allow = true; end end else allow = true; @@ -181,7 +173,7 @@ local function get_stream_host(origin, stanza) replies_cache.stream_host = reply; end else - module:log("warn", "Denying use of proxy for %s", tostring(jid_join(jid_node, jid_host, jid_resource))); + module:log("warn", "Denying use of proxy for %s", tostring(jid)); if err_reply == nil then err_reply = st.iq({type="error", from=host}) :query("http://jabber.org/protocol/bytestreams") @@ -194,24 +186,21 @@ local function get_stream_host(origin, stanza) reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; reply.tags[1].attr.sid = sid; - return reply; -end + origin.send(reply); + return true; +end); module.unload = function() - componentmanager.deregister_component(host); connlisteners.deregister(module.host .. ':proxy65'); end local function set_activation(stanza) - local from, to, sid, reply = nil; - from = stanza.attr.from; - if stanza.tags[1] ~= nil and tostring(stanza.tags[1].name) == "query" then - if stanza.tags[1].attr ~= nil then - sid = stanza.tags[1].attr.sid; - end - if stanza.tags[1].tags[1] ~= nil and tostring(stanza.tags[1].tags[1].name) == "activate" then - to = stanza.tags[1].tags[1][1]; - end + local to, reply; + local from = stanza.attr.from; + local query = stanza.tags[1]; + local sid = query.attr.sid; + if query.tags[1] and query.tags[1].name == "activate" then + to = query.tags[1][1]; end if from ~= nil and to ~= nil and sid ~= nil then reply = st.iq({type="result", from=host, to=from}); @@ -220,55 +209,35 @@ local function set_activation(stanza) return reply, from, to, sid; end -function handle_to_domain(origin, stanza) - local to_node, to_host, to_resource = jid_split(stanza.attr.to); - if to_node == nil then - 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)); - return true; - elseif xmlns == "http://jabber.org/protocol/disco#items" then - origin.send(get_disco_items(stanza)); - return true; - elseif xmlns == "http://jabber.org/protocol/bytestreams" then - origin.send(get_stream_host(origin, stanza)); - return true; - else - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); - return true; - end - elseif stanza.name == "iq" and type == "set" then - module:log("debug", "Received activation request from %s", stanza.attr.from); - local reply, from, to, sid = set_activation(stanza); - if reply ~= nil and from ~= nil and to ~= nil and sid ~= nil then - local sha = sha1(sid .. from .. to, true); - if transfers[sha] == nil then - module:log("error", "transfers[sha]: nil"); - elseif(transfers[sha] ~= nil and transfers[sha].initiator ~= nil and transfers[sha].target ~= nil) then - origin.send(reply); - transfers[sha].activated = true; - transfers[sha].target:lock_read(false); - transfers[sha].initiator:lock_read(false); - else - module:log("debug", "Both parties were not yet connected"); - local message = "Neither party is connected to the proxy"; - if transfers[sha].initiator then - message = "The recipient is not connected to the proxy"; - elseif transfers[sha].target then - message = "The sender (you) is not connected to the proxy"; - end - origin.send(st.error_reply(stanza, "cancel", "not-allowed", message)); - end - else - module:log("error", "activation failed: sid: %s, initiator: %s, target: %s", tostring(sid), tostring(from), tostring(to)); +module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event) + local origin, stanza = event.origin, event.stanza; + + module:log("debug", "Received activation request from %s", stanza.attr.from); + local reply, from, to, sid = set_activation(stanza); + if reply ~= nil and from ~= nil and to ~= nil and sid ~= nil then + local sha = sha1(sid .. from .. to, true); + if transfers[sha] == nil then + module:log("error", "transfers[sha]: nil"); + elseif(transfers[sha] ~= nil and transfers[sha].initiator ~= nil and transfers[sha].target ~= nil) then + origin.send(reply); + transfers[sha].activated = true; + transfers[sha].target:lock_read(false); + transfers[sha].initiator:lock_read(false); + else + module:log("debug", "Both parties were not yet connected"); + local message = "Neither party is connected to the proxy"; + if transfers[sha].initiator then + message = "The recipient is not connected to the proxy"; + elseif transfers[sha].target then + message = "The sender (you) is not connected to the proxy"; end + origin.send(st.error_reply(stanza, "cancel", "not-allowed", message)); end + return true; + else + module:log("error", "activation failed: sid: %s, initiator: %s, target: %s", tostring(sid), tostring(from), tostring(to)); end - return; -end +end); if not connlisteners.register(module.host .. ':proxy65', connlistener) then module:log("error", "mod_proxy65: Could not establish a connection listener. Check your configuration please."); @@ -276,4 +245,3 @@ if not connlisteners.register(module.host .. ':proxy65', connlistener) then end connlisteners.start(module.host .. ':proxy65'); -component = componentmanager.register_component(host, handle_to_domain); diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua index 2818e336..25c09dfa 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -13,82 +13,94 @@ local datamanager = require "util.datamanager"; local usermanager_user_exists = require "core.usermanager".user_exists; local usermanager_create_user = require "core.usermanager".create_user; local usermanager_set_password = require "core.usermanager".set_password; -local datamanager_store = require "util.datamanager".store; +local usermanager_delete_user = require "core.usermanager".delete_user; local os_time = os.time; local nodeprep = require "util.encodings".stringprep.nodeprep; +local jid_bare = require "util.jid".bare; + +local compat = module:get_option_boolean("registration_compat", true); module:add_feature("jabber:iq:register"); -module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza) - if stanza.tags[1].name == "query" then - local query = stanza.tags[1]; - if stanza.attr.type == "get" then - local reply = st.reply(stanza); - reply:tag("query", {xmlns = "jabber:iq:register"}) - :tag("registered"):up() - :tag("username"):text(session.username):up() - :tag("password"):up(); - session.send(reply); - elseif stanza.attr.type == "set" then - if query.tags[1] and query.tags[1].name == "remove" then - -- TODO delete user auth data, send iq response, kick all user resources with a <not-authorized/>, delete all user data - 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 - -- 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)); - local roster = session.roster; - for _, session in pairs(hosts[host].sessions[username].sessions) do -- disconnect all resources - session:close({condition = "not-authorized", text = "Account deleted"}); - end - -- TODO datamanager should be able to delete all user data itself - datamanager.store(username, host, "vcard", nil); - datamanager.store(username, host, "private", nil); - datamanager.list_store(username, host, "offline", nil); - local bare = username.."@"..host; - for jid, item in pairs(roster) do - if jid and jid ~= "pending" then - if item.subscription == "both" or item.subscription == "from" or (roster.pending and roster.pending[jid]) then - core_post_stanza(hosts[host], st.presence({type="unsubscribed", from=bare, to=jid})); - end - if item.subscription == "both" or item.subscription == "to" or item.ask then - core_post_stanza(hosts[host], st.presence({type="unsubscribe", from=bare, to=jid})); - end +local function handle_registration_stanza(event) + local session, stanza = event.origin, event.stanza; + + local query = stanza.tags[1]; + if stanza.attr.type == "get" then + local reply = st.reply(stanza); + reply:tag("query", {xmlns = "jabber:iq:register"}) + :tag("registered"):up() + :tag("username"):text(session.username):up() + :tag("password"):up(); + session.send(reply); + else -- stanza.attr.type == "set" + if query.tags[1] and query.tags[1].name == "remove" then + -- TODO delete user auth data, send iq response, kick all user resources with a <not-authorized/>, delete all user data + local username, host = session.username, session.host; + + local ok, err = usermanager_delete_user(username, host); + + if not ok then + module:log("debug", "Removing user account %s@%s failed: %s", username, host, err); + session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); + return true; + end + + session.send(st.reply(stanza)); + local roster = session.roster; + for _, session in pairs(hosts[host].sessions[username].sessions) do -- disconnect all resources + session:close({condition = "not-authorized", text = "Account deleted"}); + end + -- TODO datamanager should be able to delete all user data itself + datamanager.store(username, host, "vcard", nil); + datamanager.store(username, host, "private", nil); + datamanager.list_store(username, host, "offline", nil); + local bare = username.."@"..host; + for jid, item in pairs(roster) do + if jid and jid ~= "pending" then + if item.subscription == "both" or item.subscription == "from" or (roster.pending and roster.pending[jid]) then + core_post_stanza(hosts[host], st.presence({type="unsubscribed", from=bare, to=jid})); + end + if item.subscription == "both" or item.subscription == "to" or item.ask then + core_post_stanza(hosts[host], st.presence({type="unsubscribe", from=bare, to=jid})); end end - datamanager.store(username, host, "roster", nil); - datamanager.store(username, host, "privacy", nil); - datamanager.store(username, host, "accounts", nil); -- delete accounts datastore at the end - module:log("info", "User removed their account: %s@%s", username, host); - module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session }); - else - local username = query:child_with_name("username"); - local password = query:child_with_name("password"); - if username and password then - -- FIXME shouldn't use table.concat - username = nodeprep(table.concat(username)); - password = table.concat(password); - if username == session.username then - if usermanager_set_password(username, session.host, password) 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 + datamanager.store(username, host, "roster", nil); + datamanager.store(username, host, "privacy", nil); + module:log("info", "User removed their account: %s@%s", username, host); + module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session }); + else + local username = nodeprep(query:get_child("username"):get_text()); + local password = query:get_child("password"):get_text(); + if username and password then + if username == session.username then + if usermanager_set_password(username, password, session.host) then + session.send(st.reply(stanza)); else - session.send(st.error_reply(stanza, "modify", "bad-request")); + -- 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, "modify", "bad-request")); end + else + session.send(st.error_reply(stanza, "modify", "bad-request")); end end - else - session.send(st.error_reply(stanza, "cancel", "service-unavailable")); - end; -end); + end + return true; +end + +module:hook("iq/self/jabber:iq:register:query", handle_registration_stanza); +if compat then + module:hook("iq/host/jabber:iq:register:query", function (event) + local session, stanza = event.origin, event.stanza; + if session.type == "c2s" and jid_bare(stanza.attr.to) == session.host then + return handle_registration_stanza(event); + end + end); +end local recent_ips = {}; local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations"); @@ -99,10 +111,12 @@ 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 module:get_option("allow_registration") == false then +module:hook("stanza/iq/jabber:iq:register:query", function(event) + local session, stanza = event.origin, event.stanza; + + if module:get_option("allow_registration") == false or session.type ~= "c2s_unauthed" then session.send(st.error_reply(stanza, "cancel", "service-unavailable")); - elseif stanza.tags[1].name == "query" then + else local query = stanza.tags[1]; if stanza.attr.type == "get" then local reply = st.reply(stanza); @@ -123,7 +137,7 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s module:log("debug", "User's IP not known; can't apply blacklist/whitelist"); elseif blacklisted_ips[session.ip] or (whitelist_only and not whitelisted_ips[session.ip]) then session.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not allowed to register an account.")); - return; + return true; 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 }; @@ -134,7 +148,7 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s if os_time() - ip.time < min_seconds_between_registrations then ip.time = os_time(); session.send(st.error_reply(stanza, "wait", "not-acceptable")); - return; + return true; end ip.time = os_time(); end @@ -151,7 +165,7 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s if usermanager_create_user(username, password, host) then session.send(st.reply(stanza)); -- user created! module:log("info", "User account created: %s@%s", username, host); - module:fire_event("user-registered", { + module:fire_event("user-registered", { username = username, host = host, source = "mod_register", session = session }); else @@ -164,8 +178,6 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s end end end - else - session.send(st.error_reply(stanza, "cancel", "service-unavailable")); - end; + end + return true; end); - diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua index ddf02f2f..fe2eea71 100644 --- a/plugins/mod_roster.lua +++ b/plugins/mod_roster.lua @@ -7,13 +7,13 @@ -- - local st = require "util.stanza" local jid_split = require "util.jid".split; local jid_prep = require "util.jid".prep; local t_concat = table.concat; -local tostring = tostring; +local tonumber = tonumber; +local pairs, ipairs = pairs, ipairs; local rm_remove_from_roster = require "core.rostermanager".remove_from_roster; local rm_add_to_roster = require "core.rostermanager".add_to_roster; @@ -30,112 +30,110 @@ module:hook("stream-features", function(event) end end); -module:add_iq_handler("c2s", "jabber:iq:roster", - function (session, stanza) - if stanza.tags[1].name == "query" then - if stanza.attr.type == "get" then - local roster = st.reply(stanza); - - local client_ver = tonumber(stanza.tags[1].attr.ver); - local server_ver = tonumber(session.roster[false].version or 1); - - if not (client_ver and server_ver) or client_ver ~= server_ver then - roster:query("jabber:iq:roster"); - -- Client does not support versioning, or has stale roster - for jid in pairs(session.roster) do - if jid ~= "pending" and jid then - roster:tag("item", { - jid = jid, - subscription = session.roster[jid].subscription, - ask = session.roster[jid].ask, - name = session.roster[jid].name, - }); - for group in pairs(session.roster[jid].groups) do - roster:tag("group"):text(group):up(); - end - roster:up(); -- move out from item - end - end - roster.tags[1].attr.ver = server_ver; +module:hook("iq/self/jabber:iq:roster:query", function(event) + local session, stanza = event.origin, event.stanza; + + if stanza.attr.type == "get" then + local roster = st.reply(stanza); + + local client_ver = tonumber(stanza.tags[1].attr.ver); + local server_ver = tonumber(session.roster[false].version or 1); + + if not (client_ver and server_ver) or client_ver ~= server_ver then + roster:query("jabber:iq:roster"); + -- Client does not support versioning, or has stale roster + for jid, item in pairs(session.roster) do + if jid ~= "pending" and jid then + roster:tag("item", { + jid = jid, + subscription = item.subscription, + ask = item.ask, + name = item.name, + }); + for group in pairs(item.groups) do + roster:tag("group"):text(group):up(); end - session.send(roster); - session.interested = true; -- resource is interested in roster updates - return true; - elseif stanza.attr.type == "set" then - local query = stanza.tags[1]; - if #query.tags == 1 and query.tags[1].name == "item" - and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid - -- Protection against overwriting roster.pending, until we move it - and query.tags[1].attr.jid ~= "pending" then - local item = query.tags[1]; - local from_node, from_host = jid_split(stanza.attr.from); - local from_bare = from_node and (from_node.."@"..from_host) or from_host; -- bare JID - local jid = jid_prep(item.attr.jid); - local node, host, resource = jid_split(jid); - if not resource and host then - if jid ~= from_node.."@"..from_host then - if item.attr.subscription == "remove" then - local roster = session.roster; - local r_item = roster[jid]; - if r_item then - local to_bare = node and (node.."@"..host) or host; -- bare JID - if r_item.subscription == "both" or r_item.subscription == "from" or (roster.pending and roster.pending[jid]) then - core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=to_bare})); - end - if r_item.subscription == "both" or r_item.subscription == "to" or r_item.ask then - core_post_stanza(session, st.presence({type="unsubscribe", from=session.full_jid, to=to_bare})); - end - local success, err_type, err_cond, err_msg = rm_remove_from_roster(session, jid); - if success then - session.send(st.reply(stanza)); - rm_roster_push(from_node, from_host, jid); - else - session.send(st.error_reply(stanza, err_type, err_cond, err_msg)); - end - else - session.send(st.error_reply(stanza, "modify", "item-not-found")); - end - else - local r_item = {name = item.attr.name, groups = {}}; - if r_item.name == "" then r_item.name = nil; end - if session.roster[jid] then - r_item.subscription = session.roster[jid].subscription; - r_item.ask = session.roster[jid].ask; - else - r_item.subscription = "none"; - end - for _, child in ipairs(item) do - if child.name == "group" then - local text = t_concat(child); - if text and text ~= "" then - r_item.groups[text] = true; - end - end - end - local success, err_type, err_cond, err_msg = rm_add_to_roster(session, jid, r_item); - if success then - -- Ok, send success - session.send(st.reply(stanza)); - -- and push change to all resources - rm_roster_push(from_node, from_host, jid); - else - -- Adding to roster failed - session.send(st.error_reply(stanza, err_type, err_cond, err_msg)); - end - end + roster:up(); -- move out from item + end + end + roster.tags[1].attr.ver = server_ver; + end + session.send(roster); + session.interested = true; -- resource is interested in roster updates + else -- stanza.attr.type == "set" + local query = stanza.tags[1]; + if #query.tags == 1 and query.tags[1].name == "item" + and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid + -- Protection against overwriting roster.pending, until we move it + and query.tags[1].attr.jid ~= "pending" then + local item = query.tags[1]; + local from_node, from_host = jid_split(stanza.attr.from); + local from_bare = from_node and (from_node.."@"..from_host) or from_host; -- bare JID + local jid = jid_prep(item.attr.jid); + local node, host, resource = jid_split(jid); + if not resource and host then + if jid ~= from_node.."@"..from_host then + if item.attr.subscription == "remove" then + local roster = session.roster; + local r_item = roster[jid]; + if r_item then + local to_bare = node and (node.."@"..host) or host; -- bare JID + if r_item.subscription == "both" or r_item.subscription == "from" or (roster.pending and roster.pending[jid]) then + core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=to_bare})); + end + if r_item.subscription == "both" or r_item.subscription == "to" or r_item.ask then + core_post_stanza(session, st.presence({type="unsubscribe", from=session.full_jid, to=to_bare})); + end + local success, err_type, err_cond, err_msg = rm_remove_from_roster(session, jid); + if success then + session.send(st.reply(stanza)); + rm_roster_push(from_node, from_host, jid); else - -- Trying to add self to roster - session.send(st.error_reply(stanza, "cancel", "not-allowed")); + session.send(st.error_reply(stanza, err_type, err_cond, err_msg)); end else - -- Invalid JID added to roster - session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error? + session.send(st.error_reply(stanza, "modify", "item-not-found")); end else - -- Roster set didn't include a single item, or its name wasn't 'item' - session.send(st.error_reply(stanza, "modify", "bad-request")); + local r_item = {name = item.attr.name, groups = {}}; + if r_item.name == "" then r_item.name = nil; end + if session.roster[jid] then + r_item.subscription = session.roster[jid].subscription; + r_item.ask = session.roster[jid].ask; + else + r_item.subscription = "none"; + end + for _, child in ipairs(item) do + if child.name == "group" then + local text = t_concat(child); + if text and text ~= "" then + r_item.groups[text] = true; + end + end + end + local success, err_type, err_cond, err_msg = rm_add_to_roster(session, jid, r_item); + if success then + -- Ok, send success + session.send(st.reply(stanza)); + -- and push change to all resources + rm_roster_push(from_node, from_host, jid); + else + -- Adding to roster failed + session.send(st.error_reply(stanza, err_type, err_cond, err_msg)); + end end - return true; + else + -- Trying to add self to roster + session.send(st.error_reply(stanza, "cancel", "not-allowed")); end + else + -- Invalid JID added to roster + session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error? end - end); + else + -- Roster set didn't include a single item, or its name wasn't 'item' + session.send(st.error_reply(stanza, "modify", "bad-request")); + end + end + return true; +end); diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index d407e5da..4906d01f 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -14,25 +14,11 @@ 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; -local usermanager_user_exists = require "core.usermanager".user_exists; -local usermanager_get_password = require "core.usermanager".get_password; -local t_concat, t_insert = table.concat, table.insert; +local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; local tostring = tostring; -local jid_split = require "util.jid".split; -local md5 = require "util.hashes".md5; -local config = require "core.configmanager"; local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption"); -local sasl_backend = module:get_option("sasl_backend") or "builtin"; - --- Cyrus config options -local require_provisioning = module:get_option("cyrus_require_provisioning") or false; -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"); +local allow_unencrypted_plain_auth = module:get_option("allow_unencrypted_plain_auth") local log = module._log; @@ -40,53 +26,6 @@ local xmlns_sasl ='urn:ietf:params:xml:ns:xmpp-sasl'; local xmlns_bind ='urn:ietf:params:xml:ns:xmpp-bind'; local xmlns_stanzas ='urn:ietf:params:xml:ns:xmpp-stanzas'; -local new_sasl; -if sasl_backend == "builtin" then - new_sasl = require "util.sasl".new; -elseif sasl_backend == "cyrus" then - prosody.unlock_globals(); --FIXME: Figure out why this is needed and - -- why cyrussasl isn't caught by the sandbox - local ok, cyrus = pcall(require, "util.sasl_cyrus"); - prosody.lock_globals(); - if ok then - local cyrus_new = cyrus.new; - new_sasl = function(realm) - return cyrus_new( - cyrus_service_realm or realm, - cyrus_service_name or "xmpp", - cyrus_application_name or "prosody" - ); - end - else - module:log("error", "Failed to load Cyrus SASL because: %s", cyrus); - error("Failed to load Cyrus SASL"); - end -else - module:log("error", "Unknown SASL backend: %s", sasl_backend); - 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 - end -}; - local function build_reply(status, ret, err_msg) local reply = st.stanza(status, {xmlns = xmlns_sasl}); if status == "challenge" then @@ -110,45 +49,20 @@ local function handle_status(session, status, ret, err_msg) elseif status == "success" then local username = nodeprep(session.sasl_handler.username); - if not(require_provisioning) or usermanager_user_exists(username, session.host) then - local aret, err = sm_make_authenticated(session, session.sasl_handler.username); - if aret then - session.sasl_handler = nil; - session:reset_stream(); - else - module:log("warn", "SASL succeeded but username was invalid"); - session.sasl_handler = session.sasl_handler:clean_clone(); - return "failure", "not-authorized", "User authenticated successfully, but username was invalid"; - end + local ok, err = sm_make_authenticated(session, session.sasl_handler.username); + if ok then + session.sasl_handler = nil; + session:reset_stream(); else - module:log("warn", "SASL succeeded but we don't have an account provisioned for %s", username); + module:log("warn", "SASL succeeded but username was invalid"); session.sasl_handler = session.sasl_handler:clean_clone(); - return "failure", "not-authorized", "User authenticated successfully, but not provisioned for XMPP"; + return "failure", "not-authorized", "User authenticated successfully, but username was invalid"; end end return status, ret, err_msg; end -local function sasl_handler(session, stanza) - if stanza.name == "auth" then - -- FIXME ignoring duplicates because ejabberd does - if config.get(session.host or "*", "core", "anonymous_login") then - if stanza.attr.mechanism ~= "ANONYMOUS" then - return session.send(build_reply("failure", "invalid-mechanism")); - end - elseif stanza.attr.mechanism == "ANONYMOUS" then - return session.send(build_reply("failure", "mechanism-too-weak")); - end - local valid_mechanism = session.sasl_handler:select(stanza.attr.mechanism); - if not valid_mechanism then - return session.send(build_reply("failure", "invalid-mechanism")); - end - if secure_auth_only and not session.secure then - return session.send(build_reply("failure", "encryption-required")); - end - elseif not session.sasl_handler then - return; -- FIXME ignoring out of order stanzas because ejabberd does - end +local function sasl_process_cdata(session, stanza) local text = stanza[1]; if text then text = base64.decode(text); @@ -156,7 +70,7 @@ local function sasl_handler(session, stanza) if not text then session.sasl_handler = nil; session.send(build_reply("failure", "incorrect-encoding")); - return; + return true; end end local status, ret, err_msg = session.sasl_handler:process(text); @@ -164,11 +78,45 @@ local function sasl_handler(session, stanza) local s = build_reply(status, ret, err_msg); log("debug", "sasl reply: %s", tostring(s)); session.send(s); + return true; end -module:add_handler("c2s_unauthed", "auth", xmlns_sasl, sasl_handler); -module:add_handler("c2s_unauthed", "abort", xmlns_sasl, sasl_handler); -module:add_handler("c2s_unauthed", "response", xmlns_sasl, sasl_handler); +module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event) + local session, stanza = event.origin, event.stanza; + if session.type ~= "c2s_unauthed" then return; end + + if session.sasl_handler and session.sasl_handler.selected then + session.sasl_handler = nil; -- allow starting a new SASL negotiation before completing an old one + end + if not session.sasl_handler then + session.sasl_handler = usermanager_get_sasl_handler(module.host); + end + local mechanism = stanza.attr.mechanism; + if not session.secure and (secure_auth_only or (mechanism == "PLAIN" and not allow_unencrypted_plain_auth)) then + session.send(build_reply("failure", "encryption-required")); + return true; + end + local valid_mechanism = session.sasl_handler:select(mechanism); + if not valid_mechanism then + session.send(build_reply("failure", "invalid-mechanism")); + return true; + end + return sasl_process_cdata(session, stanza); +end); +module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:response", function(event) + local session = event.origin; + if not(session.sasl_handler and session.sasl_handler.selected) then + session.send(build_reply("failure", "not-authorized", "Out of order SASL element")); + return true; + end + return sasl_process_cdata(session, event.stanza); +end); +module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event) + local session = event.origin; + session.sasl_handler = nil; + session.send(build_reply("failure", "aborted")); + return true; +end); local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' }; local bind_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-bind' }; @@ -179,18 +127,12 @@ module:hook("stream-features", function(event) if secure_auth_only and not origin.secure then return; end - local realm = module:get_option("sasl_realm") or origin.host; - 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); - if not (module:get_option("allow_unencrypted_plain_auth")) and not origin.secure then - origin.sasl_handler:forbidden({"PLAIN"}); - end - end + origin.sasl_handler = usermanager_get_sasl_handler(module.host); features:tag("mechanisms", mechanisms_attr); - for k, v in pairs(origin.sasl_handler:mechanisms()) do - features:tag("mechanism"):text(v):up(); + for mechanism in pairs(origin.sasl_handler:mechanisms()) do + if mechanism ~= "PLAIN" or origin.secure or allow_unencrypted_plain_auth then + features:tag("mechanism"):text(mechanism):up(); + end end features:up(); else @@ -199,29 +141,31 @@ module:hook("stream-features", function(event) end end); -module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind", function(session, stanza) - log("debug", "Client requesting a resource bind"); +module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event) + local origin, stanza = event.origin, event.stanza; local resource; if stanza.attr.type == "set" then local bind = stanza.tags[1]; - if bind and bind.attr.xmlns == xmlns_bind then - resource = bind:child_with_name("resource"); - if resource then - resource = resource[1]; - end - end + resource = bind:child_with_name("resource"); + resource = resource and #resource.tags == 0 and resource[1] or nil; end - local success, err_type, err, err_msg = sm_bind_resource(session, resource); - if not success then - session.send(st.error_reply(stanza, err_type, err, err_msg)); + local success, err_type, err, err_msg = sm_bind_resource(origin, resource); + if success then + origin.send(st.reply(stanza) + :tag("bind", { xmlns = xmlns_bind }) + :tag("jid"):text(origin.full_jid)); + origin.log("debug", "Resource bound: %s", origin.full_jid); else - session.send(st.reply(stanza) - :tag("bind", { xmlns = xmlns_bind}) - :tag("jid"):text(session.full_jid)); + origin.send(st.error_reply(stanza, err_type, err, err_msg)); + origin.log("debug", "Resource bind failed: %s", err_msg or err); end + return true; end); -module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-session", function(session, stanza) - log("debug", "Client requesting a session"); - session.send(st.reply(stanza)); -end); +local function handle_legacy_session(event) + event.origin.send(st.reply(event.stanza)); + return true; +end + +module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session); +module:hook("iq/host/urn:ietf:params:xml:ns:xmpp-session:session", handle_legacy_session); diff --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua new file mode 100644 index 00000000..821d1e1a --- /dev/null +++ b/plugins/mod_storage_internal.lua @@ -0,0 +1,19 @@ +local datamanager = require "core.storagemanager".olddm; + +local host = module.host; + +local driver = { name = "internal" }; +local driver_mt = { __index = driver }; + +function driver:open(store) + return setmetatable({ store = store }, driver_mt); +end +function driver:get(user) + return datamanager.load(user, host, self.store); +end + +function driver:set(user, data) + return datamanager.store(user, host, self.store, data); +end + +module:add_item("data-driver", driver); diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua new file mode 100644 index 00000000..dd148704 --- /dev/null +++ b/plugins/mod_storage_sql.lua @@ -0,0 +1,340 @@ + +--[[ + +DB Tables: + Prosody - key-value, map + | host | user | store | key | type | value | + ProsodyArchive - list + | host | user | store | key | time | stanzatype | jsonvalue | + +Mapping: + Roster - Prosody + | host | user | "roster" | "contactjid" | type | value | + | host | user | "roster" | NULL | "json" | roster[false] data | + Account - Prosody + | host | user | "accounts" | "username" | type | value | + + Offline - ProsodyArchive + | host | user | "offline" | "contactjid" | time | "message" | json|XML | + +]] + +local type = type; +local tostring = tostring; +local tonumber = tonumber; +local pairs = pairs; +local next = next; +local setmetatable = setmetatable; +local xpcall = xpcall; +local json = require "util.json"; + +local DBI; +local connection; +local host,user,store = module.host; +local params = module:get_option("sql"); + +local resolve_relative_path = require "core.configmanager".resolve_relative_path; + +local function test_connection() + if not connection then return nil; end + if connection:ping() then + return true; + else + module:log("debug", "Database connection closed"); + connection = nil; + end +end +local function connect() + if not test_connection() then + prosody.unlock_globals(); + local dbh, err = DBI.Connect( + params.driver, params.database, + params.username, params.password, + params.host, params.port + ); + prosody.lock_globals(); + if not dbh then + module:log("debug", "Database connection failed: %s", tostring(err)); + return nil, err; + end + module:log("debug", "Successfully connected to database"); + dbh:autocommit(false); -- don't commit automatically + connection = dbh; + return connection; + end +end + +local function create_table() + local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);"; + if params.driver == "PostgreSQL" then + create_sql = create_sql:gsub("`", "\""); + elseif params.driver == "MySQL" then + create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT"); + end + + local stmt = connection:prepare(create_sql); + if stmt then + local ok = stmt:execute(); + local commit_ok = connection:commit(); + if ok and commit_ok then + module:log("info", "Initialized new %s database with prosody table", params.driver); + local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)"; + if params.driver == "PostgreSQL" then + index_sql = index_sql:gsub("`", "\""); + elseif params.driver == "MySQL" then + index_sql = index_sql:gsub("`([,)])", "`(20)%1"); + end + local stmt, err = connection:prepare(index_sql); + local ok, commit_ok, commit_err; + if stmt then + ok, err = stmt:execute(); + commit_ok, commit_err = connection:commit(); + end + if not(ok and commit_ok) then + module:log("warn", "Failed to create index (%s), lookups may not be optimised", err or commit_err); + end + else -- COMPAT: Upgrade tables from 0.8.0 + -- Failed to create, but check existing MySQL table here + local stmt = connection:prepare("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'"); + local ok = stmt:execute(); + local commit_ok = connection:commit(); + if ok and commit_ok then + if stmt:rowcount() > 0 then + local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT"); + local ok = stmt:execute(); + local commit_ok = connection:commit(); + if ok and commit_ok then + module:log("info", "Database table automatically upgraded"); + end + end + repeat until not stmt:fetch(); + end + end + end +end + +do -- process options to get a db connection + local ok; + prosody.unlock_globals(); + ok, DBI = pcall(require, "DBI"); + if not ok then + package.loaded["DBI"] = {}; + module:log("error", "Failed to load the LuaDBI library for accessing SQL databases: %s", DBI); + module:log("error", "More information on installing LuaDBI can be found at http://prosody.im/doc/depends#luadbi"); + end + prosody.lock_globals(); + if not ok or not DBI.Connect then + return; -- Halt loading of this module + end + + params = params or { driver = "SQLite3" }; + + if params.driver == "SQLite3" then + params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite"); + end + + assert(params.driver and params.database, "Both the SQL driver and the database need to be specified"); + + assert(connect()); + + -- Automatically create table, ignore failure (table probably already exists) + create_table(); +end + +local function serialize(value) + local t = type(value); + if t == "string" or t == "boolean" or t == "number" then + return t, tostring(value); + elseif t == "table" then + local value,err = json.encode(value); + if value then return "json", value; end + return nil, err; + end + return nil, "Unhandled value type: "..t; +end +local function deserialize(t, value) + if t == "string" then return value; + elseif t == "boolean" then + if value == "true" then return true; + elseif value == "false" then return false; end + elseif t == "number" then return tonumber(value); + elseif t == "json" then + return json.decode(value); + end +end + +local function getsql(sql, ...) + if params.driver == "PostgreSQL" then + sql = sql:gsub("`", "\""); + end + -- do prepared statement stuff + local stmt, err = connection:prepare(sql); + if not stmt and not test_connection() then error("connection failed"); end + if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end + -- run query + local ok, err = stmt:execute(host or "", user or "", store or "", ...); + if not ok and not test_connection() then error("connection failed"); end + if not ok then return nil, err; end + + return stmt; +end +local function setsql(sql, ...) + local stmt, err = getsql(sql, ...); + if not stmt then return stmt, err; end + return stmt:affected(); +end +local function transact(...) + -- ... +end +local function rollback(...) + if connection then connection:rollback(); end -- FIXME check for rollback error? + return ...; +end +local function commit(...) + if not connection:commit() then return nil, "SQL commit failed"; end + return ...; +end + +local function keyval_store_get() + local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?"); + if not stmt then return rollback(nil, err); end + + local haveany; + local result = {}; + for row in stmt:rows(true) do + haveany = true; + local k = row.key; + local v = deserialize(row.type, row.value); + if k and v then + if k ~= "" then result[k] = v; elseif type(v) == "table" then + for a,b in pairs(v) do + result[a] = b; + end + end + end + end + return commit(haveany and result or nil); +end +local function keyval_store_set(data) + local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?"); + if not affected then return rollback(affected, err); end + + if data and next(data) ~= nil then + local extradata = {}; + for key, value in pairs(data) do + if type(key) == "string" and key ~= "" then + local t, value = serialize(value); + if not t then return rollback(t, value); end + local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value); + if not ok then return rollback(ok, err); end + else + extradata[key] = value; + end + end + if next(extradata) ~= nil then + local t, extradata = serialize(extradata); + if not t then return rollback(t, extradata); end + local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", "", t, extradata); + if not ok then return rollback(ok, err); end + end + end + return commit(true); +end + +local keyval_store = {}; +keyval_store.__index = keyval_store; +function keyval_store:get(username) + user,store = username,self.store; + if not connection and not connect() then return nil, "Unable to connect to database"; end + local success, ret, err = xpcall(keyval_store_get, debug.traceback); + if not connection and connect() then + success, ret, err = xpcall(keyval_store_get, debug.traceback); + end + if success then return ret, err; else return rollback(nil, ret); end +end +function keyval_store:set(username, data) + user,store = username,self.store; + if not connection and not connect() then return nil, "Unable to connect to database"; end + local success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback); + if not connection and connect() then + success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback); + end + if success then return ret, err; else return rollback(nil, ret); end +end + +local function map_store_get(key) + local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); + if not stmt then return rollback(nil, err); end + + local haveany; + local result = {}; + for row in stmt:rows(true) do + haveany = true; + local k = row.key; + local v = deserialize(row.type, row.value); + if k and v then + if k ~= "" then result[k] = v; elseif type(v) == "table" then + for a,b in pairs(v) do + result[a] = b; + end + end + end + end + return commit(haveany and result[key] or nil); +end +local function map_store_set(key, data) + local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); + if not affected then return rollback(affected, err); end + + if data and next(data) ~= nil then + if type(key) == "string" and key ~= "" then + local t, value = serialize(data); + if not t then return rollback(t, value); end + local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value); + if not ok then return rollback(ok, err); end + else + -- TODO non-string keys + end + end + return commit(true); +end + +local map_store = {}; +map_store.__index = map_store; +function map_store:get(username, key) + user,store = username,self.store; + local success, ret, err = xpcall(function() return map_store_get(key); end, debug.traceback); + if success then return ret, err; else return rollback(nil, ret); end +end +function map_store:set(username, key, data) + user,store = username,self.store; + local success, ret, err = xpcall(function() return map_store_set(key, data); end, debug.traceback); + if success then return ret, err; else return rollback(nil, ret); end +end + +local list_store = {}; +list_store.__index = list_store; +function list_store:scan(username, from, to, jid, typ) + user,store = username,self.store; + + local cols = {"from", "to", "jid", "typ"}; + local vals = { from , to , jid , typ }; + local stmt, err; + local query = "SELECT * FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=?"; + + query = query.." ORDER BY time"; + --local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); + + return nil, "not-implemented" +end + +local driver = { name = "sql" }; + +function driver:open(store, typ) + if not typ then -- default key-value store + return setmetatable({ store = store }, keyval_store); + end + return nil, "unsupported-store"; +end + +module:add_item("data-driver", driver); diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index 8b96aa15..cace2d69 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -6,6 +6,8 @@ -- COPYING file in the source package for more information. -- +local config = require "core.configmanager"; +local create_context = require "core.certmanager".create_context; local st = require "util.stanza"; local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption"); @@ -45,7 +47,7 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event) local host = origin.to_host or origin.host; local ssl_ctx = host and hosts[host].ssl_ctx_in or global_ssl_ctx; origin.conn:starttls(ssl_ctx); - origin.log("info", "TLS negotiation started for %s...", origin.type); + origin.log("debug", "TLS negotiation started for %s...", origin.type); origin.secure = false; else origin.log("warn", "Attempt to start TLS, but TLS is not available on this %s connection", origin.type); @@ -83,7 +85,22 @@ module:hook_stanza(xmlns_starttls, "proceed", function (session, stanza) module:log("debug", "Proceeding with TLS on s2sout..."); session:reset_stream(); local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx; - session.conn:starttls(ssl_ctx, true); + session.conn:starttls(ssl_ctx); session.secure = false; return true; end); + +function module.load() + local ssl_config = config.rawget(module.host, "core", "ssl"); + if not ssl_config then + local base_host = module.host:match("%.(.*)"); + ssl_config = config.get(base_host, "core", "ssl"); + end + host.ssl_ctx = create_context(host.host, "client", ssl_config); -- for outgoing connections + host.ssl_ctx_in = create_context(host.host, "server", ssl_config); -- for incoming connections +end + +function module.unload() + host.ssl_ctx = nil; + host.ssl_ctx_in = nil; +end diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua index 24d10180..52b33c74 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/mod_version.lua b/plugins/mod_version.lua index 69e914c0..52d8d290 100644 --- a/plugins/mod_version.lua +++ b/plugins/mod_version.lua @@ -10,28 +10,35 @@ local st = require "util.stanza"; module:add_feature("jabber:iq:version"); -local version = "the best operating system ever!"; +local version; + +local query = st.stanza("query", {xmlns = "jabber:iq:version"}) + :tag("name"):text("Prosody"):up() + :tag("version"):text(prosody.version):up(); if not module:get_option("hide_os_type") then if os.getenv("WINDIR") then version = "Windows"; else - local uname = io.popen("uname"); - if uname then - version = uname:read("*a"); - else - version = "an OS"; + local os_version_command = module:get_option("os_version_command"); + local ok pposix = pcall(require, "pposix"); + if not os_version_command and (ok and pposix and pposix.uname) then + version = pposix.uname().sysname; end + if not version then + local uname = io.popen(os_version_command or "uname"); + if uname then + version = uname:read("*a"); + end + uname:close(); + end + end + if version then + version = version:match("^%s*(.-)%s*$") or version; + query:tag("os"):text(version):up(); end end -version = version:match("^%s*(.-)%s*$") or 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 diff --git a/plugins/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua index f006818e..ac1e6302 100644 --- a/plugins/mod_watchregistrations.lua +++ b/plugins/mod_watchregistrations.lua @@ -9,7 +9,7 @@ local host = module:get_host(); -local registration_watchers = module:get_option("registration_watchers") +local registration_watchers = module:get_option("registration_watchers") or module:get_option("admins") or {}; local registration_alert = module:get_option("registration_notification") or "User $username just registered on $host from $ip"; @@ -21,7 +21,7 @@ module:hook("user-registered", module:log("debug", "Notifying of new registration"); local message = st.message{ type = "chat", from = host } :tag("body") - :text(registration_alert:gsub("%$(%w+)", + :text(registration_alert:gsub("%$(%w+)", function (v) return user[v] or user.session and user.session[v] or nil; end)); for _, jid in ipairs(registration_watchers) do diff --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua index 8f92010a..8f9cca2a 100644 --- a/plugins/mod_welcome.lua +++ b/plugins/mod_welcome.lua @@ -11,9 +11,9 @@ local welcome_text = module:get_option("welcome_message") or "Hello $username, w local st = require "util.stanza"; -module:hook("user-registered", +module:hook("user-registered", function (user) - local welcome_stanza = + local welcome_stanza = st.message({ to = user.username.."@"..user.host, from = host }) :tag("body"):text(welcome_text:gsub("$(%w+)", user)); core_route_stanza(hosts[host], welcome_stanza); diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index de23aebb..ca2e6e20 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -15,11 +15,14 @@ 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 - +if restrict_room_creation then + if restrict_room_creation == true then + restrict_room_creation = "admin"; + elseif restrict_room_creation ~= "admin" and restrict_room_creation ~= "local" then + restrict_room_creation = nil; + end +end 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"; @@ -27,12 +30,16 @@ local uuid_gen = require "util.uuid".generate; local datamanager = require "util.datamanager"; local um_is_admin = require "core.usermanager".is_admin; -local rooms = {}; +rooms = {}; +local rooms = rooms; local persistent_rooms = datamanager.load(nil, muc_host, "persistent") or {}; -local component; +local component = hosts[module.host]; + +-- Configurable options +local max_history_messages = module:get_option_number("max_history_messages"); local function is_admin(jid) - return um_is_admin(jid) or um_is_admin(jid, module.host); + return um_is_admin(jid, module.host); end local function room_route_stanza(room, stanza) core_post_stanza(component, stanza); end @@ -51,6 +58,9 @@ local function room_save(room, forced) room._data.history = history; elseif forced then datamanager.store(node, muc_host, "config", nil); + if not next(room._occupants) then -- Room empty + rooms[room.jid] = nil; + end end if forced then datamanager.store(nil, muc_host, "persistent", persistent_rooms); end end @@ -58,15 +68,20 @@ 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); + local room = muc_new_room(jid, { + history_length = max_history_messages; + }); room._data = data._data; + room._data.history_length = max_history_messages; --TODO: Need to allow per-room with a global limit 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); +local host_room = muc_new_room(muc_host, { + history_length = max_history_messages; +}); host_room.route_stanza = room_route_stanza; host_room.save = room_save; @@ -78,8 +93,8 @@ 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(); + if not room:is_hidden() then + reply:tag("item", {jid=jid, name=room:get_name()}):up(); end end return reply; -- TODO cache disco reply @@ -105,15 +120,20 @@ local function handle_to_domain(origin, stanza) end end -component = register_component(muc_host, function(origin, stanza) +function stanza_handler(event) + local origin, stanza = event.origin, event.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); + if not(restrict_room_creation) or + (restrict_room_creation == "admin" and is_admin(stanza.attr.from)) or + (restrict_room_creation == "local" and select(2, jid_split(stanza.attr.from)) == module.host:gsub("^[^%.]+%.", "")) then + room = muc_new_room(bare, { + history_length = max_history_messages; + }); room.route_stanza = room_route_stanza; room.save = room_save; rooms[bare] = room; @@ -128,12 +148,23 @@ component = register_component(muc_host, function(origin, stanza) origin.send(st.error_reply(stanza, "cancel", "not-allowed")); end else --[[not for us?]] end - return; + return true; end -- to the main muc domain handle_to_domain(origin, stanza); -end); -function component.send(stanza) -- FIXME do a generic fix + return true; +end +module:hook("iq/bare", stanza_handler, -1); +module:hook("message/bare", stanza_handler, -1); +module:hook("presence/bare", stanza_handler, -1); +module:hook("iq/full", stanza_handler, -1); +module:hook("message/full", stanza_handler, -1); +module:hook("presence/full", stanza_handler, -1); +module:hook("iq/host", stanza_handler, -1); +module:hook("message/host", stanza_handler, -1); +module:hook("presence/host", stanza_handler, -1); + +hosts[module.host].send = function(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 @@ -141,14 +172,10 @@ 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; diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index 18c80325..647bf915 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -6,9 +6,14 @@ -- COPYING file in the source package for more information. -- +local select = select; +local pairs, ipairs = pairs, ipairs; + local datamanager = require "util.datamanager"; local datetime = require "util.datetime"; +local dataform = require "util.dataforms"; + local jid_split = require "util.jid".split; local jid_bare = require "util.jid".bare; local jid_prep = require "util.jid".prep; @@ -21,7 +26,7 @@ local base64 = require "util.encodings".base64; local md5 = require "util.hashes".md5; local muc_domain = nil; --module:get_host(); -local history_length = 20; +local default_history_length = 20; ------------ local function filter_xmlns_from_array(array, filters) @@ -88,8 +93,12 @@ 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 + elseif affiliation == "member" then return "participant"; + elseif not affiliation then + if not self:is_members_only() then + return self:is_moderated() and "visitor" or "participant"; + end end end @@ -104,7 +113,7 @@ function room_mt:broadcast_presence(stanza, sid, code, nick) self:broadcast_except_nick(stanza, stanza.attr.from); local me = self._occupants[stanza.attr.from]; if me then - stanza:tag("status", {code='110'}); + stanza:tag("status", {code='110'}):up(); stanza.attr.to = sid; self:_route_stanza(stanza); end @@ -122,10 +131,14 @@ 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)); - while #history > history_length do t_remove(history, 1) end + local entry = { stanza = stanza, stamp = stamp }; + t_insert(history, entry); + while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end end end function room_mt:broadcast_except_nick(stanza, nick) @@ -151,24 +164,69 @@ 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 then since = datetime.parse(since); since = since and datetime.datetime(since); end + 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 if self._data['subject'] then - self:_route_stanza(st.message({type='groupchat', from=self.jid, to=to}):tag("subject"):text(self._data['subject'])); + self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject'])); end end function room_mt:get_disco_info(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"}); + :tag("identity", {category="conference", type="text", name=self:get_name()}):up() + :tag("feature", {var="http://jabber.org/protocol/muc"}):up() + :tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up() + :tag("feature", {var=self:is_moderated() and "muc_moderated" or "muc_unmoderated"}):up() + :tag("feature", {var=self:is_members_only() and "muc_membersonly" or "muc_open"}):up() + :tag("feature", {var=self:is_persistent() and "muc_persistent" or "muc_temporary"}):up() + :tag("feature", {var=self:is_hidden() and "muc_hidden" or "muc_public"}):up() + :tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up() + :add_child(dataform.new({ + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }, + { name = "muc#roominfo_description", label = "Description"} + }):form({["muc#roominfo_description"] = self:get_description()}, 'result')) + ; end function room_mt:get_disco_items(stanza) local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); @@ -181,6 +239,7 @@ function room_mt:set_subject(current_nick, subject) -- TODO check nick's authority if subject == "" then subject = nil; end self._data['subject'] = subject; + self._data['subject_from'] = current_nick; if self.save then self:save(); end local msg = st.message({type='groupchat', from=current_nick}) :tag('subject'):text(subject):up(); @@ -190,7 +249,7 @@ end local function build_unavailable_presence_from_error(stanza) local type, condition, text = stanza:get_error(); - local error_message = "Kicked: "..condition:gsub("%-", " "); + local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error"); if text then error_message = error_message..": "..text; end @@ -198,6 +257,87 @@ local function build_unavailable_presence_from_error(stanza) :tag('status'):text(error_message); end +function room_mt:set_name(name) + if name == "" or type(name) ~= "string" or name == (jid_split(self.jid)) then name = nil; end + if self._data.name ~= name then + self._data.name = name; + if self.save then self:save(true); end + end +end +function room_mt:get_name() + return self._data.name or jid_split(self.jid); +end +function room_mt:set_description(description) + if description == "" or type(description) ~= "string" then description = nil; end + if self._data.description ~= description then + self._data.description = description; + if self.save then self:save(true); end + end +end +function room_mt:get_description() + return self._data.description; +end +function room_mt:set_password(password) + if password == "" or type(password) ~= "string" then password = nil; end + if self._data.password ~= password then + self._data.password = password; + if self.save then self:save(true); end + end +end +function room_mt:get_password() + return self._data.password; +end +function room_mt:set_moderated(moderated) + moderated = moderated and true or nil; + if self._data.moderated ~= moderated then + self._data.moderated = moderated; + if self.save then self:save(true); end + end +end +function room_mt:is_moderated() + return self._data.moderated; +end +function room_mt:set_members_only(members_only) + members_only = members_only and true or nil; + if self._data.members_only ~= members_only then + self._data.members_only = members_only; + if self.save then self:save(true); end + end +end +function room_mt:is_members_only() + return self._data.members_only; +end +function room_mt:set_persistent(persistent) + persistent = persistent and true or nil; + if self._data.persistent ~= persistent then + self._data.persistent = persistent; + if self.save then self:save(true); end + end +end +function room_mt:is_persistent() + return self._data.persistent; +end +function room_mt:set_hidden(hidden) + hidden = hidden and true or nil; + if self._data.hidden ~= hidden then + self._data.hidden = hidden; + if self.save then self:save(true); end + end +end +function room_mt:is_hidden() + return self._data.hidden; +end +function room_mt:set_changesubject(changesubject) + changesubject = changesubject and true or nil; + if self._data.changesubject ~= changesubject then + self._data.changesubject = changesubject; + if self.save then self:save(true); end + end +end +function room_mt:get_changesubject() + return self._data.changesubject; +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); @@ -226,7 +366,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc 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'}); + :tag("status", {code='110'}):up(); self:_route_stanza(pr); if jid ~= new_jid then pr = st.clone(occupant.sessions[new_jid]) @@ -290,7 +430,15 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc end is_merge = true; end - if not new_nick then + local password = stanza:get_child("x", "http://jabber.org/protocol/muc"); + password = password and password:get_child("password", "http://jabber.org/protocol/muc"); + password = password and password[1] ~= "" and password[1]; + if self:get_password() and self:get_password() ~= password then + log("debug", "%s couldn't join due to invalid password: %s", from, to); + local reply = st.error_reply(stanza, "auth", "not-authorized"):up(); + reply.tags[1].attr.code = "401"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); + elseif 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"; @@ -311,20 +459,22 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc self._jid_nick[from] = to; self:send_occupant_list(from); pr.attr.from = to; + pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) + :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up(); 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'})); + self:broadcast_except_nick(pr, to); 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'})); + pr:tag("status", {code='110'}):up(); + if self._data.whois == 'anyone' then + pr:tag("status", {code='100'}):up(); end - self:send_history(from); + pr.attr.to = from; + self:_route_stanza(pr); + self:send_history(from, stanza); + elseif not affiliation then -- registration required for entering members-only room + local reply = st.error_reply(stanza, "auth", "registration-required"):up(); + reply.tags[1].attr.code = "407"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); else -- banned local reply = st.error_reply(stanza, "auth", "forbidden"):up(); reply.tags[1].attr.code = "403"; @@ -385,33 +535,84 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc end function room_mt:send_form(origin, stanza) - 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() - :tag("field", {type='list-single', label='Who May Discover Real JIDs?', var='muc#roomconfig_whois'}) - :tag("value"):text(self._data.whois or 'moderators'):up() - :tag("option", {label = 'Moderators Only'}) - :tag("value"):text('moderators'):up() - :up() - :tag("option", {label = 'Anyone'}) - :tag("value"):text('anyone'):up() - :up() - :up() + :add_child(self:get_form_layout():form()) ); end +function room_mt:get_form_layout() + local title = "Configuration for "..self.jid; + return dataform.new({ + title = title, + instructions = title, + { + name = 'FORM_TYPE', + type = 'hidden', + value = 'http://jabber.org/protocol/muc#roomconfig' + }, + { + name = 'muc#roomconfig_roomname', + type = 'text-single', + label = 'Name', + value = self:get_name() or "", + }, + { + name = 'muc#roomconfig_roomdesc', + type = 'text-single', + label = 'Description', + value = self:get_description() or "", + }, + { + name = 'muc#roomconfig_persistentroom', + type = 'boolean', + label = 'Make Room Persistent?', + value = self:is_persistent() + }, + { + name = 'muc#roomconfig_publicroom', + type = 'boolean', + label = 'Make Room Publicly Searchable?', + value = not self:is_hidden() + }, + { + name = 'muc#roomconfig_changesubject', + type = 'boolean', + label = 'Allow Occupants to Change Subject?', + value = self:get_changesubject() + }, + { + name = 'muc#roomconfig_whois', + type = 'list-single', + label = 'Who May Discover Real JIDs?', + value = { + { value = 'moderators', label = 'Moderators Only', default = self._data.whois == 'moderators' }, + { value = 'anyone', label = 'Anyone', default = self._data.whois == 'anyone' } + } + }, + { + name = 'muc#roomconfig_roomsecret', + type = 'text-private', + label = 'Password', + value = self:get_password() or "", + }, + { + name = 'muc#roomconfig_moderatedroom', + type = 'boolean', + label = 'Make Room Moderated?', + value = self:is_moderated() + }, + { + name = 'muc#roomconfig_membersonly', + type = 'boolean', + label = 'Make Room Members-Only?', + value = self:is_members_only() + } + }); +end + local valid_whois = { - moderators = true, - anyone = true, + moderators = true, + anyone = true, } function room_mt:process_form(origin, stanza) @@ -420,55 +621,77 @@ function room_mt:process_form(origin, stanza) 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 + if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); return; end + + local fields = self:get_form_layout():data(form); + if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); return; end local dirty = false + local name = fields['muc#roomconfig_roomname']; + if name ~= self:get_name() then + self:set_name(name); + end + + local description = fields['muc#roomconfig_roomdesc']; + if description ~= self:get_description() then + self:set_description(description); + 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 - dirty = dirty or (self._data.persistent ~= persistent) - self._data.persistent = persistent; + dirty = dirty or (self:is_persistent() ~= persistent) module:log("debug", "persistent=%s", tostring(persistent)); + local moderated = fields['muc#roomconfig_moderatedroom']; + dirty = dirty or (self:is_moderated() ~= moderated) + module:log("debug", "moderated=%s", tostring(moderated)); + + local membersonly = fields['muc#roomconfig_membersonly']; + dirty = dirty or (self:is_members_only() ~= membersonly) + module:log("debug", "membersonly=%s", tostring(membersonly)); + 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 - dirty = dirty or (self._data.hidden ~= (not public and true or nil)) - self._data.hidden = not public and true or nil; + dirty = dirty or (self:is_hidden() ~= (not public and true or nil)) + + local changesubject = fields['muc#roomconfig_changesubject']; + dirty = dirty or (self:get_changesubject() ~= (not changesubject and true or nil)) + module:log('debug', 'changesubject=%s', changesubject and "true" or "false") local whois = fields['muc#roomconfig_whois']; if not valid_whois[whois] then - origin.send(st.error_reply(stanza, 'cancel', 'bad-request')); + origin.send(st.error_reply(stanza, 'cancel', 'bad-request', "Invalid value for 'whois'")); return; end local whois_changed = self._data.whois ~= whois self._data.whois = whois - module:log('debug', 'whois=%s', tostring(whois)) + module:log('debug', 'whois=%s', whois) + + local password = fields['muc#roomconfig_roomsecret']; + if self:get_password() ~= password then + self:set_password(password); + end + self:set_moderated(moderated); + self:set_members_only(membersonly); + self:set_persistent(persistent); + self:set_hidden(not public); + self:set_changesubject(changesubject); if self.save then self:save(true); end origin.send(st.reply(stanza)); if dirty or whois_changed then - local msg = st.message({type='groupchat', from=self.jid}) - :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up() + local msg = st.message({type='groupchat', from=self.jid}) + :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up() - if dirty then - msg.tags[1]:tag('status', {code = '104'}) - end - if whois_changed then - local code = (whois == 'moderators') and 173 or 172 - msg.tags[1]:tag('status', {code = code}) - end + if dirty then + msg.tags[1]:tag('status', {code = '104'}):up(); + end + if whois_changed then + local code = (whois == 'moderators') and "173" or "172"; + msg.tags[1]:tag('status', {code = code}):up(); + end - self:broadcast_message(msg, false) + self:broadcast_message(msg, false) end end @@ -488,8 +711,7 @@ function room_mt:destroy(newjid, reason, password) end self._occupants[nick] = nil; end - self._data.persistent = nil; - if self.save then self:save(true); end + self:set_persistent(false); end function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc @@ -576,7 +798,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha end elseif xmlns == "http://jabber.org/protocol/muc#owner" and (type == "get" or type == "set") and stanza.tags[1].name == "query" then if self:get_affiliation(stanza.attr.from) ~= "owner" then - origin.send(st.error_reply(stanza, "auth", "forbidden")); + origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms")); elseif stanza.attr.type == "get" then self:send_form(origin, stanza); elseif stanza.attr.type == "set" then @@ -616,7 +838,8 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha stanza.attr.from = current_nick; local subject = getText(stanza, {"subject"}); if subject then - if occupant.role == "moderator" then + if occupant.role == "moderator" or + ( self._data.changesubject and occupant.role == "participant" ) then -- and participant self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza else stanza.attr.from = from; @@ -654,14 +877,21 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) :tag('invite', {from=_from}) :tag('reason'):text(_reason or ""):up() - :up() - :up() + :up(); + if self:get_password() then + invite:tag("password"):text(self:get_password()):up(); + end + invite: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(); + if self:is_members_only() and not self:get_affiliation(_invitee) then + log("debug", "%s invited %s into members only room %s, granting membership", _from, _invitee, _to); + self:set_affiliation(_from, _invitee, "member", nil, "Invited by " .. self._jid_nick[_from]) + end self:_route_stanza(invite); else origin.send(st.error_reply(stanza, "cancel", "jid-malformed")); @@ -699,19 +929,32 @@ function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) 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 + local actor_affiliation = self:get_affiliation(actor); + local target_affiliation = self:get_affiliation(jid); + if target_affiliation == affiliation then -- no change, shortcut + if callback then callback(); end + return true; + end + if actor_affiliation ~= "owner" then + if actor_affiliation ~= "admin" or target_affiliation == "owner" or target_affiliation == "admin" then + return nil, "cancel", "not-allowed"; + end + elseif target_affiliation == "owner" and jid_bare(actor) == jid then -- self change + local is_last = true; + for j, aff in pairs(self._affiliations) do if j ~= jid and aff == "owner" then is_last = false; break; end end + if is_last then + return nil, "cancel", "conflict"; + end + 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"}) + local x = st.stanza("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]; + local presence_type = nil; if not role then -- getting kicked - p.attr.type = "unavailable"; + presence_type = "unavailable"; if affiliation == "outcast" then x:tag("status", {code="301"}):up(); -- banned else @@ -724,20 +967,25 @@ function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) 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 + for jid,pres in pairs(occupant.sessions) do -- remove for all sessions of the nick if not role then self._jid_nick[jid] = nil; end + local p = st.clone(pres); + p.attr.from = nick; + p.attr.type = presence_type; p.attr.to = jid; + p:add_child(x); self:_route_stanza(p); + if occupant.jid == jid then + modified_nicks[nick] = p; + end end end end if self.save then self:save(); end if callback then callback(); end - for _, nick in ipairs(modified_nicks) do + for nick,p in pairs(modified_nicks) do p.attr.from = nick; self:broadcast_except_nick(p, nick); end @@ -748,34 +996,60 @@ function room_mt:get_role(nick) local session = self._occupants[nick]; return session and session.role or nil; end +function room_mt:can_set_role(actor_jid, occupant_jid, role) + local actor = self._occupants[self._jid_nick[actor_jid]]; + local occupant = self._occupants[occupant_jid]; + + if not occupant or not actor then return nil, "modify", "not-acceptable"; end + + if actor.role == "moderator" then + if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then + if actor.affiliation == "owner" or actor.affiliation == "admin" then + return true; + elseif occupant.role ~= "moderator" and role ~= "moderator" then + return true; + end + end + end + return nil, "cancel", "not-allowed"; +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 + local allowed, err_type, err_condition = self:can_set_role(actor, occupant_jid, role); + if not allowed then return allowed, err_type, err_condition; 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 - local p = st.presence({from = occupant_jid}) - :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) + local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) :tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"}) :tag("reason"):text(reason or ""):up() :up(); + local presence_type = nil; if not role then -- kick - p.attr.type = "unavailable"; + presence_type = "unavailable"; self._occupants[occupant_jid] = 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(); + x:tag("status", {code = "307"}):up(); else occupant.role = role; end - for jid in pairs(occupant.sessions) do -- send to all sessions of the nick + local bp; + for jid,pres in pairs(occupant.sessions) do -- send to all sessions of the nick + local p = st.clone(pres); + p.attr.from = occupant_jid; + p.attr.type = presence_type; p.attr.to = jid; + p:add_child(x); self:_route_stanza(p); + if occupant.jid == jid then + bp = p; + end end if callback then callback(); end - self:broadcast_except_nick(p, occupant_jid); + if bp then + self:broadcast_except_nick(bp, occupant_jid); + end return true; end @@ -817,13 +1091,14 @@ end local _M = {}; -- module "muc" -function _M.new_room(jid) +function _M.new_room(jid, config) return setmetatable({ jid = jid; _jid_nick = {}; _occupants = {}; _data = { - whois = 'moderators', + whois = 'moderators'; + history_length = (config and config.history_length); }; _affiliations = {}; }, room_mt); diff --git a/plugins/storage/mod_xep0227.lua b/plugins/storage/mod_xep0227.lua new file mode 100644 index 00000000..b6d2e627 --- /dev/null +++ b/plugins/storage/mod_xep0227.lua @@ -0,0 +1,163 @@ + +local ipairs, pairs = ipairs, pairs; +local setmetatable = setmetatable; +local tostring = tostring; +local next = next; +local t_remove = table.remove; +local os_remove = os.remove; +local io_open = io.open; + +local st = require "util.stanza"; +local parse_xml_real = module:require("xmlparse"); + +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 + t_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 = {}; + +function driver:open(host, datastore, typ) + 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 + return instance; +end + +module:add_item("data-driver", driver); 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;
|