diff options
Diffstat (limited to 'plugins')
62 files changed, 6057 insertions, 3462 deletions
diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua index 0cb4efe1..b544ddc8 100644 --- a/plugins/adhoc/adhoc.lib.lua +++ b/plugins/adhoc/adhoc.lib.lua @@ -12,7 +12,7 @@ local states = {} local _M = {}; -function _cmdtag(desc, status, sessionid, action) +local 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 @@ -34,7 +34,7 @@ function _M.handle_cmd(command, origin, stanza) local data, state = command:handler(dataIn, states[sessionid]); states[sessionid] = state; - local stanza = st.reply(stanza); + local cmdtag; if data.status == "completed" then states[sessionid] = nil; cmdtag = command:cmdtag("completed", sessionid); @@ -43,11 +43,12 @@ function _M.handle_cmd(command, origin, stanza) 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); + local reply = st.error_reply(stanza, data.error.type, data.error.condition, data.error.message); + origin.send(reply); return true; else cmdtag = command:cmdtag("executing", sessionid); + data.actions = data.actions or { "complete" }; end for name, content in pairs(data) do @@ -57,14 +58,14 @@ function _M.handle_cmd(command, origin, stanza) 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"); + elseif name == "actions" then + local actions = st.stanza("actions", { execute = content.default }); 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..'"'); + module:log("error", "Command %q at node %q provided an invalid action %q", + command.name, command.node, action); end end cmdtag:add_child(actions); @@ -76,8 +77,9 @@ function _M.handle_cmd(command, origin, stanza) cmdtag:add_child(content); end end - stanza:add_child(cmdtag); - origin.send(stanza); + local reply = st.reply(stanza); + reply:add_child(cmdtag); + origin.send(reply); return true; end diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua index 20c0f2be..f3e7f520 100644 --- a/plugins/adhoc/mod_adhoc.lua +++ b/plugins/adhoc/mod_adhoc.lua @@ -1,81 +1,86 @@ -- Copyright (C) 2009 Thilo Cestonaro --- Copyright (C) 2009-2010 Florian Zeitz +-- Copyright (C) 2009-2011 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 keys = require "util.iterators".keys; +local array_collect = require "util.array".collect; local is_admin = require "core.usermanager".is_admin; +local jid_split = require "util.jid".split; 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; - +module:hook("host-disco-info-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + if commands[node] then + local from = stanza.attr.from; + local privileged = is_admin(from, stanza.attr.to); + local global_admin = is_admin(from); + local username, hostname = jid_split(from); + local command = commands[node]; + if (command.permission == "admin" and privileged) + or (command.permission == "global_admin" and global_admin) + or (command.permission == "local_user" and hostname == module.host) + or (command.permission == "user") then + reply:tag("identity", { name = command.name, + category = "automation", type = "command-node" }):up(); + reply:tag("feature", { var = xmlns_cmd }):up(); + reply:tag("feature", { var = "jabber:x:data" }):up(); + event.exists = true; + else + return origin.send(st.error_reply(stanza, "auth", "forbidden", "This item is not available to you")); end + elseif node == xmlns_cmd then + reply:tag("identity", { name = "Ad-Hoc Commands", + category = "automation", type = "command-list" }):up(); + event.exists = true; 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 +module:hook("host-disco-items-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + if node ~= xmlns_cmd then + return; + end + + local from = stanza.attr.from; + local admin = is_admin(from, stanza.attr.to); + local global_admin = is_admin(from); + local username, hostname = jid_split(from); + local nodes = array_collect(keys(commands)):sort(); + for _, node in ipairs(nodes) do + local command = commands[node]; + if (command.permission == "admin" and admin) + or (command.permission == "global_admin" and global_admin) + or (command.permission == "local_user" and hostname == module.host) + or (command.permission == "user") then + reply:tag("item", { name = command.name, + node = node, jid = module:get_host() }); + reply:up(); end - origin.send(reply); - return true; end -end, 500); + event.exists = true; +end); 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 + local command = commands[node]; + if command then + local from = stanza.attr.from; + local admin = is_admin(from, stanza.attr.to); + local global_admin = is_admin(from); + local username, hostname = jid_split(from); + if (command.permission == "admin" and not admin) + or (command.permission == "global_admin" and not global_admin) + or (command.permission == "local_user" and hostname ~= module.host) 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"))); @@ -87,19 +92,14 @@ module:hook("iq/host/"..xmlns_cmd..":command", function (event) end end, 500); -local function handle_item_added(item) +local function adhoc_added(event) + local item = event.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) +local function adhoc_removed(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 + +module:handle_items("adhoc", adhoc_added, adhoc_removed); +module:handle_items("adhoc-provider", adhoc_added, adhoc_removed); diff --git a/plugins/mod_admin_adhoc.lua b/plugins/mod_admin_adhoc.lua index 984ae5ea..d5aaa0c4 100644 --- a/plugins/mod_admin_adhoc.lua +++ b/plugins/mod_admin_adhoc.lua @@ -1,4 +1,4 @@ --- Copyright (C) 2009-2010 Florian Zeitz +-- Copyright (C) 2009-2011 Florian Zeitz -- -- This file is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -10,179 +10,153 @@ local prosody = _G.prosody; local hosts = prosody.hosts; local t_concat = table.concat; -require "util.iterators"; +local module_host = module:get_host(); + +local keys = require "util.iterators".keys; local usermanager_user_exists = require "core.usermanager".user_exists; local usermanager_create_user = require "core.usermanager".create_user; +local usermanager_delete_user = require "core.usermanager".delete_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 hostmanager_activate = require "core.hostmanager".activate; +local hostmanager_deactivate = require "core.hostmanager".deactivate; local rm_load_roster = require "core.rostermanager".load_roster; -local st, jid, uuid = require "util.stanza", require "util.jid", require "util.uuid"; +local st, jid = require "util.stanza", require "util.jid"; 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 core_post_stanza = prosody.core_post_stanza; +local adhoc_simple = require "util.adhoc".new_simple_form; +local adhoc_initial = require "util.adhoc".new_initial_data_form; +module:depends("adhoc"); 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."; +local function generate_error_message(errors) + local errmsg = {}; + for name, err in pairs(errors) do + errmsg[#errmsg + 1] = name .. ": " .. err; + end + return { status = "completed", error = { message = t_concat(errmsg, "\n") } }; +end - { 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" }; - }; +-- Adding a new user +local add_user_layout = dataforms_new{ + title = "Adding a User"; + instructions = "Fill out this form to add a user."; - 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" } }; + { 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" }; +}; + +local add_user_command_handler = adhoc_simple(add_user_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local username, host, resource = jid.split(fields.accountjid); + if module_host ~= host then + return { status = "completed", error = { message = "Trying to add a user on " .. host .. " but command was sent to " .. module_host}}; + 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 %s@%s", username, host); + return { status = "completed", info = "Account successfully created" }; 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 + return { status = "completed", error = { message = "Failed to write data to disk" } }; 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"; + module:log("debug", "Invalid data, password mismatch or empty username while creating account for %s", fields.accountjid or "<nil>"); + return { status = "completed", error = { message = "Invalid data.\nPassword mismatch, or empty username" } }; end -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."; +-- Changing a user's password +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" }; - }; + { 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 +local change_user_password_command_handler = adhoc_simple(change_user_password_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local username, host, resource = jid.split(fields.accountjid); + if module_host ~= host then + return { status = "completed", error = { message = "Trying to change the password of a user on " .. host .. " but command was sent to " .. module_host}}; + 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 +end); + +-- Reloading the config +local function config_reload_handler(self, data, state) + local ok, err = prosody.reload_config(); + if ok then + return { status = "completed", info = "Configuration reloaded (modules may need to be reloaded for this to have an effect)" }; else - return { status = "executing", form = change_user_password_layout }, "executing"; + return { status = "completed", error = { message = "Failed to reload config: " .. tostring(err) } }; 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."; +-- Deleting a user's account +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" }; - }; + { 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 +local delete_user_command_handler = adhoc_simple(delete_user_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local failed = {}; + local succeeded = {}; + for _, aJID in ipairs(fields.accountjids) do + local username, host, resource = jid.split(aJID); + if (host == module_host) and usermanager_user_exists(username, host) and usermanager_delete_user(username, host) then + module:log("debug", "User %s has been deleted", aJID); + succeeded[#succeeded+1] = aJID; + else + module:log("debug", "Tried to delete non-existant user %s", aJID); + failed[#failed+1] = aJID; 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) + 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 "") }; +end); + +-- Ending a user's session +local 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); + module:log("debug", "Disconnecting %s@%s/%s", 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."; @@ -191,298 +165,374 @@ local end_user_session_layout = dataforms_new{ { name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) for which to end sessions" }; }; +local end_user_session_handler = adhoc_simple(end_user_session_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local failed = {}; + local succeeded = {}; + for _, aJID in ipairs(fields.accountjids) do + local username, host, resource = jid.split(aJID); + if (host == module_host) 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 "") }; +end); -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."; +-- Getting a user's password +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" }; - }; + { 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" }; - }; +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} } }; +local get_user_password_handler = adhoc_simple(get_user_password_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local user, host, resource = jid.split(fields.accountjid); + local accountjid = ""; + local password = ""; + if host ~= module_host then + return { status = "completed", error = { message = "Tried to get password for a user on " .. host .. " but command was sent to " .. module_host } }; + elseif usermanager_user_exists(user, host) then + accountjid = fields.accountjid; + password = usermanager_get_password(user, host); else - return { status = "executing", form = get_user_password_layout }, "executing"; + return { status = "completed", error = { message = "User does not exist" } }; end -end + return { status = "completed", result = { layout = get_user_password_result_layout, values = {accountjid = accountjid, password = password} } }; +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 +-- Getting a user's roster +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 fields = get_user_roster_layout:data(data.form); +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 not fields.accountjid then - return { status = "completed", error = { message = "Please specify a JID" } }; - end +local get_user_roster_handler = adhoc_simple(get_user_roster_layout, function(fields, err) + if err then + return generate_error_message(err); + 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(); + local user, host, resource = jid.split(fields.accountjid); + if host ~= module_host then + return { status = "completed", error = { message = "Tried to get roster for a user on " .. host .. " but command was sent to " .. module_host } }; + 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 - - 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."; + local query_text = tostring(query):gsub("><", ">\n<"); - { 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 result = get_user_roster_result_layout:form({ accountjid = user.."@"..host, roster = query_text }, "result"); + result:add_child(query); + return { status = "completed", other = result }; +end); - 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" }; - }; +-- Getting user statistics +local get_user_stats_layout = dataforms_new{ + title = "Get User Statistics"; + instructions = "Fill out this form to gather user statistics."; - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end + { 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 fields = get_user_stats_layout:data(data.form); +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 not fields.accountjid then - return { status = "completed", error = { message = "Please specify a JID." } }; - end +local get_user_stats_handler = adhoc_simple(get_user_stats_layout, function(fields, err) + if err then + return generate_error_message(err); + 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; + local user, host, resource = jid.split(fields.accountjid); + if host ~= module_host then + return { status = "completed", error = { message = "Tried to get stats for a user on " .. host .. " but command was sent to " .. module_host } }; + 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 - 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?"; + 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}} }; +end); - { 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" }; - }; +-- Getting a list of online users +local get_online_users_layout = dataforms_new{ + title = "Getting List of Online Users"; + instructions = "How many users should be returned at most?"; - 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" }; - }; + { 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" }; +}; - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end +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" }; +}; - local fields = get_online_users_layout:data(data.form); +local get_online_users_command_handler = adhoc_simple(get_online_users_layout, function(fields, err) + if err then + return generate_error_message(err); + end - 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 + 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[module_host].sessions or {}) do + if (max_items ~= nil) and (count >= max_items) then + break; + end + users[#users+1] = username.."@"..module_host; + 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 - users[#users+1] = " - "..resource..": "..status.."("..priority..")"; end + users[#users+1] = " - "..resource..": "..status.."("..priority..")"; 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 + return { status = "completed", result = {layout = get_online_users_result_layout, values = {onlineuserjids=t_concat(users, "\n")}} }; +end); + +-- Getting a list of loaded modules +local list_modules_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 function list_modules_handler(self, data, state) + local modules = array.collect(keys(hosts[module_host].modules)):sort():concat("\n"); + return { status = "completed", result = { layout = list_modules_result; values = { modules = modules } } }; end -function list_modules_handler(self, data, state) - local result = dataforms_new { - title = "List of loaded modules"; +-- Loading a module +local load_module_layout = dataforms_new { + title = "Load module"; + instructions = "Specify the module to be loaded"; - { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#list" }; - { name = "modules", type = "text-multi", label = "The following modules are 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:"}; +}; - local modules = array.collect(keys(hosts[data.to].modules)):sort():concat("\n"); +local load_module_handler = adhoc_simple(load_module_layout, function(fields, err) + if err then + return generate_error_message(err); + end + if modulemanager.is_loaded(module_host, fields.module) then + return { status = "completed", info = "Module already loaded" }; + end + local ok, err = modulemanager.load(module_host, fields.module); + if ok then + return { status = "completed", info = 'Module "'..fields.module..'" successfully loaded on host "'..module_host..'".' }; + else + return { status = "completed", error = { message = 'Failed to load module "'..fields.module..'" on host "'..module_host.. + '". Error was: "'..tostring(err or "<unspecified>")..'"' } }; + end +end); - return { status = "completed", result = { layout = result; values = { modules = modules } } }; -end +-- Globally loading a module +local globally_load_module_layout = dataforms_new { + title = "Globally load module"; + instructions = "Specify the module to be loaded on all hosts"; -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#global-load" }; + { name = "module", type = "text-single", required = true, label = "Module to globally load:"}; +}; - { 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" }; +local globally_load_module_handler = adhoc_simple(globally_load_module_layout, function(fields, err) + local ok_list, err_list = {}, {}; + + if err then + return generate_error_message(err); + end + + local ok, err = modulemanager.load(module_host, fields.module); + if ok then + ok_list[#ok_list + 1] = module_host; + else + err_list[#err_list + 1] = module_host .. " (Error: " .. tostring(err) .. ")"; + end + + -- Is this a global module? + if modulemanager.is_loaded("*", fields.module) and not modulemanager.is_loaded(module_host, fields.module) then + return { status = "completed", info = 'Global module '..fields.module..' loaded.' }; + end + + -- This is either a shared or "normal" module, load it on all other hosts + for host_name, host in pairs(hosts) do + if host_name ~= module_host and host.type == "local" then + local ok, err = modulemanager.load(host_name, fields.module); + if ok then + ok_list[#ok_list + 1] = host_name; + else + err_list[#err_list + 1] = host_name .. " (Error: " .. tostring(err) .. ")"; + end end - local ok, err = modulemanager.load(data.to, fields.module); + end + + local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully loaded onto the hosts:\n"..t_concat(ok_list, "\n")) or "") + .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. + (#err_list > 0 and ("Failed to load the module "..fields.module.." onto the hosts:\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; +end); + +-- Reloading modules +local reload_modules_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:"}; +}; + +local reload_modules_handler = adhoc_initial(reload_modules_layout, function() + return { modules = array.collect(keys(hosts[module_host].modules)):sort() }; +end, function(fields, err) + if err then + return generate_error_message(err); + end + local ok_list, err_list = {}, {}; + for _, module in ipairs(fields.modules) do + local ok, err = modulemanager.reload(module_host, module); if ok then - return { status = "completed", info = 'Module "'..fields.module..'" successfully loaded on host "'..data.to..'".' }; + ok_list[#ok_list + 1] = module; else - return { status = "completed", error = { message = 'Failed to load module "'..fields.module..'" on host "'..data.to.. - '". Error was: "'..tostring(err or "<unspecified>")..'"' } }; + err_list[#err_list + 1] = module .. "(Error: " .. tostring(err) .. ")"; end - else - local modules = array.collect(keys(hosts[data.to].modules)):sort(); - return { status = "executing", form = layout }, "executing"; end -end + local info = (#ok_list > 0 and ("The following modules were successfully reloaded on host "..module_host..":\n"..t_concat(ok_list, "\n")) or "") + .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. + (#err_list > 0 and ("Failed to reload the following modules on host "..module_host..":\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; +end); + +-- Globally reloading a module +local globally_reload_module_layout = dataforms_new { + title = "Globally reload module"; + instructions = "Specify the module to reload on all hosts"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#global-reload" }; + { name = "module", type = "list-single", required = true, label = "Module to globally reload:"}; +}; -function reload_modules_handler(self, data, state) - local layout = dataforms_new { - title = "Reload modules"; - instructions = "Select the modules to be reloaded"; +local globally_reload_module_handler = adhoc_initial(globally_reload_module_layout, function() + local loaded_modules = array(keys(modulemanager.get_modules("*"))); + for _, host in pairs(hosts) do + loaded_modules:append(array(keys(host.modules))); + end + loaded_modules = array(set.new(loaded_modules):items()):sort(); + return { module = loaded_modules }; +end, function(fields, err) + local is_global = false; - { 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)" - } }; + if err then + return generate_error_message(err); + end + + if modulemanager.is_loaded("*", fields.module) then + local ok, err = modulemanager.reload("*", fields.module); + if not ok then + return { status = "completed", info = 'Global module '..fields.module..' failed to reload: '..err }; end - local ok_list, err_list = {}, {}; - for _, module in ipairs(fields.modules) do - local ok, err = modulemanager.reload(data.to, module); + is_global = true; + end + + local ok_list, err_list = {}, {}; + for host_name, host in pairs(hosts) do + if modulemanager.is_loaded(host_name, fields.module) then + local ok, err = modulemanager.reload(host_name, fields.module); if ok then - ok_list[#ok_list + 1] = module; + ok_list[#ok_list + 1] = host_name; else - err_list[#err_list + 1] = module .. "(Error: " .. tostring(err) .. ")"; + err_list[#err_list + 1] = host_name .. " (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 #ok_list == 0 and #err_list == 0 then + if is_global then + return { status = "completed", info = 'Successfully reloaded global module '..fields.module }; + else + return { status = "completed", info = 'Module '..fields.module..' not loaded on any host.' }; + end + end + + local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully reloaded on the hosts:\n"..t_concat(ok_list, "\n")) or "") + .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. + (#err_list > 0 and ("Failed to reload the module "..fields.module.." on the hosts:\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; +end); + +local function send_to_online(message, server) if server then sessions = { [server] = hosts[server] }; else @@ -502,108 +552,208 @@ function send_to_online(message, server) 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"}, - }; +-- Shutting down the service +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" }; }; + { name = "announcement", type = "text-multi", label = "Announcement" }; +}; - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end +local shut_down_service_handler = adhoc_simple(shut_down_service_layout, function(fields, err) + if err then + return generate_error_message(err); + 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 - 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"), function(time) prosody.shutdown("Shutdown by adhoc command") end); - timer_add_task(tonumber(fields.delay or "5"), prosody.shutdown); + return { status = "completed", info = "Server is about to shut down" }; +end); - return { status = "completed", info = "Server is about to shut down" }; - else - return { status = "executing", form = shut_down_service_layout }, "executing"; - end +-- Unloading modules +local unload_modules_layout = dataforms_new { + title = "Unload modules"; + instructions = "Select the modules to be unloaded"; - 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:"}; +}; - { 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" }; +local unload_modules_handler = adhoc_initial(unload_modules_layout, function() + return { modules = array.collect(keys(hosts[module_host].modules)):sort() }; +end, function(fields, err) + if err then + return generate_error_message(err); + end + local ok_list, err_list = {}, {}; + for _, module in ipairs(fields.modules) do + local ok, err = modulemanager.unload(module_host, module); + if ok then + ok_list[#ok_list + 1] = module; + else + err_list[#err_list + 1] = module .. "(Error: " .. tostring(err) .. ")"; 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 info = (#ok_list > 0 and ("The following modules were successfully unloaded on host "..module_host..":\n"..t_concat(ok_list, "\n")) or "") + .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. + (#err_list > 0 and ("Failed to unload the following modules on host "..module_host..":\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; +end); + +-- Globally unloading a module +local globally_unload_module_layout = dataforms_new { + title = "Globally unload module"; + instructions = "Specify a module to unload on all hosts"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#global-unload" }; + { name = "module", type = "list-single", required = true, label = "Module to globally unload:"}; +}; + +local globally_unload_module_handler = adhoc_initial(globally_unload_module_layout, function() + local loaded_modules = array(keys(modulemanager.get_modules("*"))); + for _, host in pairs(hosts) do + loaded_modules:append(array(keys(host.modules))); + end + loaded_modules = array(set.new(loaded_modules):items()):sort(); + return { module = loaded_modules }; +end, function(fields, err) + local is_global = false; + if err then + return generate_error_message(err); + end + + if modulemanager.is_loaded("*", fields.module) then + local ok, err = modulemanager.unload("*", fields.module); + if not ok then + return { status = "completed", info = 'Global module '..fields.module..' failed to unload: '..err }; end - local ok_list, err_list = {}, {}; - for _, module in ipairs(fields.modules) do - local ok, err = modulemanager.unload(data.to, module); + is_global = true; + end + + local ok_list, err_list = {}, {}; + for host_name, host in pairs(hosts) do + if modulemanager.is_loaded(host_name, fields.module) then + local ok, err = modulemanager.unload(host_name, fields.module); if ok then - ok_list[#ok_list + 1] = module; + ok_list[#ok_list + 1] = host_name; else - err_list[#err_list + 1] = module .. "(Error: " .. tostring(err) .. ")"; + err_list[#err_list + 1] = host_name .. " (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 }; + end + + if #ok_list == 0 and #err_list == 0 then + if is_global then + return { status = "completed", info = 'Successfully unloaded global module '..fields.module }; + else + return { status = "completed", info = 'Module '..fields.module..' not loaded on any host.' }; + end + end + + local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully unloaded on the hosts:\n"..t_concat(ok_list, "\n")) or "") + .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") .. + (#err_list > 0 and ("Failed to unload the module "..fields.module.." on the hosts:\n"..t_concat(err_list, "\n")) or ""); + return { status = "completed", info = info }; +end); + +-- Activating a host +local activate_host_layout = dataforms_new { + title = "Activate host"; + instructions = ""; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/hosts#activate" }; + { name = "host", type = "text-single", required = true, label = "Host:"}; +}; + +local activate_host_handler = adhoc_simple(activate_host_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local ok, err = hostmanager_activate(fields.host); + + if ok then + return { status = "completed", info = fields.host .. " activated" }; else - local modules = array.collect(keys(hosts[data.to].modules)):sort(); - return { status = "executing", form = { layout = layout; values = { modules = modules } } }, "executing"; + return { status = "canceled", error = err } end -end +end); + +-- Deactivating a host +local deactivate_host_layout = dataforms_new { + title = "Deactivate host"; + instructions = ""; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/hosts#activate" }; + { name = "host", type = "text-single", required = true, label = "Host:"}; +}; + +local deactivate_host_handler = adhoc_simple(deactivate_host_layout, function(fields, err) + if err then + return generate_error_message(err); + end + local ok, err = hostmanager_deactivate(fields.host); + + if ok then + return { status = "completed", info = fields.host .. " deactivated" }; + else + return { status = "canceled", error = err } + 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 config_reload_desc = adhoc_new("Reload configuration", "http://prosody.im/protocol/config#reload", config_reload_handler, "global_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 get_online_users_desc = adhoc_new("Get List of Online Users", "http://jabber.org/protocol/admin#get-online-users-list", 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 globally_load_module_desc = adhoc_new("Globally load module", "http://prosody.im/protocol/modules#global-load", globally_load_module_handler, "global_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 globally_reload_module_desc = adhoc_new("Globally reload module", "http://prosody.im/protocol/modules#global-reload", globally_reload_module_handler, "global_admin"); +local shut_down_service_desc = adhoc_new("Shut Down Service", "http://jabber.org/protocol/admin#shutdown", shut_down_service_handler, "global_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); +local globally_unload_module_desc = adhoc_new("Globally unload module", "http://prosody.im/protocol/modules#global-unload", globally_unload_module_handler, "global_admin"); +local activate_host_desc = adhoc_new("Activate host", "http://prosody.im/protocol/hosts#activate", activate_host_handler, "global_admin"); +local deactivate_host_desc = adhoc_new("Deactivate host", "http://prosody.im/protocol/hosts#deactivate", deactivate_host_handler, "global_admin"); + +module:provides("adhoc", add_user_desc); +module:provides("adhoc", change_user_password_desc); +module:provides("adhoc", config_reload_desc); +module:provides("adhoc", delete_user_desc); +module:provides("adhoc", end_user_session_desc); +module:provides("adhoc", get_user_password_desc); +module:provides("adhoc", get_user_roster_desc); +module:provides("adhoc", get_user_stats_desc); +module:provides("adhoc", get_online_users_desc); +module:provides("adhoc", list_modules_desc); +module:provides("adhoc", load_module_desc); +module:provides("adhoc", globally_load_module_desc); +module:provides("adhoc", reload_modules_desc); +module:provides("adhoc", globally_reload_module_desc); +module:provides("adhoc", shut_down_service_desc); +module:provides("adhoc", unload_modules_desc); +module:provides("adhoc", globally_unload_module_desc); +module:provides("adhoc", activate_host_desc); +module:provides("adhoc", deactivate_host_desc); diff --git a/plugins/mod_admin_telnet.lua b/plugins/mod_admin_telnet.lua index da40f57e..e13d27c2 100644 --- a/plugins/mod_admin_telnet.lua +++ b/plugins/mod_admin_telnet.lua @@ -1,38 +1,45 @@ -- 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. -- -module.host = "*"; +module:set_global(); + +local hostmanager = require "core.hostmanager"; +local modulemanager = require "core.modulemanager"; +local s2smanager = require "core.s2smanager"; +local portmanager = require "core.portmanager"; local _G = _G; local prosody = _G.prosody; local hosts = prosody.hosts; -local connlisteners_register = require "net.connlisteners".register; -local console_listener = { default_port = 5582; default_mode = "*l"; default_interface = "127.0.0.1" }; +local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" }; -require "util.iterators"; -local jid_bare = require "util.jid".bare; +local iterators = require "util.iterators"; +local keys, values = iterators.keys, iterators.values; +local jid = require "util.jid"; +local jid_bare, jid_split = jid.bare, jid.split; local set, array = require "util.set", require "util.array"; local cert_verify_identity = require "util.x509".verify_identity; +local envload = require "util.envload".envload; +local envloadfile = require "util.envload".envloadfile; -local commands = {}; -local def_env = {}; +local commands = module:shared("commands") +local def_env = module:shared("env"); local default_env_mt = { __index = def_env }; - -prosody.console = { commands = commands, env = def_env }; +local core_post_stanza = prosody.core_post_stanza; local function redirect_output(_G, session) local env = setmetatable({ print = session.print }, { __index = function (t, k) return rawget(_G, k); end }); env.dofile = function(name) - local f, err = loadfile(name); + local f, err = envloadfile(name, env); if not f then return f, err; end - return setfenv(f, env)(); + return f(); end; return env; end @@ -53,17 +60,75 @@ function console:new_session(conn) disconnect = function () conn:close(); end; }; session.env = setmetatable({}, default_env_mt); - + -- Load up environment with helper objects for name, t in pairs(def_env) do if type(t) == "table" then session.env[name] = setmetatable({ session = session }, { __index = t }); end end - + return session; end +function console:process_line(session, line) + local useglobalenv; + + if line:match("^>") then + line = line:gsub("^>", ""); + useglobalenv = true; + elseif line == "\004" then + commands["bye"](session, line); + return; + else + local command = line:match("^%w+") or line:match("%p"); + if commands[command] then + commands[command](session, line); + return; + end + end + + session.env._ = line; + + local chunkname = "=console"; + local env = (useglobalenv and redirect_output(_G, session)) or session.env or nil + local chunk, err = envload("return "..line, chunkname, env); + if not chunk then + chunk, err = envload(line, chunkname, env); + if not chunk then + err = err:gsub("^%[string .-%]:%d+: ", ""); + err = err:gsub("^:%d+: ", ""); + err = err:gsub("'<eof>'", "the end of the line"); + session.print("Sorry, I couldn't understand that... "..err); + return; + end + end + + local ranok, taskok, message = pcall(chunk); + + if not (ranok or message or useglobalenv) and commands[line:lower()] then + commands[line:lower()](session, line); + return; + end + + if not ranok then + session.print("Fatal error while running command, it did not complete"); + session.print("Error: "..taskok); + return; + end + + if not message then + session.print("Result: "..tostring(taskok)); + return; + elseif (not taskok) and message then + session.print("Command completed with a problem"); + session.print("Message: "..tostring(message)); + return; + end + + session.print("OK: "..tostring(message)); +end + local sessions = {}; function console_listener.onconnect(conn) @@ -77,68 +142,17 @@ end function console_listener.onincoming(conn, data) local session = sessions[conn]; - -- Handle data - (function(session, data) - local useglobalenv; - - if data:match("^>") then - data = data:gsub("^>", ""); - useglobalenv = true; - elseif data == "\004" then - commands["bye"](session, data); - return; - else - local command = data:lower(); - command = data:match("^%w+") or data:match("%p"); - if commands[command] then - commands[command](session, data); - return; - end - end + local partial = session.partial_data; + if partial then + data = partial..data; + end - session.env._ = data; - - local chunkname = "=console"; - local chunk, err = loadstring("return "..data, chunkname); - if not chunk then - chunk, err = loadstring(data, chunkname); - if not chunk then - err = err:gsub("^%[string .-%]:%d+: ", ""); - err = err:gsub("^:%d+: ", ""); - err = err:gsub("'<eof>'", "the end of the line"); - session.print("Sorry, I couldn't understand that... "..err); - return; - end - end - - setfenv(chunk, (useglobalenv and redirect_output(_G, session)) or session.env or nil); - - local ranok, taskok, message = pcall(chunk); - - if not (ranok or message or useglobalenv) and commands[data:lower()] then - commands[data:lower()](session, data); - return; - end - - if not ranok then - session.print("Fatal error while running command, it did not complete"); - session.print("Error: "..taskok); - return; - end - - if not message then - session.print("Result: "..tostring(taskok)); - return; - elseif (not taskok) and message then - session.print("Command completed with a problem"); - session.print("Message: "..tostring(message)); - return; - end - - session.print("OK: "..tostring(message)); - end)(session, data); - - session.send(string.char(0)); + for line in data:gmatch("[^\n]*[\n\004]") do + if session.closed then return end + console:process_line(session, line); + session.send(string.char(0)); + end + session.partial_data = data:match("[^\n]+$"); end function console_listener.ondisconnect(conn, err) @@ -149,13 +163,12 @@ function console_listener.ondisconnect(conn, err) end end -connlisteners_register('console', console_listener); - -- Console commands -- -- These are simple commands, not valid standalone in Lua function commands.bye(session) session.print("See you! :)"); + session.closed = true; session.disconnect(); end commands.quit, commands.exit = commands.bye, commands.bye; @@ -190,7 +203,10 @@ function commands.help(session, data) print [[s2s - Commands to manage sessions between this server and others]] print [[module - Commands to load/reload/unload modules/plugins]] print [[host - Commands to activate, deactivate and list virtual hosts]] + print [[user - Commands to create and delete users, and change their passwords]] print [[server - Uptime, version, shutting down, etc.]] + print [[port - Commands to manage ports the server is listening on]] + print [[dns - Commands to manage and inspect the internal DNS resolver]] print [[config - Reloading the configuration, etc.]] print [[console - Help regarding the console itself]] elseif section == "c2s" then @@ -201,6 +217,7 @@ function commands.help(session, data) elseif section == "s2s" then print [[s2s:show(domain) - Show all s2s connections for the given domain (or all if no domain given)]] print [[s2s:close(from, to) - Close a connection from one domain to another]] + print [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]] elseif section == "module" then print [[module:load(module, host) - Load the specified module on the specified host (or all hosts if none given)]] print [[module:reload(module, host) - The same, but unloads and loads the module (saving state if the module supports it)]] @@ -210,10 +227,25 @@ function commands.help(session, data) 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 == "user" then + print [[user:create(jid, password) - Create the specified user account]] + print [[user:password(jid, password) - Set the password for the specified user account]] + print [[user:delete(jid) - Permanently remove the specified user account]] + print [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]] elseif section == "server" then print [[server:version() - Show the server's version number]] print [[server:uptime() - Show how long the server has been running]] + print [[server:memory() - Show details about the server's memory usage]] print [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]] + elseif section == "port" then + print [[port:list() - Lists all network ports prosody currently listens on]] + print [[port:close(port, interface) - Close a port]] + elseif section == "dns" then + print [[dns:lookup(name, type, class) - Do a DNS lookup]] + print [[dns:addnameserver(nameserver) - Add a nameserver to the list]] + print [[dns:setnameserver(nameserver) - Replace the list of name servers with the supplied one]] + print [[dns:purge() - Clear the DNS cache]] + print [[dns:cache() - Show cached records]] elseif section == "config" then print [[config:reload() - Reload the server configuration. Modules may need to be reloaded for changes to take effect.]] elseif section == "console" then @@ -268,6 +300,26 @@ function def_env.server:shutdown(reason) return true, "Shutdown initiated"; end +local function human(kb) + local unit = "K"; + if kb > 1024 then + kb, unit = kb/1024, "M"; + end + return ("%0.2f%sB"):format(kb, unit); +end + +function def_env.server:memory() + if not pposix.meminfo then + return true, "Lua is using "..collectgarbage("count"); + end + local mem, lua_mem = pposix.meminfo(), collectgarbage("count"); + local print = self.session.print; + print("Process: "..human((mem.allocated+mem.allocated_mmap)/1024)); + print(" Used: "..human(mem.used/1024).." ("..human(lua_mem).." by Lua)"); + print(" Free: "..human(mem.unused/1024).." ("..human(mem.returnable/1024).." returnable)"); + return true, "OK"; +end + def_env.module = {}; local function get_hosts_set(hosts, module) @@ -281,39 +333,49 @@ local function get_hosts_set(hosts, module) return set.new { hosts }; elseif hosts == nil then local mm = require "modulemanager"; - return set.new(array.collect(keys(prosody.hosts))) - / function (host) return prosody.hosts[host].type == "local" or module and mm.is_loaded(host, module); end; + local hosts_set = set.new(array.collect(keys(prosody.hosts))) + / function (host) return (prosody.hosts[host].type == "local" or module and mm.is_loaded(host, module)) and host or nil; end; + if module and mm.get_module("*", module) then + hosts_set:add("*"); + end + return hosts_set; end end function def_env.module:load(name, hosts, config) local mm = require "modulemanager"; - + hosts = get_hosts_set(hosts); - + -- Load the module for each host - local ok, err, count = true, nil, 0; + local ok, err, count, mod = true, nil, 0, nil; for host in hosts do if (not mm.is_loaded(host, name)) then - ok, err = mm.load(host, name, config); - if not ok then + mod, err = mm.load(host, name, config); + if not mod then ok = false; + if err == "global-module-already-loaded" then + if count > 0 then + ok, err, count = true, nil, 1; + end + break; + end self.session.print(err or "Unknown error loading module"); else count = count + 1; - self.session.print("Loaded for "..host); + self.session.print("Loaded for "..mod.module.host); end end end - - return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); + + return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); end function def_env.module:unload(name, hosts) local mm = require "modulemanager"; hosts = get_hosts_set(hosts, name); - + -- Unload the module for each host local ok, err, count = true, nil, 0; for host in hosts do @@ -334,11 +396,15 @@ end function def_env.module:reload(name, hosts) local mm = require "modulemanager"; - hosts = get_hosts_set(hosts, name); - + hosts = array.collect(get_hosts_set(hosts, name)):sort(function (a, b) + if a == "*" then return true + elseif b == "*" then return false + else return a < b; end + end); + -- Reload the module for each host local ok, err, count = true, nil, 0; - for host in hosts do + for _, host in ipairs(hosts) do if mm.is_loaded(host, name) then ok, err = mm.reload(host, name); if not ok then @@ -359,6 +425,7 @@ end function def_env.module:list(hosts) if hosts == nil then hosts = array.collect(keys(prosody.hosts)); + table.insert(hosts, 1, "*"); end if type(hosts) == "string" then hosts = { hosts }; @@ -366,11 +433,11 @@ function def_env.module:list(hosts) if type(hosts) ~= "table" then return false, "Please supply a host or a list of hosts you would like to see"; end - + local print = self.session.print; for _, host in ipairs(hosts) do - print(host..":"); - local modules = array.collect(keys(prosody.hosts[host] and prosody.hosts[host].modules or {})):sort(); + print((host == "*" and "Global" or host)..":"); + local modules = array.collect(keys(modulemanager.get_modules(host) or {})):sort(); if #modules == 0 then if prosody.hosts[host] then print(" No modules loaded"); @@ -416,6 +483,25 @@ end function def_env.hosts:add(name) end +local function session_flags(session, line) + line = line or {}; + if session.cert_identity_status == "valid" then + line[#line+1] = "(secure)"; + elseif session.secure then + line[#line+1] = "(encrypted)"; + end + if session.compressed then + line[#line+1] = "(compressed)"; + end + if session.smacks then + line[#line+1] = "(sm)"; + end + if session.ip and session.ip:match(":") then + line[#line+1] = "(IPv6)"; + end + return table.concat(line, " "); +end + def_env.c2s = {}; local function show_c2s(callback) @@ -429,6 +515,16 @@ local function show_c2s(callback) end end +function def_env.c2s:count(match_jid) + local count = 0; + show_c2s(function (jid, session) + if (not match_jid) or jid:match(match_jid) then + count = count + 1; + end + end); + return true, "Total: "..count.." clients"; +end + function def_env.c2s:show(match_jid) local print, count = self.session.print, 0; local curr_host; @@ -441,15 +537,10 @@ function def_env.c2s:show(match_jid) count = count + 1; 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 + status = session.presence:get_child_text("show") or "available"; end - print(" "..jid.." - "..status.."("..priority..")"); - end + print(session_flags(session, { " "..jid.." - "..status.."("..priority..")" })); + end end); return true, "Total: "..count.." clients"; end @@ -460,7 +551,7 @@ function def_env.c2s:show_insecure(match_jid) if ((not match_jid) or jid:match(match_jid)) and not session.secure then count = count + 1; print(jid); - end + end end); return true, "Total: "..count.." insecure client connections"; end @@ -471,13 +562,13 @@ function def_env.c2s:show_secure(match_jid) if ((not match_jid) or jid:match(match_jid)) and session.secure then count = count + 1; print(jid); - end + end end); return true, "Total: "..count.." secure client connections"; end function def_env.c2s:close(match_jid) - local print, count = self.session.print, 0; + local count = 0; show_c2s(function (jid, session) if jid == match_jid or jid_bare(jid) == match_jid then count = count + 1; @@ -487,78 +578,80 @@ function def_env.c2s:close(match_jid) return true, "Total: "..count.." sessions closed"; end + def_env.s2s = {}; function def_env.s2s:show(match_jid) - local _print = self.session.print; local print = self.session.print; - + local count_in, count_out = 0,0; - - for host, host_session in pairs(hosts) do - print = function (...) _print(host); _print(...); print = _print; end - for remotehost, session in pairs(host_session.s2sout) do - if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then - count_out = count_out + 1; - print(" "..host.." -> "..remotehost..(session.cert_identity_status == "valid" and " (secure)" or "")..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or "")); - if session.sendq then - print(" There are "..#session.sendq.." queued outgoing stanzas for this connection"); - end - if session.type == "s2sout_unauthed" then - if session.connecting then - print(" Connection not yet established"); - if not session.srv_hosts then - if not session.conn then - print(" We do not yet have a DNS answer for this host's SRV records"); - else - print(" This host has no SRV records, using A record instead"); - end - elseif session.srv_choice then - print(" We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts); - local srv_choice = session.srv_hosts[session.srv_choice]; - print(" Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269)); + local s2s_list = { }; + + local s2s_sessions = module:shared"/*/s2s/sessions"; + for _, session in pairs(s2s_sessions) do + local remotehost, localhost, direction; + if session.direction == "outgoing" then + direction = "->"; + count_out = count_out + 1; + remotehost, localhost = session.to_host or "?", session.from_host or "?"; + else + direction = "<-"; + count_in = count_in + 1; + remotehost, localhost = session.from_host or "?", session.to_host or "?"; + end + local sess_lines = { l = localhost, r = remotehost, + session_flags(session, { "", direction, remotehost or "?", + "["..session.type..tostring(session):match("[a-f0-9]*$").."]" })}; + + if (not match_jid) or remotehost:match(match_jid) or localhost:match(match_jid) then + table.insert(s2s_list, sess_lines); + local print = function (s) table.insert(sess_lines, " "..s); end + if session.sendq then + print("There are "..#session.sendq.." queued outgoing stanzas for this connection"); + end + if session.type == "s2sout_unauthed" then + if session.connecting then + print("Connection not yet established"); + if not session.srv_hosts then + if not session.conn then + print("We do not yet have a DNS answer for this host's SRV records"); + else + print("This host has no SRV records, using A record instead"); end - elseif session.notopen then - print(" The <stream> has not yet been opened"); - elseif not session.dialback_key then - print(" Dialback has not been initiated yet"); - elseif session.dialback_key then - print(" Dialback has been requested, but no result received"); + elseif session.srv_choice then + print("We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts); + local srv_choice = session.srv_hosts[session.srv_choice]; + print("Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269)); end + elseif session.notopen then + print("The <stream> has not yet been opened"); + elseif not session.dialback_key then + print("Dialback has not been initiated yet"); + elseif session.dialback_key then + print("Dialback has been requested, but no result received"); end end - end - local subhost_filter = function (h) - return (match_jid and h:match(match_jid)); - end - for session in pairs(incoming_s2s) do - if session.to_host == host and ((not match_jid) or host:match(match_jid) - or (session.from_host and session.from_host:match(match_jid)) - -- Pft! is what I say to list comprehensions - or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then - count_in = count_in + 1; - print(" "..host.." <- "..(session.from_host or "(unknown)")..(session.cert_identity_status == "valid" and " (secure)" or "")..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or "")); - if session.type == "s2sin_unauthed" then - print(" Connection not yet authenticated"); - end + if session.type == "s2sin_unauthed" then + print("Connection not yet authenticated"); + elseif session.type == "s2sin" then for name in pairs(session.hosts) do if name ~= session.from_host then - print(" also hosts "..tostring(name)); + print("also hosts "..tostring(name)); end end end end - - print = _print; end - - for session in pairs(incoming_s2s) do - if not session.to_host and ((not match_jid) or session.from_host and session.from_host:match(match_jid)) then - count_in = count_in + 1; - print("Other incoming s2s connections"); - print(" (unknown) <- "..(session.from_host or "(unknown)")); - end + + -- Sort by local host, then remote host + table.sort(s2s_list, function(a,b) + if a.l == b.l then return a.r < b.r; end + return a.l < b.l; + end); + local lasthost; + for _, sess_lines in ipairs(s2s_list) do + if sess_lines.l ~= lasthost then print(sess_lines.l); lasthost=sess_lines.l end + for _, line in ipairs(sess_lines) do print(line); end end - return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections"; end @@ -573,31 +666,41 @@ local function print_subject(print, subject) end end +-- As much as it pains me to use the 0-based depths that OpenSSL does, +-- I think there's going to be more confusion among operators if we +-- break from that. +local function print_errors(print, errors) + for depth, t in pairs(errors) do + print( + (" %d: %s"):format( + depth-1, + table.concat(t, "\n| ") + ) + ); + end +end + function def_env.s2s:showcert(domain) local ser = require "util.serialization".serialize; local print = self.session.print; - local domain_sessions = set.new(array.collect(keys(incoming_s2s))) - /function(session) return session.from_host == domain; end; - for local_host in values(prosody.hosts) do - local s2sout = local_host.s2sout; - if s2sout and s2sout[domain] then - domain_sessions:add(s2sout[domain]); - end - end + local s2s_sessions = module:shared"/*/s2s/sessions"; + local domain_sessions = set.new(array.collect(values(s2s_sessions))) + /function(session) return (session.to_host == domain or session.from_host == domain) and session or nil; end; local cert_set = {}; for session in domain_sessions do local conn = session.conn; conn = conn and conn:socket(); - if not conn.getpeercertificate then + if not conn.getpeerchain then if conn.dohandshake then error("This version of LuaSec does not support certificate viewing"); end else local cert = conn:getpeercertificate(); if cert then + local certs = conn:getpeerchain(); local digest = cert:digest("sha1"); if not cert_set[digest] then - local chain_valid, chain_err = conn:getpeerchainvalid(); + local chain_valid, chain_errors = conn:getpeerverification(); cert_set[digest] = { { from = session.from_host, @@ -605,8 +708,8 @@ function def_env.s2s:showcert(domain) direction = session.direction }; chain_valid = chain_valid; - chain_err = chain_err; - cert = cert; + chain_errors = chain_errors; + certs = certs; }; else table.insert(cert_set[digest], { @@ -620,22 +723,22 @@ function def_env.s2s:showcert(domain) end local domain_certs = array.collect(values(cert_set)); -- Phew. We now have a array of unique certificates presented by domain. - local print = self.session.print; local n_certs = #domain_certs; - + if n_certs == 0 then return "No certificates found for "..domain; end - + local function _capitalize_and_colon(byte) return string.upper(byte)..":"; end local function pretty_fingerprint(hash) return hash:gsub("..", _capitalize_and_colon):sub(1, -2); end - + for cert_info in values(domain_certs) do - local cert = cert_info.cert; + local certs = cert_info.certs; + local cert = certs[1]; print("---") print("Fingerprint (SHA1): "..pretty_fingerprint(cert:digest("sha1"))); print(""); @@ -649,9 +752,15 @@ function def_env.s2s:showcert(domain) end end print(""); - local chain_valid, err = cert_info.chain_valid, cert_info.chain_err; + local chain_valid, errors = cert_info.chain_valid, cert_info.chain_errors; local valid_identity = cert_verify_identity(domain, "xmpp-server", cert); - print("Trusted certificate: "..(chain_valid and "Yes" or ("No ("..err..")"))); + if chain_valid then + print("Trusted certificate: Yes"); + else + print("Trusted certificate: No"); + print_errors(print, errors); + end + print(""); print("Issuer: "); print_subject(print, cert:issuer()); print(""); @@ -667,46 +776,42 @@ end function def_env.s2s:close(from, to) local print, count = self.session.print, 0; - - if not (from and to) then + local s2s_sessions = module:shared"/*/s2s/sessions"; + + local match_id; + if from and not to then + match_id, from = from; + elseif not to then return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'"; elseif from == to then return false, "Both from and to are the same... you can't do that :)"; end - - if hosts[from] and not hosts[to] then - -- Is an outgoing connection - local session = hosts[from].s2sout[to]; - if not session then - print("No outgoing connection from "..from.." to "..to) - else + + for _, session in pairs(s2s_sessions) do + local id = session.type..tostring(session):match("[a-f0-9]+$"); + if (match_id and match_id == id) + or (session.from_host == from and session.to_host == to) then + print(("Closing connection from %s to %s [%s]"):format(session.from_host, session.to_host, id)); (session.close or s2smanager.destroy_session)(session); - count = count + 1; - print("Closed outgoing session from "..from.." to "..to); + count = count + 1 ; end - elseif hosts[to] and not hosts[from] then - -- Is an incoming connection - for session in pairs(incoming_s2s) do - if session.to_host == to and session.from_host == from then - (session.close or s2smanager.destroy_session)(session); - count = count + 1; end - end - - if count == 0 then - print("No incoming connections from "..from.." to "..to); - else - print("Closed "..count.." incoming session"..((count == 1 and "") or "s").." from "..from.." to "..to); - end - elseif hosts[to] and hosts[from] then - return false, "Both of the hostnames you specified are local, there are no s2s sessions to close"; - else - return false, "Neither of the hostnames you specified are being used on this server"; - end - return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end +function def_env.s2s:closeall(host) + local count = 0; + local s2s_sessions = module:shared"/*/s2s/sessions"; + for _,session in pairs(s2s_sessions) do + if not host or session.from_host == host or session.to_host == host then + session:close(); + count = count + 1; + end + end + if count == 0 then return false, "No sessions to close."; + else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end +end + def_env.host = {}; def_env.hosts = def_env.host; function def_env.host:activate(hostname, config) @@ -726,34 +831,232 @@ function def_env.host:list() return true, i.." hosts"; end +def_env.port = {}; + +function def_env.port:list() + local print = self.session.print; + local services = portmanager.get_active_services().data; + local ordered_services, n_ports = {}, 0; + for service, interfaces in pairs(services) do + table.insert(ordered_services, service); + end + table.sort(ordered_services); + for _, service in ipairs(ordered_services) do + local ports_list = {}; + for interface, ports in pairs(services[service]) do + for port in pairs(ports) do + table.insert(ports_list, "["..interface.."]:"..port); + end + end + n_ports = n_ports + #ports_list; + print(service..": "..table.concat(ports_list, ", ")); + end + return true, #ordered_services.." services listening on "..n_ports.." ports"; +end + +function def_env.port:close(close_port, close_interface) + close_port = assert(tonumber(close_port), "Invalid port number"); + local n_closed = 0; + local services = portmanager.get_active_services().data; + for service, interfaces in pairs(services) do + for interface, ports in pairs(interfaces) do + if not close_interface or close_interface == interface then + if ports[close_port] then + self.session.print("Closing ["..interface.."]:"..close_port.."..."); + local ok, err = portmanager.close(interface, close_port) + if not ok then + self.session.print("Failed to close "..interface.." "..close_port..": "..err); + else + n_closed = n_closed + 1; + end + end + end + end + end + return true, "Closed "..n_closed.." ports"; +end + +def_env.muc = {}; + +local console_room_mt = { + __index = function (self, k) return self.room[k]; end; + __tostring = function (self) + return "MUC room <"..self.room.jid..">"; + end; +}; + +local function check_muc(jid) + local room_name, host = jid_split(jid); + if not hosts[host] then + return nil, "No such host: "..host; + elseif not hosts[host].modules.muc then + return nil, "Host '"..host.."' is not a MUC service"; + end + return room_name, host; +end + +function def_env.muc:create(room_jid) + local room, host = check_muc(room_jid); + if not room then return nil, host end + if hosts[host].modules.muc.rooms[room_jid] then return nil, "Room exists already" end + return hosts[host].modules.muc.create_room(room_jid); +end + +function def_env.muc:room(room_jid) + local room_name, host = check_muc(room_jid); + local room_obj = hosts[host].modules.muc.rooms[room_jid]; + if not room_obj then + return nil, "No such room: "..room_jid; + end + return setmetatable({ room = room_obj }, console_room_mt); +end + +local um = require"core.usermanager"; + +def_env.user = {}; +function def_env.user:create(jid, password) + local username, host = jid_split(jid); + if not hosts[host] then + return nil, "No such host: "..host; + elseif um.user_exists(username, host) then + return nil, "User exists"; + end + local ok, err = um.create_user(username, password, host); + if ok then + return true, "User created"; + else + return nil, "Could not create user: "..err; + end +end + +function def_env.user:delete(jid) + local username, host = jid_split(jid); + if not hosts[host] then + return nil, "No such host: "..host; + elseif not um.user_exists(username, host) then + return nil, "No such user"; + end + local ok, err = um.delete_user(username, host); + if ok then + return true, "User deleted"; + else + return nil, "Could not delete user: "..err; + end +end + +function def_env.user:password(jid, password) + local username, host = jid_split(jid); + if not hosts[host] then + return nil, "No such host: "..host; + elseif not um.user_exists(username, host) then + return nil, "No such user"; + end + local ok, err = um.set_password(username, password, host); + if ok then + return true, "User password changed"; + else + return nil, "Could not change password for user: "..err; + end +end + +function def_env.user:list(host, pat) + if not host then + return nil, "No host given"; + elseif not hosts[host] then + return nil, "No such host"; + end + local print = self.session.print; + local total, matches = 0, 0; + for user in um.users(host) do + if not pat or user:match(pat) then + print(user.."@"..host); + matches = matches + 1; + end + total = total + 1; + end + return true, "Showing "..(pat and (matches.." of ") or "all " )..total.." users"; +end + +def_env.xmpp = {}; + +local st = require "util.stanza"; +function def_env.xmpp:ping(localhost, remotehost) + if hosts[localhost] then + core_post_stanza(hosts[localhost], + st.iq{ from=localhost, to=remotehost, type="get", id="ping" } + :tag("ping", {xmlns="urn:xmpp:ping"})); + return true, "Sent ping"; + else + return nil, "No such host"; + end +end + +def_env.dns = {}; +local adns = require"net.adns"; +local dns = require"net.dns"; + +function def_env.dns:lookup(name, typ, class) + local ret = "Query sent"; + local print = self.session.print; + local function handler(...) + ret = "Got response"; + print(...); + end + adns.lookup(handler, name, typ, class); + return true, ret; +end + +function def_env.dns:addnameserver(...) + dns.addnameserver(...) + return true +end + +function def_env.dns:setnameserver(...) + dns.setnameserver(...) + return true +end + +function def_env.dns:purge() + dns.purge() + return true +end + +function def_env.dns:cache() + return true, "Cache:\n"..tostring(dns.cache()) +end + ------------- function printbanner(session) - local option = config.get("*", "core", "console_banner"); -if option == nil or option == "full" or option == "graphic" then -session.print [[ - ____ \ / _ - | _ \ _ __ ___ ___ _-_ __| |_ _ + local option = module:get_option("console_banner"); + if option == nil or option == "full" or option == "graphic" then + session.print [[ + ____ \ / _ + | _ \ _ __ ___ ___ _-_ __| |_ _ | |_) | '__/ _ \/ __|/ _ \ / _` | | | | | __/| | | (_) \__ \ |_| | (_| | |_| | |_| |_| \___/|___/\___/ \__,_|\__, | - A study in simplicity |___/ + A study in simplicity |___/ ]] -end -if option == nil or option == "short" or option == "full" then -session.print("Welcome to the Prosody administration console. For a list of commands, type: help"); -session.print("You may find more help on using this console in our online documentation at "); -session.print("http://prosody.im/doc/console\n"); -end -if option and option ~= "short" and option ~= "full" and option ~= "graphic" then - if type(option) == "string" then - session.print(option) - elseif type(option) == "function" then - setfenv(option, redirect_output(_G, session)); - pcall(option, session); end -end + if option == nil or option == "short" or option == "full" then + session.print("Welcome to the Prosody administration console. For a list of commands, type: help"); + session.print("You may find more help on using this console in our online documentation at "); + session.print("http://prosody.im/doc/console\n"); + end + if option and option ~= "short" and option ~= "full" and option ~= "graphic" then + if type(option) == "string" then + session.print(option) + elseif type(option) == "function" then + module:log("warn", "Using functions as value for the console_banner option is no longer supported"); + end + end end -prosody.net_activate_ports("console", "console", {5582}, "tcp"); +module:provides("net", { + name = "console"; + listener = console_listener; + default_port = 5582; + private = true; +}); diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua index 77555bec..9327556c 100644 --- a/plugins/mod_announce.lua +++ b/plugins/mod_announce.lua @@ -1,13 +1,14 @@ -- 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 st, jid = require "util.stanza", require "util.jid"; +local hosts = prosody.hosts; local is_admin = require "core.usermanager".is_admin; function send_to_online(message, host) @@ -25,7 +26,7 @@ function send_to_online(message, host) for username in pairs(host_session.sessions) do c = c + 1; message.attr.to = username.."@"..hostname; - core_post_stanza(host_session, message); + module:send(message); end end end @@ -38,22 +39,22 @@ end 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 end - + if not is_admin(stanza.attr.from) then -- Not an admin? Not allowed! 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 message = st.clone(stanza); message.attr.type = "headline"; message.attr.from = host; - + local c = send_to_online(message, host); module:log("info", "Announcement sent to %d online users", c); return true; @@ -82,13 +83,13 @@ function announce_handler(self, data, state) 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"; + return { status = "executing", actions = {"next", "complete", default = "complete"}, form = announce_layout }, "executing"; end return true; @@ -96,5 +97,5 @@ 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); +module:provides("adhoc", announce_desc); diff --git a/plugins/mod_auth_anonymous.lua b/plugins/mod_auth_anonymous.lua index 9d0896e5..c877d532 100644 --- a/plugins/mod_auth_anonymous.lua +++ b/plugins/mod_auth_anonymous.lua @@ -6,63 +6,66 @@ -- 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"; +local hosts = prosody.hosts; -function new_default_provider(host) - local provider = { name = "anonymous" }; +-- define auth provider +local provider = {}; - function provider.test_password(username, password) - return nil, "Password based auth not supported."; - end +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.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.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.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.create_user(username, password) + return nil, "Account creation/modification not supported."; +end - function provider.get_sasl_handler() - local realm = module:get_option("sasl_realm") or module.host; - local anonymous_authentication_profile = { - anonymous = function(sasl, username, realm) - return true; -- for normal usage you should always return true here - end - }; - return new_sasl(realm, anonymous_authentication_profile); - 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; +function provider.users() + return next, hosts[host].sessions, nil; end +-- datamanager callback to disable writes 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; + +if not module:get_option_boolean("allow_anonymous_s2s", false) then + module:hook("route/remote", function (event) + return false; -- Block outgoing s2s from anonymous users + end, 300); +end + 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)); +module:provides("auth", provider); diff --git a/plugins/mod_auth_cyrus.lua b/plugins/mod_auth_cyrus.lua index ed3d5408..7668f8c4 100644 --- a/plugins/mod_auth_cyrus.lua +++ b/plugins/mod_auth_cyrus.lua @@ -14,6 +14,7 @@ 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; +local host_fqdn = module:get_option("cyrus_server_fqdn"); prosody.unlock_globals(); --FIXME: Figure out why this is needed and -- why cyrussasl isn't caught by the sandbox @@ -23,50 +24,61 @@ local new_sasl = function(realm) return cyrus_new( cyrus_service_realm or realm, cyrus_service_name or "xmpp", - cyrus_application_name or "prosody" + cyrus_application_name or "prosody", + host_fqdn ); 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."; +do -- diagnostic + local list; + for mechanism in pairs(new_sasl(module.host):mechanisms()) do + list = (not(list) and mechanism) or (list..", "..mechanism); end - - function provider.set_password(username, password) - return nil, "Passwords unavailable for Cyrus SASL."; + 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 provider.user_exists(username) - if require_provisioning then - return usermanager_user_exists(username, module.host); - end - return true; - end +local host = module.host; + +-- define auth provider +local provider = {}; +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.create_user(username, password) - return nil, "Account creation/modification not available with Cyrus SASL."; +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, 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 realm = module:get_option("sasl_realm") or module.host; - local handler = new_sasl(realm); - if require_provisioning then - function handler.require_provisioning(username) - return usermanager_user_exists(username, module.host); - end +function provider.get_sasl_handler() + local handler = new_sasl(host); + if require_provisioning then + function handler.require_provisioning(username) + return usermanager_user_exists(username, host); end - return handler; end - - return provider; + return handler; end -module:add_item("auth-provider", new_default_provider(module.host)); +module:provides("auth", provider); diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua index ec8da9ab..fb87bb9f 100644 --- a/plugins/mod_auth_internal_hashed.lua +++ b/plugins/mod_auth_internal_hashed.lua @@ -7,24 +7,15 @@ -- 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 log = module._log; +local host = module.host; + +local accounts = module:open_store("accounts"); local to_hex; do @@ -47,135 +38,113 @@ do 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); +-- define auth provider +local provider = {}; - 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 +function provider.test_password(username, password) + log("debug", "test password for user '%s'", username); + local credentials = accounts:get(username) or {}; - if provider.set_password(username, credentials.password) == nil then - return nil, "Auth failed. Could not set hashed password from plaintext."; - else - return true; - end + 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 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; + if provider.set_password(username, credentials.password) == nil then + return nil, "Auth failed. Could not set hashed password from plaintext."; else - return nil, "Auth failed. Invalid username, password, or password hash information."; + return true; 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."; + 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 - 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 + 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.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); +function provider.set_password(username, password) + log("debug", "set_password for username '%s'", username); + local account = accounts:get(username); + 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); - return datamanager.store(username, host, "accounts", {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = iteration_count}); + + account.stored_key = stored_key_hex + account.server_key = server_key_hex + + account.password = nil; + return accounts:set(username, account); + end + return nil, "Account not available."; +end + +function provider.user_exists(username) + local account = accounts:get(username); + if not account then + log("debug", "account not found for username '%s'", username); + return nil, "Auth failed. Invalid username"; + end + return true; +end + +function provider.users() + return accounts:users(); +end + +function provider.create_user(username, password) + if password == nil then + return accounts:set(username, {}); 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 accounts:set(username, {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = iteration_count}); +end + +function provider.delete_user(username) + return accounts:set(username, nil); +end - function provider.get_sasl_handler() - local realm = module:get_option("sasl_realm") or module.host; - 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"); +function provider.get_sasl_handler() + local testpass_authentication_profile = { + plain_test = function(sasl, username, password, realm) + return usermanager.test_password(username, realm, password), true; + end, + scram_sha_1 = function(sasl, username, realm) + local credentials = accounts:get(username); + if not credentials then return; end + if credentials.password then + usermanager.set_password(username, credentials.password, host); + credentials = accounts:get(username); 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(realm, testpass_authentication_profile); - end - - return provider; + + 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(host, testpass_authentication_profile); end -module:add_item("auth-provider", new_hashpass_provider(module.host)); +module:provides("auth", provider); diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua index 3721781b..db528432 100644 --- a/plugins/mod_auth_internal_plain.lua +++ b/plugins/mod_auth_internal_plain.lua @@ -6,84 +6,76 @@ -- 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; +local log = module._log; +local host = module.host; -function new_default_provider(host) - local provider = { name = "internal_plain" }; - log("debug", "initializing default authentication provider for host '%s'", host); +local accounts = module:open_store("accounts"); - 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 +-- define auth provider +local provider = {}; - 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.test_password(username, password) + log("debug", "test password for user '%s'", username); + local credentials = accounts:get(username) or {}; - 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 + 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'", username); + return (accounts:get(username) or {}).password; +end - function provider.create_user(username, password) - return datamanager.store(username, host, "accounts", {password = password}); +function provider.set_password(username, password) + log("debug", "set_password for username '%s'", username); + local account = accounts:get(username); + if account then + account.password = password; + return accounts:set(username, account); end + return nil, "Account not available."; +end - function provider.get_sasl_handler() - local realm = module:get_option("sasl_realm") or module.host; - 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(realm, getpass_authentication_profile); +function provider.user_exists(username) + local account = accounts:get(username); + if not account then + log("debug", "account not found for username '%s'", username); + return nil, "Auth failed. Invalid username"; end - - return provider; + return true; +end + +function provider.users() + return accounts:users(); +end + +function provider.create_user(username, password) + return accounts:set(username, {password = password}); +end + +function provider.delete_user(username) + return accounts:set(username, nil); +end + +function provider.get_sasl_handler() + local getpass_authentication_profile = { + plain = function(sasl, username, realm) + local password = usermanager.get_password(username, realm); + if not password then + return "", nil; + end + return password, true; + end + }; + return new_sasl(host, getpass_authentication_profile); end -module:add_item("auth-provider", new_default_provider(module.host)); +module:provides("auth", provider); diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua index 58254169..ca67db73 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -1,26 +1,27 @@ -- 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. -- -module.host = "*" -- Global module +module:set_global(); -- Global module local hosts = _G.hosts; -local lxp = require "lxp"; 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 = prosody.events.fire_event; -local core_process_stanza = core_process_stanza; +local core_process_stanza = prosody.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 initialize_filters = require "util.filters".initialize; +local math_min = math.min; +local xpcall, tostring, type = xpcall, tostring, type; +local traceback = debug.traceback; local xmlns_streams = "http://etherx.jabber.org/streams"; local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; @@ -29,36 +30,23 @@ local xmlns_bosh = "http://jabber.org/protocol/httpbind"; -- (hard-coded into a 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_HOLD = module:get_option_number("bosh_default_hold", 1); +local BOSH_DEFAULT_INACTIVITY = module:get_option_number("bosh_max_inactivity", 60); +local BOSH_DEFAULT_POLLING = module:get_option_number("bosh_max_polling", 5); +local BOSH_DEFAULT_REQUESTS = module:get_option_number("bosh_max_requests", 2); +local bosh_max_wait = module:get_option_number("bosh_max_wait", 120); local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure"); +local cross_domain = module:get_option("cross_domain_bosh", false); -local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" }; - -local cross_domain = module:get_option("cross_domain_bosh"); -if cross_domain then - default_headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"; - default_headers["Access-Control-Allow-Headers"] = "Content-Type"; - default_headers["Access-Control-Max-Age"] = "7200"; - - if cross_domain == true then - default_headers["Access-Control-Allow-Origin"] = "*"; - elseif type(cross_domain) == "table" then - cross_domain = table.concat(cross_domain, ", "); - end - if type(cross_domain) == "string" then - default_headers["Access-Control-Allow-Origin"] = cross_domain; - end -end +if cross_domain == true then cross_domain = "*"; end +if type(cross_domain) == "table" then cross_domain = table.concat(cross_domain, ", "); 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"]; + local ip = request.conn: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 @@ -73,61 +61,91 @@ end local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; local os_time = os.time; -local sessions = {}; -local inactive_sessions = {}; -- Sessions which have no open requests +-- All sessions, and sessions that have no requests open +local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions"); -- Used to respond to idle sessions (those with waiting requests) -local waiting_requests = {}; +local waiting_requests = module:shared("waiting_requests"); function on_destroy_request(request) + log("debug", "Request destroyed: %s", tostring(request)); waiting_requests[request] = nil; - local session = sessions[request.sid]; + local session = sessions[request.context.sid]; if session then local requests = session.requests; - for i,r in ipairs(requests) do + for i, r in ipairs(requests) do if r == request then t_remove(requests, i); break; end end - + -- If this session now has no requests open, mark it as inactive - if #requests == 0 and session.bosh_max_inactive and not inactive_sessions[session] then - inactive_sessions[session] = os_time(); - (session.log or log)("debug", "BOSH session marked as inactive at %d", inactive_sessions[session]); + local max_inactive = session.bosh_max_inactive; + if max_inactive and #requests == 0 then + inactive_sessions[session] = os_time() + max_inactive; + (session.log or log)("debug", "BOSH session marked as inactive (for %ds)", max_inactive); end end end -function handle_request(method, body, request) - if (not body) or request.method ~= "POST" then - if request.method == "OPTIONS" then - 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 +local function set_cross_domain_headers(response) + local headers = response.headers; + headers.access_control_allow_methods = "GET, POST, OPTIONS"; + headers.access_control_allow_headers = "Content-Type"; + headers.access_control_max_age = "7200"; + headers.access_control_allow_origin = cross_domain; + return response; +end + +function handle_OPTIONS(event) + if cross_domain and event.request.headers.origin then + set_cross_domain_headers(event.response); end - if not method then - log("debug", "Request %s suffered error %s", tostring(request.id), body); - return; + return ""; +end + +function handle_POST(event) + log("debug", "Handling new request %s: %s\n----------", tostring(event.request), tostring(event.request.body)); + + local request, response = event.request, event.response; + response.on_destroy = on_destroy_request; + local body = request.body; + + local context = { request = request, response = response, notopen = true }; + local stream = new_xmpp_stream(context, stream_callbacks); + response.context = context; + + local headers = response.headers; + headers.content_type = "text/xml; charset=utf-8"; + + if cross_domain and event.request.headers.origin then + set_cross_domain_headers(response); end - --log("debug", "Handling new request %s: %s\n----------", request.id, tostring(body)); - request.notopen = true; - request.log = log; - request.on_destroy = on_destroy_request; - - 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]; + -- In particular, the streamopened() stream callback is where + -- much of the session logic happens, because it's where we first + -- get to see the 'sid' of this request. + if not stream:feed(body) then + module:log("warn", "Error parsing BOSH payload") + return 400; + end + + -- Stanzas (if any) in the request have now been processed, and + -- we take care of the high-level BOSH logic here, including + -- giving a response or putting the request "on hold". + local session = sessions[context.sid]; if session then + -- Session was marked as inactive, since we have + -- a request open now, unmark it + if inactive_sessions[session] and #session.requests > 0 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); + log("debug", "Session %s has %d out of %d requests open", context.sid, #r, session.bosh_hold); + log("debug", "and there are %d things in the send_buffer:", #session.send_buffer); if #r > session.bosh_hold then -- We are holding too many requests, send what's in the buffer, log("debug", "We are holding too many requests, so..."); @@ -146,23 +164,25 @@ function handle_request(method, body, request) session.send_buffer = {}; session.send(resp); end - - if not request.destroyed then + + if not response.finished then -- We're keeping this request open, to respond later log("debug", "Have nothing to say, so leaving request unanswered for now"); if session.bosh_wait then - 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; + waiting_requests[response] = os_time() + session.bosh_wait; end end - - return true; -- Inform httpserver we shall reply later + + if session.bosh_terminate then + session.log("debug", "Closing session with %d requests open", #session.requests); + session:close(); + return nil; + else + return true; -- Inform http server we shall reply later + end end + module:log("warn", "Unable to associate request with a session (incomplete request?)"); + return 400; end @@ -172,10 +192,10 @@ 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"); - + local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", - ["xmlns:streams"] = xmlns_streams }); - + ["xmlns:stream"] = xmlns_streams }); + if reason then close_reply.attr.condition = "remote-stream-error"; @@ -199,115 +219,105 @@ local function bosh_close_stream(session, reason) 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 + local response_body = tostring(close_reply); for _, held_request in ipairs(session.requests) do - held_request:send(session_close_response); - held_request:destroy(); + held_request:send(response_body); end - sessions[session.sid] = nil; + sessions[session.sid] = nil; + inactive_sessions[session] = nil; sm_destroy_session(session); end -function stream_callbacks.streamopened(request, attr) - log("debug", "BOSH body open (sid: %s)", attr.sid); - local sid = attr.sid +-- Handle the <body> tag in the request payload. +function stream_callbacks.streamopened(context, attr) + local request, response = context.request, context.response; + local sid = attr.sid; + log("debug", "BOSH body open (sid: %s)", sid or "<none>"); if not sid then -- New session request - request.notopen = nil; -- Signals that we accept this opening tag - + context.notopen = nil; -- Signals that we accept this opening tag + -- TODO: Sanity checks here (rid, to, known host, etc.) if not hosts[attr.to] then -- Unknown host log("debug", "BOSH client tried to connect to unknown host: %s", tostring(attr.to)); local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", - ["xmlns:streams"] = xmlns_streams, condition = "host-unknown" }); - request:send(tostring(close_reply)); + ["xmlns:stream"] = xmlns_streams, condition = "host-unknown" }); + response:send(tostring(close_reply)); return; end - + -- New session sid = new_uuid(); local session = { - type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid), host = attr.to, - bosh_version = attr.ver, bosh_wait = attr.wait, streamid = sid, + type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid)-1, host = attr.to, + bosh_version = attr.ver, bosh_wait = math_min(attr.wait, bosh_max_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, + close = bosh_close_stream, dispatch_stanza = core_process_stanza, notopen = true, log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure, ip = get_ip_from_request(request); }; sessions[sid] = session; - + + local filter = initialize_filters(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 } + + -- Send creation response + local creating_session = true; + + local r = session.requests; 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 + s = filter("stanzas/out", s); --log("debug", "Sending BOSH data: %s", tostring(s)); + t_insert(session.send_buffer, tostring(s)); + local oldest_request = r[1]; - if oldest_request then + if oldest_request and not session.bosh_processing then log("debug", "We have an open request, so sending on that"); - response.body = t_concat{"<body xmlns='http://jabber.org/protocol/httpbind' sid='", sid, "' xmlns:stream = 'http://etherx.jabber.org/streams'>", tostring(s), "</body>" }; - oldest_request:send(response); - --log("debug", "Sent"); - if oldest_request.stayopen then - if #r>1 then - -- Move front request to back - t_insert(r, oldest_request); - t_remove(r, 1); - end - else - log("debug", "Destroying the request now..."); - oldest_request:destroy(); + local body_attr = { xmlns = "http://jabber.org/protocol/httpbind", + ["xmlns:stream"] = "http://etherx.jabber.org/streams"; + type = session.bosh_terminate and "terminate" or nil; + sid = sid; + }; + if creating_session then + creating_session = nil; + body_attr.inactivity = tostring(BOSH_DEFAULT_INACTIVITY); + body_attr.polling = tostring(BOSH_DEFAULT_POLLING); + body_attr.requests = tostring(BOSH_DEFAULT_REQUESTS); + body_attr.wait = tostring(session.bosh_wait); + body_attr.hold = tostring(session.bosh_hold); + body_attr.authid = sid; + body_attr.secure = "true"; + body_attr.ver = '1.6'; + body_attr.from = session.host; + body_attr["xmlns:xmpp"] = "urn:xmpp:xbosh"; + body_attr["xmpp:version"] = "1.0"; end - elseif s ~= "" then - log("debug", "Saved to send buffer because there are %d open requests", #r); - -- Hmm, no requests are open :( - t_insert(session.send_buffer, tostring(s)); - log("debug", "There are now %d things in the send_buffer", #session.send_buffer); + oldest_request:send(st.stanza("body", body_attr):top_tag()..t_concat(session.send_buffer).."</body>"); + session.send_buffer = {}; end + return true; end - - -- Send creation response - - local features = st.stanza("stream:features"); - hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); - fire_event("stream-features", session, features); - --xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh' - local response = st.stanza("body", { xmlns = xmlns_bosh, - 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; - return; end - + local session = sessions[sid]; if not session then -- Unknown sid log("info", "Client tried to use sid '%s' which we don't know about", sid); - request:send{ headers = default_headers, body = tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" })) }; - request.notopen = nil; + response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" }))); + context.notopen = nil; return; end - + if session.rid then local rid = tonumber(attr.rid); local diff = rid - session.rid; @@ -315,56 +325,71 @@ function stream_callbacks.streamopened(request, attr) session.log("warn", "rid too large (means a request was lost). Last rid: %d New rid: %s", session.rid, attr.rid); elseif diff <= 0 then -- Repeated, ignore - session.log("debug", "rid repeated (on request %s), ignoring: %s (diff %d)", request.id, session.rid, diff); - request.notopen = nil; - request.ignore = true; - request.sid = sid; - t_insert(session.requests, request); + session.log("debug", "rid repeated, ignoring: %s (diff %d)", session.rid, diff); + context.notopen = nil; + context.ignore = true; + context.sid = sid; + t_insert(session.requests, response); return; end session.rid = rid; end - + if attr.type == "terminate" then - -- Client wants to end this session - session:close(); - request.notopen = nil; - return; + -- Client wants to end this session, which we'll do + -- after processing any stanzas in this request + session.bosh_terminate = true; end - + + context.notopen = nil; -- Signals that we accept this opening tag + t_insert(session.requests, response); + context.sid = sid; + session.bosh_processing = true; -- Used to suppress replies until processing of this request is done + if session.notopen then local features = st.stanza("stream:features"); hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); - fire_event("stream-features", session, features); session.send(features); session.notopen = nil; end - - request.notopen = nil; -- Signals that we accept this opening tag - t_insert(session.requests, request); - request.sid = sid; end -function stream_callbacks.handlestanza(request, stanza) - if request.ignore then return; end +local function handleerr(err) log("error", "Traceback[bosh]: %s", traceback(tostring(err), 2)); end +function stream_callbacks.handlestanza(context, stanza) + if context.ignore then return; end log("debug", "BOSH stanza received: %s\n", stanza:top_tag()); - local session = sessions[request.sid]; + local session = sessions[context.sid]; if session then if stanza.attr.xmlns == xmlns_bosh then stanza.attr.xmlns = nil; end - core_process_stanza(session, stanza); + stanza = session.filter("stanzas/in", stanza); + if stanza then + return xpcall(function () return core_process_stanza(session, stanza) end, handleerr); + end + end +end + +function stream_callbacks.streamclosed(context) + local session = sessions[context.sid]; + if session then + session.bosh_processing = false; + if #session.send_buffer > 0 then + session.send(""); + end end end -function stream_callbacks.error(request, error) +function stream_callbacks.error(context, error) log("debug", "Error parsing BOSH request payload; %s", error); - if not request.sid then - request:send({ headers = default_headers, status = "400 Bad Request" }); + if not context.sid then + local response = context.response; + response.status_code = 400; + response:send(); return; end - - local session = sessions[request.sid]; + + local session = sessions[context.sid]; if error == "stream-error" then -- Remote stream error, we close normally session:close(); else @@ -372,35 +397,31 @@ function stream_callbacks.error(request, error) end end -local dead_sessions = {}; +local dead_sessions = module:shared("dead_sessions"); function on_timer() -- log("debug", "Checking for requests soon to timeout..."); -- Identify requests timing out within the next few seconds local now = os_time() + 3; - for request in pairs(waiting_requests) do - if request.reply_before <= now then - log("debug", "%s was soon to timeout, sending empty response", request.id); + for request, reply_before in pairs(waiting_requests) do + if reply_before <= now then + log("debug", "%s was soon to timeout (at %d, now %d), sending empty response", tostring(request), reply_before, now); -- Send empty response to let the -- client know we're still here if request.conn then - sessions[request.sid].send(""); + sessions[request.context.sid].send(""); end end end - + now = now - 3; local n_dead_sessions = 0; - for session, inactive_since in pairs(inactive_sessions) do - if session.bosh_max_inactive then - if now - inactive_since > session.bosh_max_inactive then - (session.log or log)("debug", "BOSH client inactive too long, destroying session at %d", now); - sessions[session.sid] = nil; - inactive_sessions[session] = nil; - n_dead_sessions = n_dead_sessions + 1; - dead_sessions[n_dead_sessions] = session; - end - else + for session, close_after in pairs(inactive_sessions) do + if close_after < now then + (session.log or log)("debug", "BOSH client inactive too long, destroying session at %d", now); + sessions[session.sid] = nil; inactive_sessions[session] = nil; + n_dead_sessions = n_dead_sessions + 1; + dead_sessions[n_dead_sessions] = session; end end @@ -411,15 +432,30 @@ function on_timer() end return 1; end +module:add_timer(1, on_timer); -local function setup() - local ports = module:get_option("bosh_ports") or { 5280 }; - httpserver.new_from_config(ports, handle_request, { base = "http-bind" }); - timer.add_task(1, on_timer); -end -if prosody.start_time then -- already started - setup(); -else - prosody.events.add_handler("server-started", setup); +local GET_response = { + headers = { + content_type = "text/html"; + }; + body = [[<html><body> + <p>It works! Now point your BOSH client to this URL to connect to Prosody.</p> + <p>For more information see <a href="http://prosody.im/doc/setting_up_bosh">Prosody: Setting up BOSH</a>.</p> + </body></html>]]; +}; + +function module.add_host(module) + module:depends("http"); + module:provides("http", { + default_path = "/http-bind"; + route = { + ["GET"] = GET_response; + ["GET /"] = GET_response; + ["OPTIONS"] = handle_OPTIONS; + ["OPTIONS /"] = handle_OPTIONS; + ["POST"] = handle_POST; + ["POST /"] = handle_POST; + }; + }); end diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua new file mode 100644 index 00000000..1fb8dcf5 --- /dev/null +++ b/plugins/mod_c2s.lua @@ -0,0 +1,332 @@ +-- 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. +-- + +module:set_global(); + +local add_task = require "util.timer".add_task; +local new_xmpp_stream = require "util.xmppstream".new; +local nameprep = require "util.encodings".stringprep.nameprep; +local sessionmanager = require "core.sessionmanager"; +local st = require "util.stanza"; +local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session; +local uuid_generate = require "util.uuid".generate; +local runner = require "util.async".runner; + +local xpcall, tostring, type = xpcall, tostring, type; +local t_insert, t_remove = table.insert, table.remove; + +local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; + +local log = module._log; + +local c2s_timeout = module:get_option_number("c2s_timeout"); +local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5); +local opt_keepalives = module:get_option_boolean("c2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true)); + +local sessions = module:shared("sessions"); +local core_process_stanza = prosody.core_process_stanza; +local hosts = prosody.hosts; + +local stream_callbacks = { default_ns = "jabber:client" }; +local listener = {}; +local runner_callbacks = {}; + +--- Stream events handlers +local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; +local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; + +function stream_callbacks.streamopened(session, attr) + local send = session.send; + session.host = nameprep(attr.to); + if not session.host then + session:close{ condition = "improper-addressing", + text = "A valid 'to' attribute is required on stream headers" }; + return; + end + session.version = tonumber(attr.version) or 0; + session.streamid = uuid_generate(); + (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host); + + if not hosts[session.host] or not hosts[session.host].modules.c2s then + -- We don't serve this host... + session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)}; + return; + end + + send("<?xml version='1.0'?>"..st.stanza("stream:stream", { + xmlns = 'jabber:client', ["xmlns:stream"] = 'http://etherx.jabber.org/streams'; + id = session.streamid, from = session.host, version = '1.0', ["xml:lang"] = 'en' }):top_tag()); + + (session.log or log)("debug", "Sent reply <stream:stream> to client"); + session.notopen = nil; + + -- If session.secure is *false* (not nil) then it means we /were/ encrypting + -- since we now have a new stream header, session is secured + if session.secure == false then + session.secure = true; + + local sock = session.conn:socket(); + if sock.info then + local info = sock:info(); + (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); + session.compressed = info.compression; + else + (session.log or log)("info", "Stream encrypted"); + session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg + end + end + + local features = st.stanza("stream:features"); + hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); + send(features); +end + +function stream_callbacks.streamclosed(session) + session.log("debug", "Received </stream:stream>"); + session:close(false); +end + +function stream_callbacks.error(session, error, data) + if error == "no-stream" then + session.log("debug", "Invalid opening stream header"); + session:close("invalid-namespace"); + elseif error == "parse-error" then + (session.log or log)("debug", "Client XML parse error: %s", tostring(data)); + session:close("not-well-formed"); + elseif error == "stream-error" then + local condition, text = "undefined-condition"; + for child in data:children() do + if child.attr.xmlns == xmlns_xmpp_streams then + if child.name ~= "text" then + condition = child.name; + else + text = child:get_text(); + end + if condition ~= "undefined-condition" and text then + break; + end + end + end + text = condition .. (text and (" ("..text..")") or ""); + session.log("info", "Session closed by remote with error: %s", text); + session:close(nil, text); + end +end + +function stream_callbacks.handlestanza(session, stanza) + stanza = session.filter("stanzas/in", stanza); + session.thread:run(stanza); +end + +--- Session methods +local function session_close(session, reason) + local log = session.log or log; + if session.conn then + if session.notopen then + session.send("<?xml version='1.0'?>"); + session.send(st.stanza("stream:stream", default_stream_attr):top_tag()); + end + if reason then -- nil == no err, initiated by us, false == initiated by client + local stream_error = st.stanza("stream:error"); + if type(reason) == "string" then -- assume stream error + stream_error:tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' }); + elseif type(reason) == "table" then + if reason.condition then + stream_error:tag(reason.condition, stream_xmlns_attr):up(); + if reason.text then + stream_error:tag("text", stream_xmlns_attr):text(reason.text):up(); + end + if reason.extra then + stream_error:add_child(reason.extra); + end + elseif reason.name then -- a stanza + stream_error = reason; + end + end + stream_error = tostring(stream_error); + log("debug", "Disconnecting client, <stream:error> is: %s", stream_error); + session.send(stream_error); + end + + session.send("</stream:stream>"); + function session.send() return false; end + + local reason = (reason and (reason.name or reason.text or reason.condition)) or reason; + session.log("debug", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed"); + + -- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote + local conn = session.conn; + if reason == nil and not session.notopen and session.type == "c2s" then + -- Grace time to process data from authenticated cleanly-closed stream + add_task(stream_close_timeout, function () + if not session.destroyed then + session.log("warn", "Failed to receive a stream close response, closing connection anyway..."); + sm_destroy_session(session, reason); + conn:close(); + end + end); + else + sm_destroy_session(session, reason); + conn:close(); + end + end +end + +module:hook_global("user-deleted", function(event) + local username, host = event.username, event.host; + local user = hosts[host].sessions[username]; + if user and user.sessions then + for jid, session in pairs(user.sessions) do + session:close{ condition = "not-authorized", text = "Account deleted" }; + end + end +end, 200); + +function runner_callbacks:ready() + self.data.conn:resume(); +end + +function runner_callbacks:waiting() + self.data.conn:pause(); +end + +function runner_callbacks:error(err) + (self.data.log or log)("error", "Traceback[c2s]: %s", err); +end + +--- Port listener +function listener.onconnect(conn) + local session = sm_new_session(conn); + sessions[conn] = session; + + session.log("info", "Client connected"); + + -- Client is using legacy SSL (otherwise mod_tls sets this flag) + if conn:ssl() then + session.secure = true; + + -- Check if TLS compression is used + local sock = conn:socket(); + if sock.info then + session.compressed = sock:info"compression"; + elseif sock.compression then + session.compressed = sock:compression(); --COMPAT mw/luasec-hg + end + end + + if opt_keepalives then + conn:setoption("keepalive", opt_keepalives); + end + + session.close = session_close; + + local stream = new_xmpp_stream(session, stream_callbacks); + session.stream = stream; + session.notopen = true; + + function session.reset_stream() + session.notopen = true; + session.stream:reset(); + end + + session.thread = runner(function (stanza) + core_process_stanza(session, stanza); + end, runner_callbacks, session); + + local filter = session.filter; + function session.data(data) + -- Parse the data, which will store stanzas in session.pending_stanzas + if data then + data = filter("bytes/in", data); + if data then + local ok, err = stream:feed(data); + if not ok then + log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_")); + session:close("not-well-formed"); + end + end + end + end + + if c2s_timeout then + add_task(c2s_timeout, function () + if session.type == "c2s_unauthed" then + session:close("connection-timeout"); + end + end); + end + + session.dispatch_stanza = stream_callbacks.handlestanza; +end + +function listener.onincoming(conn, data) + local session = sessions[conn]; + if session then + session.data(data); + end +end + +function listener.ondisconnect(conn, err) + local session = sessions[conn]; + if session then + (session.log or log)("info", "Client disconnected: %s", err or "connection closed"); + sm_destroy_session(session, err); + sessions[conn] = nil; + end +end + +function listener.onreadtimeout(conn) + local session = sessions[conn]; + if session then + return (hosts[session.host] or prosody).events.fire_event("c2s-read-timeout", { session = session }); + end +end + +local function keepalive(event) + return event.session.send(' '); +end + +function listener.associate_session(conn, session) + sessions[conn] = session; +end + +function module.add_host(module) + module:hook("c2s-read-timeout", keepalive, -1); +end + +module:hook("c2s-read-timeout", keepalive, -1); + +module:hook("server-stopping", function(event) + local reason = event.reason; + for _, session in pairs(sessions) do + session:close{ condition = "system-shutdown", text = reason }; + end +end, 1000); + + + +module:provides("net", { + name = "c2s"; + listener = listener; + default_port = 5222; + encryption = "starttls"; + multiplex = { + pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:client%1.*>"; + }; +}); + +module:provides("net", { + name = "legacy_ssl"; + listener = listener; + encryption = "ssl"; + multiplex = { + pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:client%1.*>"; + }; +}); + + diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua index fda271dd..3eaacb8e 100644 --- a/plugins/mod_component.lua +++ b/plugins/mod_component.lua @@ -1,100 +1,324 @@ -- 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. -- -if module:get_host_type() ~= "component" then - error("Don't load mod_component manually, it should be for a component, please see http://prosody.im/doc/components", 0); -end - -local hosts = _G.hosts; +module:set_global(); local t_concat = table.concat; +local xpcall, tostring, type = xpcall, tostring, type; +local traceback = debug.traceback; +local logger = require "util.logger"; local sha1 = require "util.hashes".sha1; local st = require "util.stanza"; +local jid_split = require "util.jid".split; +local new_xmpp_stream = require "util.xmppstream".new; +local uuid_gen = require "util.uuid".generate; + +local core_process_stanza = prosody.core_process_stanza; +local hosts = prosody.hosts; + local log = module._log; -local main_session, send; +local sessions = module:shared("sessions"); + +function module.add_host(module) + if module:get_host_type() ~= "component" then + error("Don't load mod_component manually, it should be for a component, please see http://prosody.im/doc/components", 0); + end + + local env = module.environment; + env.connected = false; + + local send; -local function on_destroy(session, err) - if main_session == session then - main_session = nil; + local function on_destroy(session, err) + env.connected = false; send = nil; session.on_destroy = nil; end + + -- Handle authentication attempts by component + local function handle_component_auth(event) + local session, stanza = event.origin, event.stanza; + + if session.type ~= "component_unauthed" then return; end + + if (not session.host) or #stanza.tags > 0 then + (session.log or log)("warn", "Invalid component handshake for host: %s", session.host); + session:close("not-authorized"); + return true; + end + + 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.host); + session:close("not-authorized"); + 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 + module:log("info", "Component authentication failed for %s", session.host); + session:close{ condition = "not-authorized", text = "Given token does not match calculated token" }; + return true; + end + + if env.connected then + module:log("error", "Second component attempted to connect, denying connection"); + session:close{ condition = "conflict", text = "Component already connected" }; + return true; + end + + env.connected = true; + send = session.send; + session.on_destroy = on_destroy; + session.component_validate_from = module:get_option_boolean("validate_from_addresses", true); + session.type = "component"; + module:log("info", "External component successfully authenticated"); + session.send(st.stanza("handshake")); + + return true; + end + module:hook("stanza/jabber:component:accept:handshake", handle_component_auth); + + -- Handle stanzas addressed to this component + local function handle_stanza(event) + local stanza = event.stanza; + if send then + stanza.attr.xmlns = nil; + send(stanza); + else + if stanza.name == "iq" and stanza.attr.type == "get" and stanza.attr.to == module.host then + local query = stanza.tags[1]; + local node = query.attr.node; + if query.name == "query" and query.attr.xmlns == "http://jabber.org/protocol/disco#info" and (not node or node == "") then + local name = module:get_option_string("name"); + if name then + event.origin.send(st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info" }) + :tag("identity", { category = "component", type = "generic", name = module:get_option_string("name", "Prosody") })) + return true; + end + end + end + module:log("warn", "Component not connected, 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); +end + +--- Network and stream part --- + +local xmlns_component = 'jabber:component:accept'; + +local listener = {}; + +--- Callbacks/data for xmppstream to handle streams for us --- + +local stream_callbacks = { default_ns = xmlns_component }; + +local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; + +function stream_callbacks.error(session, error, data, data2) + if session.destroyed then return; end + module:log("warn", "Error processing component stream: %s", tostring(error)); + if error == "no-stream" then + session:close("invalid-namespace"); + elseif error == "parse-error" then + session.log("warn", "External component %s XML parse error: %s", tostring(session.host), tostring(data)); + session:close("not-well-formed"); + elseif error == "stream-error" then + local condition, text = "undefined-condition"; + for child in data:children() do + if child.attr.xmlns == xmlns_xmpp_streams then + if child.name ~= "text" then + condition = child.name; + else + text = child:get_text(); + end + if condition ~= "undefined-condition" and text then + break; + end + end + end + text = condition .. (text and (" ("..text..")") or ""); + session.log("info", "Session closed by remote with error: %s", text); + session:close(nil, text); + end +end + +function stream_callbacks.streamopened(session, attr) + if not hosts[attr.to] or not hosts[attr.to].modules.component then + session:close{ condition = "host-unknown", text = tostring(attr.to).." does not match any configured external components" }; + return; + end + session.host = attr.to; + session.streamid = uuid_gen(); + session.notopen = nil; + -- Return stream header + session.send("<?xml version='1.0'?>"); + session.send(st.stanza("stream:stream", { xmlns=xmlns_component, + ["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.host }):top_tag()); +end + +function stream_callbacks.streamclosed(session) + session.log("debug", "Received </stream:stream>"); + session:close(); 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")); +local function handleerr(err) log("error", "Traceback[component]: %s", traceback(tostring(err), 2)); end +function stream_callbacks.handlestanza(session, stanza) + -- Namespaces are icky. + if not stanza.attr.xmlns and stanza.name == "handshake" then + stanza.attr.xmlns = xmlns_component; + end + if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then + local from = stanza.attr.from; + if from then + if session.component_validate_from then + local _, domain = jid_split(stanza.attr.from); + if domain ~= session.host then + -- Return error + session.log("warn", "Component sent stanza with missing or invalid 'from' address"); + session:close{ + condition = "invalid-from"; + text = "Component tried to send from address <"..tostring(from) + .."> which is not in domain <"..tostring(session.host)..">"; + }; + return; + end + end + else + stanza.attr.from = session.host; -- COMPAT: Strictly we shouldn't allow this + end + if not stanza.attr.to then + session.log("warn", "Rejecting stanza with no 'to' address"); + session.send(st.error_reply(stanza, "modify", "bad-request", "Components MUST specify a 'to' address on stanzas")); + return; end end - return true; + + if stanza then + return xpcall(function () return core_process_stanza(session, stanza) end, handleerr); + end 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(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", "Invalid component handshake for host: %s", session.host); - session:close("not-authorized"); - return true; +--- Closing a component connection +local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; +local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; +local function session_close(session, reason) + if session.destroyed then return; end + if session.conn then + if session.notopen then + session.send("<?xml version='1.0'?>"); + session.send(st.stanza("stream:stream", default_stream_attr):top_tag()); + end + if reason then + if type(reason) == "string" then -- assume stream error + module:log("info", "Disconnecting component, <stream:error> is: %s", reason); + session.send(st.stanza("stream:error"):tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' })); + elseif type(reason) == "table" then + if reason.condition then + local stanza = st.stanza("stream:error"):tag(reason.condition, stream_xmlns_attr):up(); + if reason.text then + stanza:tag("text", stream_xmlns_attr):text(reason.text):up(); + end + if reason.extra then + stanza:add_child(reason.extra); + end + module:log("info", "Disconnecting component, <stream:error> is: %s", tostring(stanza)); + session.send(stanza); + elseif reason.name then -- a stanza + module:log("info", "Disconnecting component, <stream:error> is: %s", tostring(reason)); + session.send(reason); + end + end + end + session.send("</stream:stream>"); + session.conn:close(); + listener.ondisconnect(session.conn, "stream error"); end - - 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.host); - session:close("not-authorized"); - return true; +end + +--- Component connlistener + +function listener.onconnect(conn) + local _send = conn.write; + local session = { type = "component_unauthed", conn = conn, send = function (data) return _send(conn, tostring(data)); end }; + + -- Logging functions -- + local conn_name = "jcp"..tostring(session):match("[a-f0-9]+$"); + session.log = logger.init(conn_name); + session.close = session_close; + + session.log("info", "Incoming Jabber component connection"); + + local stream = new_xmpp_stream(session, stream_callbacks); + session.stream = stream; + + session.notopen = true; + + function session.reset_stream() + session.notopen = true; + session.stream:reset(); 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 authentication failed for %s", session.host); - session:close{ condition = "not-authorized", text = "Given token does not match calculated token" }; - return true; + + function session.data(conn, data) + local ok, err = stream:feed(data); + if ok then return; end + module:log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_")); + session:close("not-well-formed"); end - - -- If component not already created for this host, create one now - 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" }; + + session.dispatch_stanza = stream_callbacks.handlestanza; + + sessions[conn] = session; +end +function listener.onincoming(conn, data) + local session = sessions[conn]; + session.data(conn, data); +end +function listener.ondisconnect(conn, err) + local session = sessions[conn]; + if session then + (session.log or log)("info", "component disconnected: %s (%s)", tostring(session.host), tostring(err)); + if session.on_destroy then session:on_destroy(err); end + sessions[conn] = nil; + for k in pairs(session) do + if k ~= "log" and k ~= "close" then + session[k] = nil; + end + end + session.destroyed = true; + session = nil; end - - return true; end -module:hook("stanza/jabber:component:accept:handshake", handle_component_auth); +module:provides("net", { + name = "component"; + private = true; + listener = listener; + default_port = 5347; + multiplex = { + pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:component:accept%1.*>"; + }; +}); diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua index 82403016..f44e8a6d 100644 --- a/plugins/mod_compression.lua +++ b/plugins/mod_compression.lua @@ -1,6 +1,6 @@ -- Prosody IM --- Copyright (C) 2009 Tobias Markmann --- +-- Copyright (C) 2009-2012 Tobias Markmann +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- @@ -16,12 +16,8 @@ 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 -if compression_level == nil then compression_level = 9 end; - +local compression_level = module:get_option_number("compression_level", 7); -compression_level = tonumber(compression_level); if not compression_level or compression_level < 1 or compression_level > 9 then module:log("warn", "Invalid compression level in config: %s", tostring(compression_level)); module:log("warn", "Module loading aborted. Compression won't be available."); @@ -30,7 +26,7 @@ end module:hook("stream-features", function(event) local origin, features = event.origin, event.features; - if not origin.compressed then + if not origin.compressed and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then -- FIXME only advertise compression support when TLS layer has no compression enabled features:add_child(compression_stream_feature); end @@ -39,7 +35,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 and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then features:add_child(compression_stream_feature); end end); @@ -47,7 +43,7 @@ end); -- Hook to activate compression if remote server supports it. module:hook_stanza(xmlns_stream, "features", function (session, stanza) - if not session.compressed then + if not session.compressed and (session.type == "c2s" or session.type == "s2sin" or session.type == "s2sout") then -- does remote server support compression? local comp_st = stanza:child_with_name("compression"); if comp_st then @@ -107,7 +103,7 @@ local function setup_compression(session, deflate_stream) return; end return compressed; - end); + end); end -- setup decompression for a stream @@ -129,26 +125,23 @@ end 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:open_stream(session.from_host, session.to_host); session.compressed = true; return true; end @@ -165,29 +158,29 @@ module:hook("stanza/http://jabber.org/protocol/compress:compress", function(even 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 true; end - + local inflate_stream = get_inflate_stream(session); 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); - + session.compressed = true; elseif method then session.log("debug", "%s compression selected, but we don't support it.", tostring(method)); diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua index a8923e27..8d2bbd8f 100644 --- a/plugins/mod_dialback.lua +++ b/plugins/mod_dialback.lua @@ -1,39 +1,54 @@ -- 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 hosts = _G.hosts; -local send_s2s = require "core.s2smanager".send_to_host; -local s2s_make_authenticated = require "core.s2smanager".make_authenticated; -local s2s_initiate_dialback = require "core.s2smanager".initiate_dialback; -local s2s_verify_dialback = require "core.s2smanager".verify_dialback; -local s2s_destroy_session = require "core.s2smanager".destroy_session; local log = module._log; local st = require "util.stanza"; +local sha256_hash = require "util.hashes".sha256; +local nameprep = require "util.encodings".stringprep.nameprep; local xmlns_stream = "http://etherx.jabber.org/streams"; -local xmlns_dialback = "jabber:server:dialback"; local dialback_requests = setmetatable({}, { __mode = 'v' }); +function generate_dialback(id, to, from) + return sha256_hash(id..to..from..hosts[from].dialback_secret, true); +end + +function initiate_dialback(session) + -- generate dialback key + session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host); + session.sends2s(st.stanza("db:result", { from = session.from_host, to = session.to_host }):text(session.dialback_key)); + session.log("debug", "sent dialback key on outgoing s2s stream"); +end + +function verify_dialback(id, to, from, key) + return key == generate_dialback(id, to, from); +end + 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; + if attr.type then + module:log("warn", "Ignoring incoming session from %s claiming a dialback key for %s is %s", + origin.from_host or "(unknown)", attr.from or "(unknown)", attr.type); + return true; + end -- COMPAT: Grr, ejabberd breaks this one too?? it is black and white in XEP-220 example 34 --if attr.from ~= origin.to_host then error("invalid-from"); end local type; - if s2s_verify_dialback(attr.id, attr.from, attr.to, stanza[1]) then + if verify_dialback(attr.id, attr.from, attr.to, stanza[1]) then type = "valid" else type = "invalid" @@ -47,62 +62,68 @@ end); 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; - origin.hosts[attr.from] = { dialback_key = stanza[1] }; - - if not hosts[attr.to] then + local to, from = nameprep(attr.to), nameprep(attr.from); + + if not hosts[to] then -- 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.log("warn", "%s tried to connect to %s, which we don't serve", from, to); origin:close("host-unknown"); return true; + elseif not from then + origin:close("improper-addressing"); end - - dialback_requests[attr.from] = origin; - + + origin.hosts[from] = { dialback_key = stanza[1] }; + + dialback_requests[from.."/"..origin.streamid] = origin; + + -- COMPAT: ejabberd, gmail and perhaps others do not always set 'to' and 'from' + -- on streams. We fill in the session's to/from here instead. if not origin.from_host then - -- Just used for friendlier logging - origin.from_host = attr.from; + origin.from_host = from; end if not origin.to_host then - -- Just used for friendlier logging - origin.to_host = attr.to; + origin.to_host = to; end - - 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])); + + origin.log("debug", "asking %s if key %s belongs to them", from, stanza[1]); + module:fire_event("route/remote", { + from_host = to, to_host = from; + stanza = st.stanza("db:verify", { from = to, to = from, id = origin.streamid }):text(stanza[1]); + }); return true; end end); 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 + local dialback_verifying = dialback_requests[attr.from.."/"..(attr.id or "")]; + if dialback_verifying and attr.from == origin.to_host then local valid; if attr.type == "valid" then - s2s_make_authenticated(dialback_verifying, attr.from); + module:fire_event("s2s-authenticated", { session = dialback_verifying, host = attr.from }); valid = "valid"; else -- Warn the original connection that is was not verified successfully - log("warn", "authoritative server for "..(attr.from or "(unknown)").." denied the key"); + log("warn", "authoritative server for %s denied the key", attr.from or "(unknown)"); valid = "invalid"; end - if not dialback_verifying.sends2s then + if dialback_verifying.destroyed then log("warn", "Incoming s2s session %s was closed in the meantime, so we can't notify it of the db result", tostring(dialback_verifying):match("%w+$")); else dialback_verifying.sends2s( st.stanza("db:result", { from = attr.to, to = attr.from, id = attr.id, type = valid }) :text(dialback_verifying.hosts[attr.from].dialback_key)); end - dialback_requests[attr.from] = nil; + dialback_requests[attr.from.."/"..(attr.id or "")] = nil; end return true; end @@ -110,10 +131,10 @@ end); 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"); @@ -124,9 +145,9 @@ module:hook("stanza/jabber:server:dialback:result", function(event) return true; end if stanza.attr.type == "valid" then - s2s_make_authenticated(origin, attr.from); + module:fire_event("s2s-authenticated", { session = origin, host = attr.from }); else - s2s_destroy_session(origin) + origin:close("not-authorized", "dialback authentication failed"); end return true; end @@ -135,19 +156,26 @@ end); module:hook_stanza("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza) if origin.external_auth == "failed" then module:log("debug", "SASL EXTERNAL failed, falling back to dialback"); - s2s_initiate_dialback(origin); + initiate_dialback(origin); return true; end end, 100); module:hook_stanza(xmlns_stream, "features", function (origin, stanza) if not origin.external_auth or origin.external_auth == "failed" then - s2s_initiate_dialback(origin); + module:log("debug", "Initiating dialback..."); + initiate_dialback(origin); return true; end end, 100); +module:hook("s2sout-authenticate-legacy", function (event) + module:log("debug", "Initiating dialback..."); + initiate_dialback(event.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(); + data.features:tag("dialback", { xmlns='urn:xmpp:features:dialback' }):up(); end); diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua index 907ca753..61749580 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -32,7 +32,9 @@ do -- validate disco_items end end -module:add_identity("server", "im", "Prosody"); -- FIXME should be in the non-existing mod_router +if module:get_host_type() == "local" then + module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router +end module:add_feature("http://jabber.org/protocol/disco#info"); module:add_feature("http://jabber.org/protocol/disco#items"); @@ -54,6 +56,12 @@ local function build_server_disco_info() done[feature] = true; end end + for _,extension in ipairs(module:get_host_items("extension")) do + if not done[extension] then + query:add_child(extension); + done[extension] = true; + end + end _cached_server_disco_info = query; _cached_server_caps_hash = calculate_hash(query); _cached_server_caps_feature = st.stanza("c", { @@ -81,15 +89,28 @@ end module:hook("item-added/identity", clear_disco_cache); module:hook("item-added/feature", clear_disco_cache); +module:hook("item-added/extension", clear_disco_cache); module:hook("item-removed/identity", clear_disco_cache); module:hook("item-removed/feature", clear_disco_cache); +module:hook("item-removed/extension", 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? + if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node}); + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("host-disco-info-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end local reply_query = get_server_disco_info(); reply_query.node = node; local reply = st.reply(stanza):add_child(reply_query); @@ -100,11 +121,23 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve 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? - + if node and node ~= "" then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node}); + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("host-disco-items-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); - for jid in pairs(get_children(module.host)) do - reply:tag("item", {jid = jid}):up(); + local ret = module:fire_event("host-disco-items", { origin = origin, stanza = stanza, reply = reply }); + if ret ~= nil then return ret; end + for jid, name in pairs(get_children(module.host)) do + reply:tag("item", {jid = jid, name = name~=true and name or nil}):up(); end for _, item in ipairs(disco_items) do reply:tag("item", {jid=item[1], name=item[2]}):up(); @@ -125,12 +158,24 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(even 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 username = jid_split(stanza.attr.to) or origin.username; if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then + if node and node ~= "" then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node}); + if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("account-disco-info-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end 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", { origin = origin, stanza = reply }); + module:fire_event("account-disco-info", { origin = origin, reply = reply }); origin.send(reply); return true; end @@ -139,12 +184,24 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(eve 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 username = jid_split(stanza.attr.to) or origin.username; if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then + if node and node ~= "" then + local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node}); + if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account + local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false}; + local ret = module:fire_event("account-disco-items-node", event); + if ret ~= nil then return ret; end + if event.exists then + origin.send(reply); + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist")); + end + return true; + end 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", { origin = origin, stanza = reply }); + module:fire_event("account-disco-items", { origin = origin, stanza = stanza, reply = reply }); origin.send(reply); return true; end diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua index 7a876f1d..be1a5508 100644 --- a/plugins/mod_groups.lua +++ b/plugins/mod_groups.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -13,15 +13,17 @@ local members; local groups_file; local jid, datamanager = require "util.jid", require "util.datamanager"; -local jid_bare, jid_prep = jid.bare, jid.prep; +local jid_prep = jid.prep; local module_host = module:get_host(); -function inject_roster_contacts(username, host, roster) +function inject_roster_contacts(event) + local username, host= event.username, event.host; --module:log("debug", "Injecting group members to roster"); local bare_jid = username.."@"..host; if not members[bare_jid] and not members[false] then return; end -- Not a member of any groups - + + local roster = event.roster; local function import_jids_to_roster(group_name) for jid in pairs(groups[group_name]) do -- Add them to roster @@ -48,7 +50,7 @@ function inject_roster_contacts(username, host, roster) import_jids_to_roster(group_name); end end - + -- Import public groups if members[false] then for _, group_name in ipairs(members[false]) do @@ -56,7 +58,7 @@ function inject_roster_contacts(username, host, roster) import_jids_to_roster(group_name); end end - + if roster[false] then roster[false].version = true; end @@ -80,12 +82,12 @@ function remove_virtual_contacts(username, host, datastore, data) end function module.load() - groups_file = config.get(module:get_host(), "core", "groups_file"); + groups_file = module:get_option_string("groups_file"); if not groups_file then return; end - + module:hook("roster-load", inject_roster_contacts); datamanager.add_callback(remove_virtual_contacts); - + groups = { default = {} }; members = { }; local curr_group = "default"; @@ -121,3 +123,8 @@ end function module.unload() datamanager.remove_callback(remove_virtual_contacts); end + +-- Public for other modules to access +function group_contains(group_name, jid) + return groups[group_name][jid]; +end diff --git a/plugins/mod_http.lua b/plugins/mod_http.lua new file mode 100644 index 00000000..95933da5 --- /dev/null +++ b/plugins/mod_http.lua @@ -0,0 +1,146 @@ +-- Prosody IM +-- Copyright (C) 2008-2012 Matthew Wild +-- Copyright (C) 2008-2012 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +module:set_global(); +module:depends("http_errors"); + +local portmanager = require "core.portmanager"; +local moduleapi = require "core.moduleapi"; +local url_parse = require "socket.url".parse; +local url_build = require "socket.url".build; + +local server = require "net.http.server"; + +server.set_default_host(module:get_option_string("http_default_host")); + +local function normalize_path(path) + if path:sub(-1,-1) == "/" then path = path:sub(1, -2); end + if path:sub(1,1) ~= "/" then path = "/"..path; end + return path; +end + +local function get_http_event(host, app_path, key) + local method, path = key:match("^(%S+)%s+(.+)$"); + if not method then -- No path specified, default to "" (base path) + method, path = key, ""; + end + if method:sub(1,1) == "/" then + return nil; + end + if app_path == "/" and path:sub(1,1) == "/" then + app_path = ""; + end + return method:upper().." "..host..app_path..path; +end + +local function get_base_path(host_module, app_name, default_app_path) + return (normalize_path(host_module:get_option("http_paths", {})[app_name] -- Host + or module:get_option("http_paths", {})[app_name] -- Global + or default_app_path)) -- Default + :gsub("%$(%w+)", { host = module.host }); +end + +local ports_by_scheme = { http = 80, https = 443, }; + +-- Helper to deduce a module's external URL +function moduleapi.http_url(module, app_name, default_path) + app_name = app_name or (module.name:gsub("^http_", "")); + local external_url = url_parse(module:get_option_string("http_external_url")) or {}; + local services = portmanager.get_active_services(); + local http_services = services:get("https") or services:get("http") or {}; + for interface, ports in pairs(http_services) do + for port, services in pairs(ports) do + local url = { + scheme = (external_url.scheme or services[1].service.name); + host = (external_url.host or module:get_option_string("http_host", module.host)); + port = tonumber(external_url.port) or port or 80; + path = normalize_path(external_url.path or "/").. + (get_base_path(module, app_name, default_path or "/"..app_name):sub(2)); + } + if ports_by_scheme[url.scheme] == url.port then url.port = nil end + return url_build(url); + end + end +end + +function module.add_host(module) + local host = module:get_option_string("http_host", module.host); + local apps = {}; + module.environment.apps = apps; + local function http_app_added(event) + local app_name = event.item.name; + local default_app_path = event.item.default_path or "/"..app_name; + local app_path = get_base_path(module, app_name, default_app_path); + if not app_name then + -- TODO: Link to docs + module:log("error", "HTTP app has no 'name', add one or use module:provides('http', app)"); + return; + end + apps[app_name] = apps[app_name] or {}; + local app_handlers = apps[app_name]; + for key, handler in pairs(event.item.route or {}) do + local event_name = get_http_event(host, app_path, key); + if event_name then + if type(handler) ~= "function" then + local data = handler; + handler = function () return data; end + elseif event_name:sub(-2, -1) == "/*" then + local base_path_len = #event_name:match("/.+$"); + local _handler = handler; + handler = function (event) + local path = event.request.path:sub(base_path_len); + return _handler(event, path); + end; + end + if not app_handlers[event_name] then + app_handlers[event_name] = handler; + module:hook_object_event(server, event_name, handler); + else + module:log("warn", "App %s added handler twice for '%s', ignoring", app_name, event_name); + end + else + module:log("error", "Invalid route in %s, %q. See http://prosody.im/doc/developers/http#routes", app_name, key); + end + end + end + + local function http_app_removed(event) + local app_handlers = apps[event.item.name]; + apps[event.item.name] = nil; + for event, handler in pairs(app_handlers) do + module:unhook_object_event(server, event, handler); + end + end + + module:handle_items("http-provider", http_app_added, http_app_removed); + + server.add_host(host); + function module.unload() + server.remove_host(host); + end +end + +module:provides("net", { + name = "http"; + listener = server.listener; + default_port = 5280; + multiplex = { + pattern = "^[A-Z]"; + }; +}); + +module:provides("net", { + name = "https"; + listener = server.listener; + default_port = 5281; + encryption = "ssl"; + ssl_config = { verify = "none" }; + multiplex = { + pattern = "^[A-Z]"; + }; +}); diff --git a/plugins/mod_http_errors.lua b/plugins/mod_http_errors.lua new file mode 100644 index 00000000..0c37e104 --- /dev/null +++ b/plugins/mod_http_errors.lua @@ -0,0 +1,75 @@ +module:set_global(); + +local server = require "net.http.server"; +local codes = require "net.http.codes"; + +local show_private = module:get_option_boolean("http_errors_detailed", false); +local always_serve = module:get_option_boolean("http_errors_always_show", true); +local default_message = { module:get_option_string("http_errors_default_message", "That's all I know.") }; +local default_messages = { + [400] = { "What kind of request do you call that??" }; + [403] = { "You're not allowed to do that." }; + [404] = { "Whatever you were looking for is not here. %"; + "Where did you put it?", "It's behind you.", "Keep looking." }; + [500] = { "% Check your error log for more info."; + "Gremlins.", "It broke.", "Don't look at me." }; +}; + +local messages = setmetatable(module:get_option("http_errors_messages", {}), { __index = default_messages }); + +local html = [[ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <style> + body{ + margin-top:14%; + text-align:center; + background-color:#F8F8F8; + font-family:sans-serif; + } + h1{ + font-size:xx-large; + } + p{ + font-size:x-large; + } + p+p { font-size: large; font-family: courier } + </style> +</head> +<body> + <h1>$title</h1> + <p>$message</p> + <p>$extra</p> +</body> +</html>]]; +html = html:gsub("%s%s+", ""); + +local entities = { + ["<"] = "<", [">"] = ">", ["&"] = "&", + ["'"] = "'", ["\""] = """, ["\n"] = "<br/>", +}; + +local function tohtml(plain) + return (plain:gsub("[<>&'\"\n]", entities)); + +end + +local function get_page(code, extra) + local message = messages[code]; + if always_serve or message then + message = message or default_message; + return (html:gsub("$(%a+)", { + title = rawget(codes, code) or ("Code "..tostring(code)); + message = message[1]:gsub("%%", function () + return message[math.random(2, math.max(#message,2))]; + end); + extra = tohtml(extra or ""); + })); + end +end + +module:hook_object_event(server, "http-error", function (event) + return get_page(event.code, (show_private and event.private_message) or event.message); +end); diff --git a/plugins/mod_http_files.lua b/plugins/mod_http_files.lua new file mode 100644 index 00000000..dd04853b --- /dev/null +++ b/plugins/mod_http_files.lua @@ -0,0 +1,153 @@ +-- 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. +-- + +module:depends("http"); +local server = require"net.http.server"; +local lfs = require "lfs"; + +local os_date = os.date; +local open = io.open; +local stat = lfs.attributes; +local build_path = require"socket.url".build_path; + +local base_path = module:get_option_string("http_files_dir", module:get_option_string("http_path")); +local dir_indices = module:get_option("http_index_files", { "index.html", "index.htm" }); +local directory_index = module:get_option_boolean("http_dir_listing"); + +local mime_map = module:shared("/*/http_files/mime").types; +if not mime_map then + mime_map = { + html = "text/html", htm = "text/html", + xml = "application/xml", + txt = "text/plain", + css = "text/css", + js = "application/javascript", + png = "image/png", + gif = "image/gif", + jpeg = "image/jpeg", jpg = "image/jpeg", + svg = "image/svg+xml", + }; + module:shared("/*/http_files/mime").types = mime_map; + + local mime_types, err = open(module:get_option_string("mime_types_file", "/etc/mime.types"),"r"); + if mime_types then + local mime_data = mime_types:read("*a"); + mime_types:close(); + setmetatable(mime_map, { + __index = function(t, ext) + local typ = mime_data:match("\n(%S+)[^\n]*%s"..(ext:lower()).."%s") or "application/octet-stream"; + t[ext] = typ; + return typ; + end + }); + end +end + +local cache = setmetatable({}, { __mode = "kv" }); -- Let the garbage collector have it if it wants to. + +function serve(opts) + if type(opts) ~= "table" then -- assume path string + opts = { path = opts }; + end + local base_path = opts.path; + local dir_indices = opts.index_files or dir_indices; + local directory_index = opts.directory_index; + local function serve_file(event, path) + local request, response = event.request, event.response; + local orig_path = request.path; + local full_path = base_path .. (path and "/"..path or ""); + local attr = stat(full_path); + if not attr then + return 404; + end + + local request_headers, response_headers = request.headers, response.headers; + + local last_modified = os_date('!%a, %d %b %Y %H:%M:%S GMT', attr.modification); + response_headers.last_modified = last_modified; + + local etag = ("%02x-%x-%x-%x"):format(attr.dev or 0, attr.ino or 0, attr.size or 0, attr.modification or 0); + response_headers.etag = etag; + + local if_none_match = request_headers.if_none_match + local if_modified_since = request_headers.if_modified_since; + if etag == if_none_match + or (not if_none_match and last_modified == if_modified_since) then + return 304; + end + + local data = cache[orig_path]; + if data and data.etag == etag then + response_headers.content_type = data.content_type; + data = data.data; + elseif attr.mode == "directory" and path then + if full_path:sub(-1) ~= "/" then + local path = { is_absolute = true, is_directory = true }; + for dir in orig_path:gmatch("[^/]+") do path[#path+1]=dir; end + response_headers.location = build_path(path); + return 301; + end + for i=1,#dir_indices do + if stat(full_path..dir_indices[i], "mode") == "file" then + return serve_file(event, path..dir_indices[i]); + end + end + + if directory_index then + data = server._events.fire_event("directory-index", { path = request.path, full_path = full_path }); + end + if not data then + return 403; + end + cache[orig_path] = { data = data, content_type = mime_map.html; etag = etag; }; + response_headers.content_type = mime_map.html; + + else + local f, err = open(full_path, "rb"); + if f then + data, err = f:read("*a"); + f:close(); + end + if not data then + module:log("debug", "Could not open or read %s. Error was %s", full_path, err); + return 403; + end + local ext = full_path:match("%.([^./]+)$"); + local content_type = ext and mime_map[ext]; + cache[orig_path] = { data = data; content_type = content_type; etag = etag }; + response_headers.content_type = content_type; + end + + return response:send(data); + end + + return serve_file; +end + +function wrap_route(routes) + for route,handler in pairs(routes) do + if type(handler) ~= "function" then + routes[route] = serve(handler); + end + end + return routes; +end + +if base_path then + module:provides("http", { + route = { + ["GET /*"] = serve { + path = base_path; + directory_index = directory_index; + } + }; + }); +else + module:log("debug", "http_files_dir not set, assuming use by some other module"); +end + diff --git a/plugins/mod_httpserver.lua b/plugins/mod_httpserver.lua deleted file mode 100644 index 654aff06..00000000 --- a/plugins/mod_httpserver.lua +++ /dev/null @@ -1,97 +0,0 @@ --- 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 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"; - -local response_400 = { status = "400 Bad Request", body = "<h1>Bad Request</h1>Sorry, we didn't understand your request :(" }; -local response_403 = { status = "403 Forbidden", body = "<h1>Forbidden</h1>You don't have permission to view the contents of this directory :(" }; -local response_404 = { status = "404 Not Found", body = "<h1>Page Not Found</h1>Sorry, we couldn't find what you were looking for :(" }; - --- TODO: Should we read this from /etc/mime.types if it exists? (startup time...?) -local mime_map = { - html = "text/html"; - htm = "text/html"; - xml = "text/xml"; - xsl = "text/xml"; - txt = "text/plain; charset=utf-8"; - js = "text/javascript"; - css = "text/css"; -}; - -local function preprocess_path(path) - if path:sub(1,1) ~= "/" then - path = "/"..path; - end - local level = 0; - for component in path:gmatch("([^/]+)/") do - if component == ".." then - level = level - 1; - elseif component ~= "." then - level = level + 1; - end - if level < 0 then - return nil; - end - end - return path; -end - -function serve_file(path) - local 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(); - if not data then - return response_403; - end - local ext = path:match("%.([^.]*)$"); - local mime = mime_map[ext]; -- Content-Type should be nil when not known - return { - headers = { ["Content-Type"] = mime; }; - body = data; - }; -end - -local function handle_file_request(method, body, request) - local path = preprocess_path(request.url.path); - if not path then return response_400; end - path = path:gsub("^/[^/]+", ""); -- Strip /files/ - return serve_file(path); -end - -local function handle_default_request(method, body, request) - local path = preprocess_path(request.url.path); - if not path then return response_400; end - return serve_file(path); -end - -local function setup() - local ports = config.get(module.host, "core", "http_ports") or { 5280 }; - httpserver.set_default_handler(handle_default_request); - httpserver.new_from_config(ports, handle_file_request, { base = "files" }); -end -if prosody.start_time then -- already started - setup(); -else - prosody.events.add_handler("server-started", setup); -end diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua index 484a1f8f..c6d62e85 100644 --- a/plugins/mod_iq.lua +++ b/plugins/mod_iq.lua @@ -1,17 +1,15 @@ -- 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 st = require "util.stanza"; -local jid_split = require "util.jid".split; -local full_sessions = full_sessions; -local bare_sessions = bare_sessions; +local full_sessions = prosody.full_sessions; if module:get_host_type() == "local" then module:hook("iq/full", function(data) @@ -19,10 +17,7 @@ if module:get_host_type() == "local" then 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 not (session and session.send(stanza)) then if stanza.attr.type == "get" or stanza.attr.type == "set" then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end @@ -33,15 +28,16 @@ end module:hook("iq/bare", function(data) -- IQ to bare JID recieved - local origin, stanza = data.origin, data.stanza; + local stanza = data.stanza; local type = stanza.attr.type; -- TODO fire post processing events 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); + local xmlns = child.attr.xmlns or "jabber:client"; + local ret = module:fire_event("iq/bare/"..xmlns..":"..child.name, data); if ret ~= nil then return ret; end - return module:fire_event("iq-"..type.."/bare/"..child.attr.xmlns..":"..child.name, data); + return module:fire_event("iq-"..type.."/bare/"..xmlns..":"..child.name, data); else return module:fire_event("iq-"..type.."/bare/"..stanza.attr.id, data); end @@ -49,14 +45,15 @@ end); module:hook("iq/self", function(data) -- IQ to self JID recieved - local origin, stanza = data.origin, data.stanza; + local stanza = data.stanza; local type = stanza.attr.type; 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); + local xmlns = child.attr.xmlns or "jabber:client"; + local ret = module:fire_event("iq/self/"..xmlns..":"..child.name, data); if ret ~= nil then return ret; end - return module:fire_event("iq-"..type.."/self/"..child.attr.xmlns..":"..child.name, data); + return module:fire_event("iq-"..type.."/self/"..xmlns..":"..child.name, data); else return module:fire_event("iq-"..type.."/self/"..stanza.attr.id, data); end @@ -64,14 +61,15 @@ end); module:hook("iq/host", function(data) -- IQ to a local host recieved - local origin, stanza = data.origin, data.stanza; + local stanza = data.stanza; local type = stanza.attr.type; 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); + local xmlns = child.attr.xmlns or "jabber:client"; + local ret = module:fire_event("iq/host/"..xmlns..":"..child.name, data); if ret ~= nil then return ret; end - return module:fire_event("iq-"..type.."/host/"..child.attr.xmlns..":"..child.name, data); + return module:fire_event("iq-"..type.."/host/"..xmlns..":"..child.name, data); else return module:fire_event("iq-"..type.."/host/"..stanza.attr.id, data); end diff --git a/plugins/mod_lastactivity.lua b/plugins/mod_lastactivity.lua index 11053709..fabf07b4 100644 --- a/plugins/mod_lastactivity.lua +++ b/plugins/mod_lastactivity.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index 47a8c0ab..cb5ce0d3 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -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"; @@ -33,7 +35,7 @@ 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.")); + (session.sends2s or session.send)(st.error_reply(stanza, "cancel", "service-unavailable", "Legacy authentication is only allowed for unauthenticated client connections.")); return true; end @@ -41,7 +43,7 @@ module:hook("stanza/iq/jabber:iq:auth:query", function(event) 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"); @@ -55,7 +57,10 @@ module:hook("stanza/iq/jabber:iq:auth:query", function(event) username, password, resource = t_concat(username), t_concat(password), t_concat(resource); username = nodeprep(username); resource = resourceprep(resource) - local reply = st.reply(stanza); + if not (username and resource) then + session.send(st.error_reply(stanza, "modify", "bad-request")); + return true; + end if usermanager.test_password(username, session.host, password) then -- Authentication successful! local success, err = sessionmanager.make_authenticated(session, username); diff --git a/plugins/mod_message.lua b/plugins/mod_message.lua index df317532..fc337db0 100644 --- a/plugins/mod_message.lua +++ b/plugins/mod_message.lua @@ -1,24 +1,23 @@ -- 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 full_sessions = full_sessions; -local bare_sessions = bare_sessions; +local full_sessions = prosody.full_sessions; +local bare_sessions = prosody.bare_sessions; 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 t_insert = table.insert; local function process_to_bare(bare, origin, stanza) local user = bare_sessions[bare]; - + local t = stanza.attr.type; if t == "error" then -- discard @@ -36,10 +35,13 @@ local function process_to_bare(bare, origin, stanza) if user then -- some resources are connected local recipients = user.top_resources; if recipients then + local sent; for i=1,#recipients do - recipients[i].send(stanza); + sent = recipients[i].send(stanza) or sent; + end + if sent then + return true; end - return true; end end -- no resources are online @@ -64,11 +66,9 @@ end module:hook("message/full", function(data) -- message 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); + if session and session.send(stanza) then return true; else -- resource not online return process_to_bare(jid_bare(stanza.attr.to), origin, stanza); diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua index f323e606..1e2ee395 100644 --- a/plugins/mod_motd.lua +++ b/plugins/mod_motd.lua @@ -2,24 +2,29 @@ -- 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 motd_text = module:get_option_string("motd_text"); +local motd_jid = module:get_option_string("motd_jid", host); + +if not motd_text then return; end local st = require "util.stanza"; -module:hook("resource-bind", - function (event) - local session = event.session; - local motd_stanza = - st.message({ to = session.username..'@'..session.host, from = motd_jid }) - :tag("body"):text(motd_text); - core_route_stanza(hosts[host], motd_stanza); - module:log("debug", "MOTD send to user %s@%s", session.username, session.host); +motd_text = motd_text:gsub("^%s*(.-)%s*$", "%1"):gsub("\n%s+", "\n"); -- Strip indentation from the config -end); +module:hook("presence/bare", function (event) + local session, stanza = event.origin, event.stanza; + if session.username and not session.presence + and not stanza.attr.type and not stanza.attr.to then + local motd_stanza = + st.message({ to = session.full_jid, from = motd_jid }) + :tag("body"):text(motd_text); + module:send(motd_stanza); + module:log("debug", "MOTD send to user %s", session.full_jid); + end +end, 1); diff --git a/plugins/mod_net_multiplex.lua b/plugins/mod_net_multiplex.lua new file mode 100644 index 00000000..d666b907 --- /dev/null +++ b/plugins/mod_net_multiplex.lua @@ -0,0 +1,70 @@ +module:set_global(); + +local max_buffer_len = module:get_option_number("multiplex_buffer_size", 1024); + +local portmanager = require "core.portmanager"; + +local available_services = {}; + +local function add_service(service) + local multiplex_pattern = service.multiplex and service.multiplex.pattern; + if multiplex_pattern then + module:log("debug", "Adding multiplex service %q with pattern %q", service.name, multiplex_pattern); + available_services[service] = multiplex_pattern; + else + module:log("debug", "Service %q is not multiplex-capable", service.name); + end +end +module:hook("service-added", function (event) add_service(event.service); end); +module:hook("service-removed", function (event) available_services[event.service] = nil; end); + +for service_name, services in pairs(portmanager.get_registered_services()) do + for i, service in ipairs(services) do + add_service(service); + end +end + +local buffers = {}; + +local listener = { default_mode = "*a" }; + +function listener.onconnect() +end + +function listener.onincoming(conn, data) + if not data then return; end + local buf = buffers[conn]; + buffers[conn] = nil; + buf = buf and buf..data or data; + for service, multiplex_pattern in pairs(available_services) do + if buf:match(multiplex_pattern) then + module:log("debug", "Routing incoming connection to %s", service.name); + local listener = service.listener; + conn:setlistener(listener); + local onconnect = listener.onconnect; + if onconnect then onconnect(conn) end + return listener.onincoming(conn, buf); + end + end + if #buf > max_buffer_len then -- Give up + conn:close(); + else + buffers[conn] = buf; + end +end + +function listener.ondisconnect(conn, err) + buffers[conn] = nil; -- warn if no buffer? +end + +module:provides("net", { + name = "multiplex"; + config_prefix = ""; + listener = listener; +}); + +module:provides("net", { + name = "multiplex_ssl"; + config_prefix = "ssl"; + listener = listener; +}); diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua index 1ac62f94..c168711b 100644 --- a/plugins/mod_offline.lua +++ b/plugins/mod_offline.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -24,11 +24,11 @@ module:hook("message/offline/handle", function(event) 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); diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index 9ff6cac2..752cd28c 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -10,13 +10,12 @@ local jid_bare = require "util.jid".bare; local jid_split = require "util.jid".split; local st = require "util.stanza"; -local hosts = hosts; -local user_exists = require "core.usermanager".user_exists; local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; -local pairs, ipairs = pairs, ipairs; +local pairs = pairs; local next = next; local type = type; local calculate_hash = require "util.caps".calculate_hash; +local core_post_stanza = prosody.core_post_stanza; local NULL = {}; local data = {}; @@ -32,7 +31,7 @@ module.restore = function(state) hash_map = state.hash_map or {}; end -module:add_identity("pubsub", "pep", "Prosody"); +module:add_identity("pubsub", "pep", module:get_option_string("name", "Prosody")); module:add_feature("http://jabber.org/protocol/pubsub#publish"); local function subscription_presence(user_bare, recipient) @@ -63,7 +62,7 @@ local function publish(session, node, id, item) end else if not user_data then user_data = {}; data[bare] = user_data; end - user_data[node] = {id or "1", item}; + user_data[node] = {id, item}; end -- broadcast @@ -124,7 +123,7 @@ module:hook("presence/bare", function(event) 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 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 @@ -136,8 +135,9 @@ module:hook("presence/bare", function(event) 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 + -- COMPAT from ~= stanza.attr.to because OneTeam and Asterisk 1.8 can't deal with missing from attribute origin.send( - st.stanza("iq", {from=stanza.attr.to, to=stanza.attr.from, id="disco", type="get"}) + st.stanza("iq", {from=user, to=stanza.attr.from, id="disco", type="get"}) :query("http://jabber.org/protocol/disco#info") ); end @@ -169,7 +169,8 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) local node = payload.attr.node; payload = payload.tags[1]; if payload and payload.name == "item" then -- <item> - local id = payload.attr.id; + local id = payload.attr.id or "1"; + payload.attr.id = id; session.send(st.reply(stanza)); publish(session, node, id, st.clone(payload)); return true; @@ -262,19 +263,19 @@ module:hook("iq-result/bare/disco", function(event) end); module:hook("account-disco-info", function(event) - local stanza = event.stanza; - stanza:tag('identity', {category='pubsub', type='pep'}):up(); - stanza:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up(); + local reply = event.reply; + reply:tag('identity', {category='pubsub', type='pep'}):up(); + reply:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up(); end); module:hook("account-disco-items", function(event) - local stanza = event.stanza; - local bare = stanza.attr.to; + local reply = event.reply; + local bare = reply.attr.to; local user_data = data[bare]; if user_data then for node, _ in pairs(user_data) do - stanza:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes + reply:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes end end end); diff --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua index c0ba6189..eddb92d2 100644 --- a/plugins/mod_ping.lua +++ b/plugins/mod_ping.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -22,8 +22,10 @@ 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 = os.date("%Y-%m-%dT%X"); + local now = datetime(); return { info = "Pong\n"..now, status = "completed" }; end diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua index d229c1b8..7a6ccd94 100644 --- a/plugins/mod_posix.lua +++ b/plugins/mod_posix.lua @@ -1,16 +1,18 @@ -- 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 want_pposix_version = "0.3.5"; +local want_pposix_version = "0.3.6"; 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 +if pposix._VERSION ~= want_pposix_version then + module:log("warn", "Unknown version (%s) of binary pposix module, expected %s. Perhaps you need to recompile?", tostring(pposix._VERSION), want_pposix_version); +end local signal = select(2, pcall(require, "util.signal")); if type(signal) == "string" then @@ -22,7 +24,7 @@ local stat = lfs.attributes; local prosody = _G.prosody; -module.host = "*"; -- we're a global module +module:set_global(); -- we're a global module local umask = module:get_option("umask") or "027"; pposix.umask(umask); @@ -34,19 +36,19 @@ module:hook("server-started", function () if gid then local success, msg = pposix.setgid(gid); if success then - module:log("debug", "Changed group to "..gid.." successfully."); + module:log("debug", "Changed group to %s successfully.", gid); else - module:log("error", "Failed to change group to "..gid..". Error: "..msg); - prosody.shutdown("Failed to change group to "..gid); + module:log("error", "Failed to change group to %s. Error: %s", gid, msg); + prosody.shutdown("Failed to change group to %s", gid); end end if uid then local success, msg = pposix.setuid(uid); if success then - module:log("debug", "Changed user to "..uid.." successfully."); + module:log("debug", "Changed user to %s successfully.", uid); else - module:log("error", "Failed to change user to "..uid..". Error: "..msg); - prosody.shutdown("Failed to change user to "..uid); + module:log("error", "Failed to change user to %s. Error: %s", uid, msg); + prosody.shutdown("Failed to change user to %s", uid); end end end); @@ -112,15 +114,15 @@ end local syslog_opened; function syslog_sink_maker(config) if not syslog_opened then - pposix.syslog_open("prosody"); + pposix.syslog_open("prosody", module:get_option_string("syslog_facility")); syslog_opened = true; end local syslog, format = pposix.syslog_log, string.format; return function (name, level, message, ...) if ... then - syslog(level, format(message, ...)); + syslog(level, name, format(message, ...)); else - syslog(level, message); + syslog(level, name, message); end end; end @@ -136,8 +138,17 @@ if daemonize == nil then end end +local function remove_log_sinks() + local lm = require "core.loggingmanager"; + lm.register_sink_type("console", nil); + lm.register_sink_type("stdout", nil); + lm.reload_logging(); +end + if daemonize then local function daemonize_server() + module:log("info", "Prosody is about to detach from the console, disabling further console output"); + remove_log_sinks(); local ok, ret = pposix.daemonize(); if not ok then module:log("error", "Failed to daemonize: %s", ret); @@ -172,7 +183,7 @@ if signal.signal then prosody.reload_config(); prosody.reopen_logfiles(); end); - + signal.signal("SIGINT", function () module:log("info", "Received SIGINT"); prosody.unlock_globals(); diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua index 61239c9a..2899bd7e 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -9,15 +9,19 @@ local log = module._log; local require = require; -local pairs, ipairs = pairs, ipairs; +local pairs = pairs; local t_concat, t_insert = table.concat, table.insert; local s_find = string.find; local tonumber = tonumber; +local core_post_stanza = prosody.core_post_stanza; local st = require "util.stanza"; local jid_split = require "util.jid".split; local jid_bare = require "util.jid".bare; -local hosts = hosts; +local datetime = require "util.datetime"; +local hosts = prosody.hosts; +local bare_sessions = prosody.bare_sessions; +local full_sessions = prosody.full_sessions; local NULL = {}; local rostermanager = require "core.rostermanager"; @@ -115,8 +119,8 @@ function handle_normal_presence(origin, stanza) end if priority >= 0 then - local event = { origin = origin } - module:fire_event('message/offline/broadcast', event); + local event = { origin = origin } + module:fire_event('message/offline/broadcast', event); end end if stanza.attr.type == "unavailable" then @@ -134,6 +138,7 @@ function handle_normal_presence(origin, stanza) end else origin.presence = stanza; + stanza:tag("delay", { xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime() }):up(); if origin.priority ~= priority then origin.priority = priority; recalc_resource_map(user); @@ -160,7 +165,7 @@ function send_presence_of_available_resources(user, host, jid, recipient_session end end end - log("debug", "broadcasted presence of "..count.." resources from "..user.."@"..host.." to "..jid); + log("debug", "broadcasted presence of %d resources from %s@%s to %s", count, user, host, jid); return count; end @@ -169,7 +174,7 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_ 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); + log("debug", "outbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare); if stanza.attr.type == "probe" then stanza.attr.from, stanza.attr.to = st_from, st_to; return; @@ -197,12 +202,21 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_ 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); + -- 1. send unavailable + -- 2. route stanza + -- 3. roster push (subscription = from or both) + local success, pending_in, subscribed = rostermanager.unsubscribed(node, host, to_bare); + if success then + if subscribed then + rostermanager.roster_push(node, host, to_bare); + end + core_post_stanza(origin, stanza); + if subscribed then + send_presence_of_available_resources(node, host, to_bare, origin, st.presence({ type = "unavailable" })); + end end - 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; @@ -212,8 +226,8 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b 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; - log("debug", "inbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare); - + log("debug", "inbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare); + if stanza.attr.type == "probe" then local result, err = rostermanager.is_contact_subscribed(node, host, from_bare); if result then @@ -253,7 +267,9 @@ 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 @@ -296,7 +312,7 @@ module:hook("presence/bare", function(data) if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID 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]; if user then for _, session in pairs(user.sessions) do @@ -307,6 +323,8 @@ module:hook("presence/bare", function(data) end -- no resources not online, discard elseif not t or t == "unavailable" then handle_normal_presence(origin, stanza); + else + origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type")); end return true; end); @@ -328,8 +346,8 @@ module:hook("presence/full", function(data) end); module:hook("presence/host", function(data) -- inbound presence to the host - local origin, stanza = data.origin, data.stanza; - + local stanza = data.stanza; + local from_bare = jid_bare(stanza.attr.from); local t = stanza.attr.type; if t == "probe" then @@ -346,13 +364,15 @@ module:hook("resource-unbind", function(event) -- Send unavailable presence if session.presence then local pres = st.presence{ type = "unavailable" }; - if not(err) or err == "closed" then err = "connection closed"; end - pres:tag("status"):text("Disconnected: "..err):up(); + if err then + pres:tag("status"):text("Disconnected: "..err):up(); + end session:dispatch_stanza(pres); elseif session.directed then local pres = st.presence{ type = "unavailable", from = session.full_jid }; - if not(err) or err == "closed" then err = "connection closed"; end - pres:tag("status"):text("Disconnected: "..err):up(); + if err then + pres:tag("status"):text("Disconnected: "..err):up(); + end for jid in pairs(session.directed) do pres.attr.to = jid; core_post_stanza(session, pres, true); diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua index d5842e26..aaa8e383 100644 --- a/plugins/mod_privacy.lua +++ b/plugins/mod_privacy.lua @@ -2,23 +2,23 @@ -- Copyright (C) 2009-2010 Matthew Wild -- Copyright (C) 2009-2010 Waqas Hussain -- Copyright (C) 2009 Thilo Cestonaro --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- module:add_feature("jabber:iq:privacy"); -local prosody = prosody; local st = require "util.stanza"; -local datamanager = require "util.datamanager"; -local bare_sessions, full_sessions = bare_sessions, full_sessions; +local bare_sessions, full_sessions = prosody.bare_sessions, prosody.full_sessions; local util_Jid = require "util.jid"; local jid_bare = util_Jid.bare; local jid_split, jid_join = util_Jid.split, util_Jid.join; local load_roster = require "core.rostermanager".load_roster; local to_number = tonumber; +local privacy_storage = module:open_store(); + function isListUsed(origin, name, privacy_lists) local user = bare_sessions[origin.username.."@"..origin.host]; if user then @@ -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,9 +101,9 @@ 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 privacy_lists.lists = {}; end @@ -141,14 +119,14 @@ function createOrReplaceList (privacy_lists, origin, stanza, name, entries, rost if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then return {"modify", "bad-request", "Order attribute not valid."}; end - + if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then return {"modify", "bad-request", "Type attribute not valid."}; end - + local tmp = {}; orderCheck[item.attr.order] = true; - + tmp["type"] = item.attr.type; tmp["value"] = item.attr.value; tmp["action"] = item.attr.action; @@ -157,13 +135,13 @@ function createOrReplaceList (privacy_lists, origin, stanza, name, entries, rost tmp["presence-out"] = false; tmp["message"] = false; tmp["iq"] = false; - + if #item.tags > 0 then for _,tag in ipairs(item.tags) do tmp[tag.name] = true; end end - + if tmp.type == "subscription" then if tmp.value ~= "both" and tmp.value ~= "to" and @@ -172,13 +150,13 @@ function createOrReplaceList (privacy_lists, origin, stanza, name, entries, rost return {"cancel", "bad-request", "Subscription value must be both, to, from or none."}; end end - + if tmp.action ~= "deny" and tmp.action ~= "allow" then return {"cancel", "bad-request", "Action must be either deny or allow."}; end list.items[#list.items + 1] = tmp; end - + table.sort(list, function(a, b) return a.order < b.order; end); origin.send(st.reply(stanza)); @@ -229,18 +207,18 @@ function getList(privacy_lists, origin, stanza, name) return {"cancel", "item-not-found", "Unknown list specified."}; end end - + origin.send(reply); return true; end module:hook("iq/bare/jabber:iq:privacy:query", function(data) local origin, stanza = data.origin, data.stanza; - + if stanza.attr.to == nil then -- only service requests to own bare JID local query = stanza.tags[1]; -- the query element local valid = false; - local privacy_lists = datamanager.load(origin.username, origin.host, "privacy") or { lists = {} }; + local privacy_lists = privacy_storage:get(origin.username) or { lists = {} }; if privacy_lists.lists[1] then -- Code to migrate from old privacy lists format, remove in 0.8 module:log("info", "Upgrading format of stored privacy lists for %s@%s", origin.username, origin.host); @@ -295,7 +273,7 @@ module:hook("iq/bare/jabber:iq:privacy:query", function(data) end origin.send(st.error_reply(stanza, valid[1], valid[2], valid[3])); else - datamanager.store(origin.username, origin.host, "privacy", privacy_lists); + privacy_storage:set(origin.username, privacy_lists); end return true; end @@ -303,16 +281,16 @@ end); function checkIfNeedToBeBlocked(e, session) local origin, stanza = e.origin, e.stanza; - local privacy_lists = datamanager.load(session.username, session.host, "privacy") or {}; + local privacy_lists = privacy_storage:get(session.username) or {}; local bare_jid = session.username.."@"..session.host; local to = stanza.attr.to or bare_jid; local from = stanza.attr.from; - + local is_to_user = bare_jid == jid_bare(to); local is_from_user = bare_jid == jid_bare(from); - + --module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from)); - + if privacy_lists.lists == nil or not (session.activePrivacyList or privacy_lists.default) then @@ -322,8 +300,7 @@ function checkIfNeedToBeBlocked(e, session) --module:log("debug", "Not blocking communications between user's resources"); 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 @@ -390,6 +367,10 @@ function checkIfNeedToBeBlocked(e, session) end if apply then if block then + -- drop and not bounce groupchat messages, otherwise users will get kicked + if stanza.attr.type == "groupchat" then + return true; + end module:log("debug", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from)); if stanza.name == "message" then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); @@ -414,7 +395,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 diff --git a/plugins/mod_private.lua b/plugins/mod_private.lua index f1ebe786..446a80b2 100644 --- a/plugins/mod_private.lua +++ b/plugins/mod_private.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -9,8 +9,7 @@ local st = require "util.stanza" -local jid_split = require "util.jid".split; -local datamanager = require "util.datamanager" +local private_storage = module:open_store(); module:add_feature("jabber:iq:private"); @@ -21,7 +20,7 @@ module:hook("iq/self/jabber:iq:private:query", function(event) 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"); + local data, err = private_storage:get(origin.username); if err then origin.send(st.error_reply(stanza, "wait", "internal-server-error")); return true; @@ -40,7 +39,7 @@ module:hook("iq/self/jabber:iq:private:query", function(event) data[key] = st.preserialize(tag); end -- TODO delete datastore if empty - if datamanager.store(origin.username, origin.host, "private", data) then + if private_storage:set(origin.username, data) then origin.send(st.reply(stanza)); else origin.send(st.error_reply(stanza, "wait", "internal-server-error")); diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua index 5b490730..2ed9faac 100644 --- a/plugins/mod_proxy65.lua +++ b/plugins/mod_proxy65.lua @@ -1,109 +1,80 @@ +-- Prosody IM +-- Copyright (C) 2008-2011 Matthew Wild +-- Copyright (C) 2008-2011 Waqas Hussain -- Copyright (C) 2009 Thilo Cestonaro --- +-- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- ---[[ -* to restart the proxy in the console: e.g. -module:unload("proxy65"); -> server.removeserver(<proxy65_port>); -module:load("proxy65", <proxy65_jid>); -]]-- +module:set_global(); -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 jid_compare, jid_prep = require "util.jid".compare, require "util.jid".prep; local st = require "util.stanza"; -local connlisteners = require "net.connlisteners"; local sha1 = require "util.hashes".sha1; +local b64 = require "util.encodings".base64.encode; local server = require "net.server"; +local portmanager = require "core.portmanager"; -local host, name = module:get_host(), "SOCKS5 Bytestreams Service"; -local sessions, transfers, replies_cache = {}, {}, {}; - -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 sessions, transfers = module:shared("sessions", "transfers"); local max_buffer_size = 4096; -local connlistener = { default_port = proxy_port, default_interface = proxy_interface, default_mode = "*a" }; +local listener = {}; -function connlistener.onincoming(conn, data) +function listener.onincoming(conn, data) local session = sessions[conn] or {}; - - 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:byte(i) == 0x00) then -- 0x00 == method: NO AUTH - supported = true; - break; - end - end - if(supported) then - module:log("debug", "new session found ... ") - session.setup = true; - sessions[conn] = session; - conn:write(string.char(5, 0)); - end + + local transfer = transfers[session.sha]; + if transfer and transfer.activated then -- copy data between initiator and target + local initiator, target = transfer.initiator, transfer.target; + (conn == initiator and target or initiator):write(data); return; - end - if session.setup then - if session.sha ~= nil and transfers[session.sha] ~= nil then - local sha = session.sha; - if transfers[sha].activated == true and transfers[sha].target ~= nil then - if transfers[sha].initiator == conn then - transfers[sha].target:write(data); - else - transfers[sha].initiator:write(data); - end + end -- FIXME server.link should be doing this? + + if not session.greeting_done then + local nmethods = data:byte(2) or 0; + if data:byte(1) == 0x05 and nmethods > 0 and #data == 2 + nmethods then -- check if we have all the data + if data:find("%z") then -- 0x00 = 'No authentication' is supported + session.greeting_done = true; + sessions[conn] = session; + conn:write("\5\0"); -- send (SOCKS version 5, No authentication) + module:log("debug", "SOCKS5 greeting complete"); return; end - end - 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 + end -- else error, unexpected input + conn:write("\5\255"); -- send (SOCKS version 5, no acceptable method) + conn:close(); + module:log("debug", "Invalid SOCKS5 greeting recieved: '%s'", b64(data)); + else -- connection request + --local head = string.char( 0x05, 0x01, 0x00, 0x03, 40 ); -- ( VER=5=SOCKS5, CMD=1=CONNECT, RSV=0=RESERVED, ATYP=3=DOMAIMNAME, SHA-1 size ) + if #data == 47 and data:sub(1,5) == "\5\1\0\3\40" and data:sub(-2) == "\0\0" then + local sha = data:sub(6, 45); + conn:pause(); + conn:write("\5\0\0\3\40" .. sha .. "\0\0"); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte) + if not transfers[sha] then transfers[sha] = {}; - transfers[sha].activated = false; transfers[sha].target = conn; session.sha = sha; - module:log("debug", "target connected ... "); - elseif transfers[sha].target ~= nil then + module:log("debug", "SOCKS5 target connected for session %s", sha); + else -- transfers[sha].target ~= nil transfers[sha].initiator = conn; session.sha = sha; - module:log("debug", "initiator connected ... "); + module:log("debug", "SOCKS5 initiator connected for session %s", sha); 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) .. 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.") - conn:close(); - end - else - if data ~= nil then - module:log("warn", "unknown connection with no authentication data -> closing it"); + else -- error, unexpected input + conn:write("\5\1\0\3\0\0\0"); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte) conn:close(); + module:log("debug", "Invalid SOCKS5 negotiation recieved: '%s'", b64(data)); end end end -function connlistener.ondisconnect(conn, err) +function listener.ondisconnect(conn, err) local session = sessions[conn]; if session then - if session.sha and transfers[session.sha] then + if transfers[session.sha] then local initiator, target = transfers[session.sha].initiator, transfers[session.sha].target; if initiator == conn and target ~= nil then target:close(); @@ -117,131 +88,105 @@ function connlistener.ondisconnect(conn, err) end end -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") - :tag("identity", {category='proxy', type='bytestreams', name=name}):up() - :tag("feature", {var="http://jabber.org/protocol/bytestreams"}); - replies_cache.disco_info = reply; - end +function module.add_host(module) + local host, name = module:get_host(), module:get_option_string("name", "SOCKS5 Bytestreams Service"); - reply.attr.id = stanza.attr.id; - reply.attr.to = stanza.attr.from; - origin.send(reply); - return true; -end, -1); - -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"); - replies_cache.disco_items = reply; - end - - reply.attr.id = stanza.attr.id; - reply.attr.to = stanza.attr.from; - origin.send(reply); - return true; -end, -1); - -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 = stanza.attr.from; - - if proxy_acl and #proxy_acl > 0 then - for _, acl in ipairs(proxy_acl) do - if jid_compare(jid, acl) then allow = true; end - end - else - allow = true; + local proxy_address = module:get_option("proxy65_address", host); + local proxy_port = next(portmanager.get_active_services():search("proxy65", nil)[1] or {}); + local proxy_acl = module:get_option("proxy65_acl"); + + -- COMPAT w/pre-0.9 where proxy65_port was specified in the components section of the config + local legacy_config = module:get_option_number("proxy65_port"); + if legacy_config then + module:log("warn", "proxy65_port is deprecated, please put proxy65_ports = { %d } into the global section instead", legacy_config); end - if allow == true then - if reply == nil then - reply = st.iq({type="result", from=host}) - :query("http://jabber.org/protocol/bytestreams") - :tag("streamhost", {jid=host, host=proxy_address, port=proxy_port}); - replies_cache.stream_host = reply; + + 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; + if not stanza.tags[1].attr.node then + origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#info") + :tag("identity", {category='proxy', type='bytestreams', name=name}):up() + :tag("feature", {var="http://jabber.org/protocol/bytestreams"}) ); + return true; end - else - 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") - :tag("error", {code='403', type='auth'}) - :tag("forbidden", {xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'}); - replies_cache.stream_host_err = err_reply; + end, -1); + + module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function(event) + local origin, stanza = event.origin, event.stanza; + if not stanza.tags[1].attr.node then + origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#items")); + return true; end - reply = err_reply; - end - reply.attr.id = stanza.attr.id; - reply.attr.to = stanza.attr.from; - reply.tags[1].attr.sid = sid; - origin.send(reply); - return true; -end); - -module.unload = function() - connlisteners.deregister(module.host .. ':proxy65'); -end + end, -1); -local function set_activation(stanza) - 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}); - reply.attr.id = stanza.attr.id; - end - return reply, from, to, sid; -end + module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event) + local origin, stanza = event.origin, event.stanza; -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"; + -- check ACL + while proxy_acl and #proxy_acl > 0 do -- using 'while' instead of 'if' so we can break out of it + local jid = stanza.attr.from; + local allow; + for _, acl in ipairs(proxy_acl) do + if jid_compare(jid, acl) then allow = true; break; end end - origin.send(st.error_reply(stanza, "cancel", "not-allowed", message)); + if allow then break; end + module:log("warn", "Denying use of proxy for %s", tostring(stanza.attr.from)); + origin.send(st.error_reply(stanza, "auth", "forbidden")); + return true; end - return true; - else - module:log("error", "activation failed: sid: %s, initiator: %s, target: %s", tostring(sid), tostring(from), tostring(to)); - 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."); - module:log("error", "Possibly two proxy65 components are configured to share the same port."); + local sid = stanza.tags[1].attr.sid; + origin.send(st.reply(stanza):tag("query", {xmlns="http://jabber.org/protocol/bytestreams", sid=sid}) + :tag("streamhost", {jid=host, host=proxy_address, port=proxy_port})); + return true; + end); + + module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event) + local origin, stanza = event.origin, event.stanza; + + local query = stanza.tags[1]; + local sid = query.attr.sid; + local from = stanza.attr.from; + local to = query:get_child_text("activate"); + local prepped_to = jid_prep(to); + + local info = "sid: "..tostring(sid)..", initiator: "..tostring(from)..", target: "..tostring(prepped_to or to); + if prepped_to and sid then + local sha = sha1(sid .. from .. prepped_to, true); + if not transfers[sha] then + module:log("debug", "Activation request has unknown session id; activation failed (%s)", info); + origin.send(st.error_reply(stanza, "modify", "item-not-found")); + elseif not transfers[sha].initiator then + module:log("debug", "The sender was not connected to the proxy; activation failed (%s)", info); + origin.send(st.error_reply(stanza, "cancel", "not-allowed", "The sender (you) is not connected to the proxy")); + --elseif not transfers[sha].target then -- can't happen, as target is set when a transfer object is created + -- module:log("debug", "The recipient was not connected to the proxy; activation failed (%s)", info); + -- origin.send(st.error_reply(stanza, "cancel", "not-allowed", "The recipient is not connected to the proxy")); + else -- if transfers[sha].initiator ~= nil and transfers[sha].target ~= nil then + module:log("debug", "Transfer activated (%s)", info); + transfers[sha].activated = true; + transfers[sha].target:resume(); + transfers[sha].initiator:resume(); + origin.send(st.reply(stanza)); + end + elseif to and sid then + module:log("debug", "Malformed activation jid; activation failed (%s)", info); + origin.send(st.error_reply(stanza, "modify", "jid-malformed")); + else + module:log("debug", "Bad request; activation failed (%s)", info); + origin.send(st.error_reply(stanza, "modify", "bad-request")); + end + return true; + end); end -connlisteners.start(module.host .. ':proxy65'); +module:provides("net", { + default_port = 5000; + listener = listener; + multiplex = { + pattern = "^\5"; + }; +}); diff --git a/plugins/mod_pubsub.lua b/plugins/mod_pubsub.lua deleted file mode 100644 index 90c19815..00000000 --- a/plugins/mod_pubsub.lua +++ /dev/null @@ -1,369 +0,0 @@ -local pubsub = require "util.pubsub"; -local st = require "util.stanza"; -local jid_bare = require "util.jid".bare; -local uuid_generate = require "util.uuid".generate; - -require "core.modulemanager".load(module.host, "iq"); - -local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; -local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; -local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; - -local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false); -local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false); - -local service; - -local handlers = {}; - -function handle_pubsub_iq(event) - local origin, stanza = event.origin, event.stanza; - local pubsub = stanza.tags[1]; - local action = pubsub.tags[1]; - local handler = handlers[stanza.attr.type.."_"..action.name]; - if handler then - handler(origin, stanza, action); - return true; - end -end - -local pubsub_errors = { - ["conflict"] = { "cancel", "conflict" }; - ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; - ["item-not-found"] = { "cancel", "item-not-found" }; - ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" }; - ["forbidden"] = { "cancel", "forbidden" }; -}; -function pubsub_error_reply(stanza, error) - local e = pubsub_errors[error]; - local reply = st.error_reply(stanza, unpack(e, 1, 3)); - if e[4] then - reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); - end - return reply; -end - -function handlers.get_items(origin, stanza, items) - local node = items.attr.node; - local item = items:get_child("item"); - local id = item and item.attr.id; - - local ok, results = service:get_items(node, stanza.attr.from, id); - if not ok then - return origin.send(pubsub_error_reply(stanza, results)); - end - - local data = st.stanza("items", { node = node }); - for _, entry in pairs(results) do - data:add_child(entry); - end - if data then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :add_child(data); - else - reply = pubsub_error_reply(stanza, "item-not-found"); - end - return origin.send(reply); -end - -function handlers.get_subscriptions(origin, stanza, subscriptions) - local node = subscriptions.attr.node; - local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from); - if not ok then - return origin.send(pubsub_error_reply(stanza, ret)); - end - local reply = st.reply(stanza) - :tag("subscriptions", { xmlns = xmlns_pubsub }); - for _, sub in ipairs(ret) do - reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); - end - return origin.send(reply); -end - -function handlers.set_create(origin, stanza, create) - local node = create.attr.node; - local ok, ret, reply; - if node then - ok, ret = service:create(node, stanza.attr.from); - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - else - repeat - node = uuid_generate(); - ok, ret = service:create(node, stanza.attr.from); - until ok or ret ~= "conflict"; - if ok then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :tag("create", { node = node }); - else - reply = pubsub_error_reply(stanza, ret); - end - end - return origin.send(reply); -end - -function handlers.set_subscribe(origin, stanza, subscribe) - local node, jid = subscribe.attr.node, subscribe.attr.jid; - if jid_bare(jid) ~= jid_bare(stanza.attr.from) then - return origin.send(pubsub_error_reply(stanza, "invalid-jid")); - end - local ok, ret = service:add_subscription(node, stanza.attr.from, jid); - local reply; - if ok then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :tag("subscription", { - node = node, - jid = jid, - subscription = "subscribed" - }); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function handlers.set_unsubscribe(origin, stanza, unsubscribe) - local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid; - if jid_bare(jid) ~= jid_bare(stanza.attr.from) then - return origin.send(pubsub_error_reply(stanza, "invalid-jid")); - end - local ok, ret = service:remove_subscription(node, stanza.attr.from, jid); - local reply; - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function handlers.set_publish(origin, stanza, publish) - local node = publish.attr.node; - local item = publish:get_child("item"); - local id = (item and item.attr.id) or uuid_generate(); - local ok, ret = service:publish(node, stanza.attr.from, id, item); - local reply; - if ok then - reply = st.reply(stanza) - :tag("pubsub", { xmlns = xmlns_pubsub }) - :tag("publish", { node = node }) - :tag("item", { id = id }); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function handlers.set_retract(origin, stanza, retract) - local node, notify = retract.attr.node, retract.attr.notify; - notify = (notify == "1") or (notify == "true"); - local item = retract:get_child("item"); - local id = item and item.attr.id - local reply, notifier; - if notify then - notifier = st.stanza("retract", { id = id }); - end - local ok, ret = service:retract(node, stanza.attr.from, id, notifier); - if ok then - reply = st.reply(stanza); - else - reply = pubsub_error_reply(stanza, ret); - end - return origin.send(reply); -end - -function simple_broadcast(node, jids, item) - item = st.clone(item); - item.attr.xmlns = nil; -- Clear the pubsub namespace - local message = st.message({ from = module.host, type = "headline" }) - :tag("event", { xmlns = xmlns_pubsub_event }) - :tag("items", { node = node }) - :add_child(item); - for jid in pairs(jids) do - module:log("debug", "Sending notification to %s", jid); - message.attr.to = jid; - core_post_stanza(hosts[module.host], message); - end -end - -module:hook("iq/host/http://jabber.org/protocol/pubsub:pubsub", handle_pubsub_iq); - -local disco_info; - -local feature_map = { - create = { "create-nodes", autocreate_on_publish and "instant-nodes", "item-ids" }; - retract = { "delete-items", "retract-items" }; - publish = { "publish" }; - get_items = { "retrieve-items" }; - add_subscription = { "subscribe" }; - get_subscriptions = { "retrieve-subscriptions" }; -}; - -local function add_disco_features_from_service(disco, service) - for method, features in pairs(feature_map) do - if service[method] then - for _, feature in ipairs(features) do - if feature then - disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up(); - end - end - end - end - for affiliation in pairs(service.config.capabilities) do - if affiliation ~= "none" and affiliation ~= "owner" then - disco:tag("feature", { var = xmlns_pubsub.."#"..affiliation.."-affiliation" }):up(); - end - end -end - -local function build_disco_info(service) - local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" }) - :tag("identity", { category = "pubsub", type = "service" }):up() - :tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up(); - add_disco_features_from_service(disco_info, service); - return disco_info; -end - -module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event) - local origin, stanza = event.origin, event.stanza; - local node = stanza.tags[1].attr.node; - if not node then - return origin.send(st.reply(stanza):add_child(disco_info)); - else - local ok, ret = service:get_nodes(stanza.attr.from); - if ok and not ret[node] then - ok, ret = false, "item-not-found"; - end - if not ok then - return origin.send(pubsub_error_reply(stanza, ret)); - end - local reply = st.reply(stanza) - :tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node }) - :tag("identity", { category = "pubsub", type = "leaf" }); - return origin.send(reply); - end -end); - -local function handle_disco_items_on_node(event) - local stanza, origin = event.stanza, event.origin; - local query = stanza.tags[1]; - local node = query.attr.node; - local ok, ret = service:get_items(node, stanza.attr.from); - if not ok then - return origin.send(pubsub_error_reply(stanza, ret)); - end - - local reply = st.reply(stanza) - :tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node }); - - for id, item in pairs(ret) do - reply:tag("item", { jid = module.host, name = id }):up(); - end - - return origin.send(reply); -end - - -module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event) - if event.stanza.tags[1].attr.node then - return handle_disco_items_on_node(event); - end - local ok, ret = service:get_nodes(event.stanza.attr.from); - if not ok then - event.origin.send(pubsub_error_reply(stanza, ret)); - else - local reply = st.reply(event.stanza) - :tag("query", { xmlns = "http://jabber.org/protocol/disco#items" }); - for node, node_obj in pairs(ret) do - reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up(); - end - event.origin.send(reply); - end - return true; -end); - -local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); -local function get_affiliation(jid) - local bare_jid = jid_bare(jid); - if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then - return admin_aff; - end -end - -function set_service(new_service) - service = new_service; - module.environment.service = service; - disco_info = build_disco_info(service); -end - -function module.save() - return { service = service }; -end - -function module.restore(data) - set_service(data.service); -end - -set_service(pubsub.new({ - capabilities = { - none = { - create = false; - publish = false; - retract = false; - get_nodes = true; - - subscribe = true; - unsubscribe = true; - get_subscription = true; - get_subscriptions = true; - get_items = true; - - subscribe_other = false; - unsubscribe_other = false; - get_subscription_other = false; - get_subscriptions_other = false; - - be_subscribed = true; - be_unsubscribed = true; - - set_affiliation = false; - }; - owner = { - create = true; - publish = true; - retract = true; - get_nodes = true; - - subscribe = true; - unsubscribe = true; - get_subscription = true; - get_subscriptions = true; - get_items = true; - - - subscribe_other = true; - unsubscribe_other = true; - get_subscription_other = true; - get_subscriptions_other = true; - - be_subscribed = true; - be_unsubscribed = true; - - set_affiliation = true; - }; - }; - - autocreate_on_publish = autocreate_on_publish; - autocreate_on_subscribe = autocreate_on_subscribe; - - broadcaster = simple_broadcast; - get_affiliation = get_affiliation; - - normalize_jid = jid_bare; -})); diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua new file mode 100644 index 00000000..81a66f8b --- /dev/null +++ b/plugins/mod_pubsub/mod_pubsub.lua @@ -0,0 +1,229 @@ +local pubsub = require "util.pubsub"; +local st = require "util.stanza"; +local jid_bare = require "util.jid".bare; +local usermanager = require "core.usermanager"; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; +local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; +local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; + +local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false); +local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false); +local pubsub_disco_name = module:get_option("name"); +if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end + +local service; + +local lib_pubsub = module:require "pubsub"; +local handlers = lib_pubsub.handlers; +local pubsub_error_reply = lib_pubsub.pubsub_error_reply; + +module:depends("disco"); +module:add_identity("pubsub", "service", pubsub_disco_name); +module:add_feature("http://jabber.org/protocol/pubsub"); + +function handle_pubsub_iq(event) + local origin, stanza = event.origin, event.stanza; + local pubsub = stanza.tags[1]; + local action = pubsub.tags[1]; + if not action then + return origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + local handler = handlers[stanza.attr.type.."_"..action.name]; + if handler then + handler(origin, stanza, action, service); + return true; + end +end + +function simple_broadcast(kind, node, jids, item) + if item then + item = st.clone(item); + item.attr.xmlns = nil; -- Clear the pubsub namespace + end + local message = st.message({ from = module.host, type = "headline" }) + :tag("event", { xmlns = xmlns_pubsub_event }) + :tag(kind, { node = node }) + :add_child(item); + for jid in pairs(jids) do + module:log("debug", "Sending notification to %s", jid); + message.attr.to = jid; + module:send(message); + end +end + +module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq); +module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq); + +local feature_map = { + create = { "create-nodes", "instant-nodes", "item-ids" }; + retract = { "delete-items", "retract-items" }; + purge = { "purge-nodes" }; + publish = { "publish", autocreate_on_publish and "auto-create" }; + delete = { "delete-nodes" }; + get_items = { "retrieve-items" }; + add_subscription = { "subscribe" }; + get_subscriptions = { "retrieve-subscriptions" }; +}; + +local function add_disco_features_from_service(service) + for method, features in pairs(feature_map) do + if service[method] then + for _, feature in ipairs(features) do + if feature then + module:add_feature(xmlns_pubsub.."#"..feature); + end + end + end + end + for affiliation in pairs(service.config.capabilities) do + if affiliation ~= "none" and affiliation ~= "owner" then + module:add_feature(xmlns_pubsub.."#"..affiliation.."-affiliation"); + end + end +end + +module:hook("host-disco-info-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + local ok, ret = service:get_nodes(stanza.attr.from); + if ok and not ret[node] then + return; + end + if not ok then + return origin.send(pubsub_error_reply(stanza, ret)); + end + event.exists = true; + reply:tag("identity", { category = "pubsub", type = "leaf" }); +end); + +module:hook("host-disco-items-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + local ok, ret = service:get_items(node, stanza.attr.from); + if not ok then + return origin.send(pubsub_error_reply(stanza, ret)); + end + + for id, item in pairs(ret) do + reply:tag("item", { jid = module.host, name = id }):up(); + end + event.exists = true; +end); + + +module:hook("host-disco-items", function (event) + local stanza, origin, reply = event.stanza, event.origin, event.reply; + local ok, ret = service:get_nodes(event.stanza.attr.from); + if not ok then + return origin.send(pubsub_error_reply(event.stanza, ret)); + end + for node, node_obj in pairs(ret) do + reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up(); + end +end); + +local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); +local function get_affiliation(jid) + local bare_jid = jid_bare(jid); + if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then + return admin_aff; + end +end + +function set_service(new_service) + service = new_service; + module.environment.service = service; + add_disco_features_from_service(service); +end + +function module.save() + return { service = service }; +end + +function module.restore(data) + set_service(data.service); +end + +function module.load() + if module.reloading then return; end + + set_service(pubsub.new({ + capabilities = { + none = { + create = false; + publish = false; + retract = false; + get_nodes = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + subscribe_other = false; + unsubscribe_other = false; + get_subscription_other = false; + get_subscriptions_other = false; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = false; + }; + publisher = { + create = false; + publish = true; + retract = true; + get_nodes = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + subscribe_other = false; + unsubscribe_other = false; + get_subscription_other = false; + get_subscriptions_other = false; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = false; + }; + owner = { + create = true; + publish = true; + retract = true; + delete = true; + get_nodes = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + + subscribe_other = true; + unsubscribe_other = true; + get_subscription_other = true; + get_subscriptions_other = true; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = true; + }; + }; + + autocreate_on_publish = autocreate_on_publish; + autocreate_on_subscribe = autocreate_on_subscribe; + + broadcaster = simple_broadcast; + get_affiliation = get_affiliation; + + normalize_jid = jid_bare; + })); +end diff --git a/plugins/mod_pubsub/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua new file mode 100644 index 00000000..2b015e34 --- /dev/null +++ b/plugins/mod_pubsub/pubsub.lib.lua @@ -0,0 +1,225 @@ +local st = require "util.stanza"; +local uuid_generate = require "util.uuid".generate; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; +local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; + +local _M = {}; + +local handlers = {}; +_M.handlers = handlers; + +local pubsub_errors = { + ["conflict"] = { "cancel", "conflict" }; + ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; + ["jid-required"] = { "modify", "bad-request", nil, "jid-required" }; + ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" }; + ["item-not-found"] = { "cancel", "item-not-found" }; + ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" }; + ["forbidden"] = { "cancel", "forbidden" }; +}; +local function pubsub_error_reply(stanza, error) + local e = pubsub_errors[error]; + local reply = st.error_reply(stanza, unpack(e, 1, 3)); + if e[4] then + reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); + end + return reply; +end +_M.pubsub_error_reply = pubsub_error_reply; + +function handlers.get_items(origin, stanza, items, service) + local node = items.attr.node; + local item = items:get_child("item"); + local id = item and item.attr.id; + + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + local ok, results = service:get_items(node, stanza.attr.from, id); + if not ok then + return origin.send(pubsub_error_reply(stanza, results)); + end + + local data = st.stanza("items", { node = node }); + for _, entry in pairs(results) do + data:add_child(entry); + end + local reply; + if data then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :add_child(data); + else + reply = pubsub_error_reply(stanza, "item-not-found"); + end + return origin.send(reply); +end + +function handlers.get_subscriptions(origin, stanza, subscriptions, service) + local node = subscriptions.attr.node; + local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from); + if not ok then + return origin.send(pubsub_error_reply(stanza, ret)); + end + local reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("subscriptions"); + for _, sub in ipairs(ret) do + reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); + end + return origin.send(reply); +end + +function handlers.set_create(origin, stanza, create, service) + local node = create.attr.node; + local ok, ret, reply; + if node then + ok, ret = service:create(node, stanza.attr.from); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + else + repeat + node = uuid_generate(); + ok, ret = service:create(node, stanza.attr.from); + until ok or ret ~= "conflict"; + if ok then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("create", { node = node }); + else + reply = pubsub_error_reply(stanza, ret); + end + end + return origin.send(reply); +end + +function handlers.set_delete(origin, stanza, delete, service) + local node = delete.attr.node; + + local reply, notifier; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + local ok, ret = service:delete(node, stanza.attr.from); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +function handlers.set_subscribe(origin, stanza, subscribe, service) + local node, jid = subscribe.attr.node, subscribe.attr.jid; + if not (node and jid) then + return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); + end + --[[ + local options_tag, options = stanza.tags[1]:get_child("options"), nil; + if options_tag then + options = options_form:data(options_tag.tags[1]); + end + --]] + local options_tag, options; -- FIXME + local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options); + local reply; + if ok then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("subscription", { + node = node, + jid = jid, + subscription = "subscribed" + }):up(); + if options_tag then + reply:add_child(options_tag); + end + else + reply = pubsub_error_reply(stanza, ret); + end + origin.send(reply); +end + +function handlers.set_unsubscribe(origin, stanza, unsubscribe, service) + local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid; + if not (node and jid) then + return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); + end + local ok, ret = service:remove_subscription(node, stanza.attr.from, jid); + local reply; + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +function handlers.set_publish(origin, stanza, publish, service) + local node = publish.attr.node; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + local item = publish:get_child("item"); + local id = (item and item.attr.id); + if not id then + id = uuid_generate(); + if item then + item.attr.id = id; + end + end + local ok, ret = service:publish(node, stanza.attr.from, id, item); + local reply; + if ok then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("publish", { node = node }) + :tag("item", { id = id }); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +function handlers.set_retract(origin, stanza, retract, service) + local node, notify = retract.attr.node, retract.attr.notify; + notify = (notify == "1") or (notify == "true"); + local item = retract:get_child("item"); + local id = item and item.attr.id + if not (node and id) then + return origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required")); + end + local reply, notifier; + if notify then + notifier = st.stanza("retract", { id = id }); + end + local ok, ret = service:retract(node, stanza.attr.from, id, notifier); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +function handlers.set_purge(origin, stanza, purge, service) + local node, notify = purge.attr.node, purge.attr.notify; + notify = (notify == "1") or (notify == "true"); + local reply; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + local ok, ret = service:purge(node, stanza.attr.from, notify); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +return _M; diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua index 1df73297..e537e903 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -1,24 +1,88 @@ -- 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 hosts = _G.hosts; local st = require "util.stanza"; -local datamanager = require "util.datamanager"; +local dataform_new = require "util.dataforms".new; 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 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); +local allow_registration = module:get_option_boolean("allow_registration", false); +local additional_fields = module:get_option("additional_registration_fields", {}); + +local account_details = module:open_store("account_details"); + +local field_map = { + username = { name = "username", type = "text-single", label = "Username", required = true }; + password = { name = "password", type = "text-private", label = "Password", required = true }; + nick = { name = "nick", type = "text-single", label = "Nickname" }; + name = { name = "name", type = "text-single", label = "Full Name" }; + first = { name = "first", type = "text-single", label = "Given Name" }; + last = { name = "last", type = "text-single", label = "Family Name" }; + email = { name = "email", type = "text-single", label = "Email" }; + address = { name = "address", type = "text-single", label = "Street" }; + city = { name = "city", type = "text-single", label = "City" }; + state = { name = "state", type = "text-single", label = "State" }; + zip = { name = "zip", type = "text-single", label = "Postal code" }; + phone = { name = "phone", type = "text-single", label = "Telephone number" }; + url = { name = "url", type = "text-single", label = "Webpage" }; + date = { name = "date", type = "text-single", label = "Birth date" }; +}; + +local registration_form = dataform_new{ + title = "Creating a new account"; + instructions = "Choose a username and password for use with this service."; + + field_map.username; + field_map.password; +}; + +local registration_query = st.stanza("query", {xmlns = "jabber:iq:register"}) + :tag("instructions"):text("Choose a username and password for use with this service."):up() + :tag("username"):up() + :tag("password"):up(); + +for _, field in ipairs(additional_fields) do + if type(field) == "table" then + registration_form[#registration_form + 1] = field; + else + if field:match("%+$") then + field = field:sub(1, #field - 1); + field_map[field].required = true; + end + + registration_form[#registration_form + 1] = field_map[field]; + registration_query:tag(field):up(); + end +end +registration_query:add_child(registration_form:form()); module:add_feature("jabber:iq:register"); -module:hook("iq/self/jabber:iq:register:query", function(event) +local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up(); +module:hook("stream-features", function(event) + local session, features = event.origin, event.features; + + -- Advertise registration to unauthorized clients only. + if not(allow_registration) or session.type ~= "c2s_unauthed" then + return + end + + features:add_child(register_stream_feature); +end); + +local function handle_registration_stanza(event) local session, stanza = event.origin, event.stanza; local query = stanza.tags[1]; @@ -31,45 +95,29 @@ module:hook("iq/self/jabber:iq:register:query", function(event) 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; - --session.send(st.error_reply(stanza, "cancel", "not-allowed")); - --return; - usermanager_set_password(username, nil, host); -- Disable account - -- FIXME the disabling currently allows a different user to recreate the account - -- we should add an in-memory account block mode when we have threading - session.send(st.reply(stanza)); - 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"}); + + local old_session_close = session.close; + session.close = function(session, ...) + session.send(st.reply(stanza)); + return old_session_close(session, ...); 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 + + 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.close = old_session_close; + session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); + return true; 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"); + local username = nodeprep(query:get_child_text("username")); + local password = query:get_child_text("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, password, session.host) then session.send(st.reply(stanza)); @@ -86,38 +134,66 @@ module:hook("iq/self/jabber:iq:register:query", function(event) end end return true; -end); +end -local recent_ips = {}; -local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations"); -local whitelist_only = module:get_option("whitelist_registration_only"); -local whitelisted_ips = module:get_option("registration_whitelist") or { "127.0.0.1" }; -local blacklisted_ips = module:get_option("registration_blacklist") or {}; +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 function parse_response(query) + local form = query:get_child("x", "jabber:x:data"); + if form then + return registration_form:data(form); + else + local data = {}; + local errors = {}; + for _, field in ipairs(registration_form) do + local name, required = field.name, field.required; + if field_map[name] then + data[name] = query:get_child_text(name); + if (not data[name] or #data[name] == 0) and required then + errors[name] = "Required value missing"; + end + end + end + if next(errors) then + return data, errors; + end + return data; + end +end -for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end -for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end +local recent_ips = {}; +local min_seconds_between_registrations = module:get_option_number("min_seconds_between_registrations"); +local whitelist_only = module:get_option_boolean("whitelist_registration_only"); +local whitelisted_ips = module:get_option_set("registration_whitelist", { "127.0.0.1" })._items; +local blacklisted_ips = module:get_option_set("registration_blacklist", {})._items; 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 + if not(allow_registration) or session.type ~= "c2s_unauthed" then session.send(st.error_reply(stanza, "cancel", "service-unavailable")); else local query = stanza.tags[1]; if stanza.attr.type == "get" then local reply = st.reply(stanza); - reply:tag("query", {xmlns = "jabber:iq:register"}) - :tag("instructions"):text("Choose a username and password for use with this service."):up() - :tag("username"):up() - :tag("password"):up(); + reply:add_child(registration_query); session.send(reply); elseif stanza.attr.type == "set" then if query.tags[1] and query.tags[1].name == "remove" then session.send(st.error_reply(stanza, "auth", "registration-required")); else - local username = query:child_with_name("username"); - local password = query:child_with_name("password"); - if username and password then + local data, errors = parse_response(query); + if errors then + session.send(st.error_reply(stanza, "modify", "not-acceptable")); + else -- Check that the user is not blacklisted or registering too often if not session.ip then module:log("debug", "User's IP not known; can't apply blacklist/whitelist"); @@ -130,7 +206,7 @@ module:hook("stanza/iq/jabber:iq:register:query", function(event) else local ip = recent_ips[session.ip]; ip.count = ip.count + 1; - + if os_time() - ip.time < min_seconds_between_registrations then ip.time = os_time(); session.send(st.error_reply(stanza, "wait", "not-acceptable")); @@ -139,32 +215,40 @@ module:hook("stanza/iq/jabber:iq:register:query", function(event) ip.time = os_time(); end end - -- FIXME shouldn't use table.concat - username = nodeprep(table.concat(username)); - password = table.concat(password); + local username, password = nodeprep(data.username), data.password; + data.username, data.password = nil, nil; local host = module.host; if not username or username == "" then session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is invalid.")); + return true; + end + local user = { username = username , host = host, allowed = true } + module:fire_event("user-registering", user); + if not user.allowed then + session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is forbidden.")); elseif usermanager_user_exists(username, host) then session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists.")); else + -- TODO unable to write file, file may be locked, etc, what's the correct error? + local error_reply = st.error_reply(stanza, "wait", "internal-server-error", "Failed to write data to disk."); if usermanager_create_user(username, password, host) then + if next(data) and not account_details:set(username, data) then + usermanager_delete_user(username, host); + session.send(error_reply); + return true; + end session.send(st.reply(stanza)); -- user created! module:log("info", "User account created: %s@%s", username, host); module:fire_event("user-registered", { username = username, host = host, source = "mod_register", session = session }); else - -- TODO unable to write file, file may be locked, etc, what's the correct error? - session.send(st.error_reply(stanza, "wait", "internal-server-error", "Failed to write data to disk.")); + session.send(error_reply); end end - else - session.send(st.error_reply(stanza, "modify", "not-acceptable")); end end end end return true; end); - diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua index fe2eea71..56af5368 100644 --- a/plugins/mod_roster.lua +++ b/plugins/mod_roster.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -15,14 +15,15 @@ local t_concat = table.concat; local tonumber = tonumber; local pairs, ipairs = pairs, ipairs; +local rm_load_roster = require "core.rostermanager".load_roster; local rm_remove_from_roster = require "core.rostermanager".remove_from_roster; local rm_add_to_roster = require "core.rostermanager".add_to_roster; local rm_roster_push = require "core.rostermanager".roster_push; -local core_post_stanza = core_post_stanza; +local core_post_stanza = prosody.core_post_stanza; module:add_feature("jabber:iq:roster"); -local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"}):tag("optional"):up(); +local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"}); module:hook("stream-features", function(event) local origin, features = event.origin, event.features; if origin.username then @@ -35,10 +36,10 @@ module:hook("iq/self/jabber:iq:roster:query", function(event) 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 @@ -68,7 +69,6 @@ module:hook("iq/self/jabber:iq:roster:query", function(event) 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 @@ -137,3 +137,20 @@ module:hook("iq/self/jabber:iq:roster:query", function(event) end return true; end); + +module:hook_global("user-deleted", function(event) + local username, host = event.username, event.host; + if host ~= module.host then return end + local bare = username .. "@" .. host; + local roster = rm_load_roster(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 + module:send(st.presence({type="unsubscribed", from=bare, to=jid})); + end + if item.subscription == "both" or item.subscription == "to" or item.ask then + module:send(st.presence({type="unsubscribe", from=bare, to=jid})); + end + end + end +end, 300); diff --git a/plugins/mod_s2s/mod_s2s.lua b/plugins/mod_s2s/mod_s2s.lua new file mode 100644 index 00000000..1d03f3e4 --- /dev/null +++ b/plugins/mod_s2s/mod_s2s.lua @@ -0,0 +1,694 @@ +-- 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. +-- + +module:set_global(); + +local prosody = prosody; +local hosts = prosody.hosts; +local core_process_stanza = prosody.core_process_stanza; + +local tostring, type = tostring, type; +local t_insert = table.insert; +local xpcall, traceback = xpcall, debug.traceback; +local NULL = {}; + +local add_task = require "util.timer".add_task; +local st = require "util.stanza"; +local initialize_filters = require "util.filters".initialize; +local nameprep = require "util.encodings".stringprep.nameprep; +local new_xmpp_stream = require "util.xmppstream".new; +local s2s_new_incoming = require "core.s2smanager".new_incoming; +local s2s_new_outgoing = require "core.s2smanager".new_outgoing; +local s2s_destroy_session = require "core.s2smanager".destroy_session; +local uuid_gen = require "util.uuid".generate; +local cert_verify_identity = require "util.x509".verify_identity; +local fire_global_event = prosody.events.fire_event; + +local s2sout = module:require("s2sout"); + +local connect_timeout = module:get_option_number("s2s_timeout", 90); +local stream_close_timeout = module:get_option_number("s2s_close_timeout", 5); +local opt_keepalives = module:get_option_boolean("s2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true)); +local secure_auth = module:get_option_boolean("s2s_secure_auth", false); -- One day... +local secure_domains, insecure_domains = + module:get_option_set("s2s_secure_domains", {})._items, module:get_option_set("s2s_insecure_domains", {})._items; +local require_encryption = module:get_option_boolean("s2s_require_encryption", false); + +local sessions = module:shared("sessions"); + +local log = module._log; + +--- Handle stanzas to remote domains + +local bouncy_stanzas = { message = true, presence = true, iq = true }; +local function bounce_sendq(session, reason) + local sendq = session.sendq; + if not sendq then return; end + session.log("info", "sending error replies for "..#sendq.." queued stanzas because of failed outgoing connection to "..tostring(session.to_host)); + local dummy = { + type = "s2sin"; + send = function(s) + (session.log or log)("error", "Replying to to an s2s error reply, please report this! Traceback: %s", traceback()); + end; + dummy = true; + }; + for i, data in ipairs(sendq) do + local reply = data[2]; + if reply and not(reply.attr.xmlns) and bouncy_stanzas[reply.name] then + reply.attr.type = "error"; + reply:tag("error", {type = "cancel"}) + :tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up(); + if reason then + reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}) + :text("Server-to-server connection failed: "..reason):up(); + end + core_process_stanza(dummy, reply); + end + sendq[i] = nil; + end + session.sendq = nil; +end + +-- Handles stanzas to existing s2s sessions +function route_to_existing_session(event) + local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza; + if not hosts[from_host] then + log("warn", "Attempt to send stanza from %s - a host we don't serve", from_host); + return false; + end + if hosts[to_host] then + log("warn", "Attempt to route stanza to a remote %s - a host we do serve?!", from_host); + return false; + end + local host = hosts[from_host].s2sout[to_host]; + if host then + -- We have a connection to this host already + if host.type == "s2sout_unauthed" and (stanza.name ~= "db:verify" or not host.dialback_key) then + (host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host); + + -- Queue stanza until we are able to send it + if host.sendq then t_insert(host.sendq, {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)}); + else host.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} }; end + host.log("debug", "stanza [%s] queued ", stanza.name); + return true; + elseif host.type == "local" or host.type == "component" then + log("error", "Trying to send a stanza to ourselves??") + log("error", "Traceback: %s", traceback()); + log("error", "Stanza: %s", tostring(stanza)); + return false; + else + (host.log or log)("debug", "going to send stanza to "..to_host.." from "..from_host); + -- FIXME + if host.from_host ~= from_host then + log("error", "WARNING! This might, possibly, be a bug, but it might not..."); + log("error", "We are going to send from %s instead of %s", tostring(host.from_host), tostring(from_host)); + end + if host.sends2s(stanza) then + host.log("debug", "stanza sent over %s", host.type); + return true; + end + end + end +end + +-- Create a new outgoing session for a stanza +function route_to_new_session(event) + local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza; + log("debug", "opening a new outgoing connection for this stanza"); + local host_session = s2s_new_outgoing(from_host, to_host); + + -- Store in buffer + host_session.bounce_sendq = bounce_sendq; + host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} }; + log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name)); + s2sout.initiate_connection(host_session); + if (not host_session.connecting) and (not host_session.conn) then + log("warn", "Connection to %s failed already, destroying session...", to_host); + s2s_destroy_session(host_session, "Connection failed"); + return false; + end + return true; +end + +local function keepalive(event) + return event.session.sends2s(' '); +end + +module:hook("s2s-read-timeout", keepalive, -1); + +function module.add_host(module) + if module:get_option_boolean("disallow_s2s", false) then + module:log("warn", "The 'disallow_s2s' config option is deprecated, please see http://prosody.im/doc/s2s#disabling"); + return nil, "This host has disallow_s2s set"; + end + module:hook("route/remote", route_to_existing_session, -1); + module:hook("route/remote", route_to_new_session, -10); + module:hook("s2s-authenticated", make_authenticated, -1); + module:hook("s2s-read-timeout", keepalive, -1); +end + +-- Stream is authorised, and ready for normal stanzas +function mark_connected(session) + local sendq, send = session.sendq, session.sends2s; + + local from, to = session.from_host, session.to_host; + + session.log("info", "%s s2s connection %s->%s complete", session.direction:gsub("^.", string.upper), from, to); + + local event_data = { session = session }; + if session.type == "s2sout" then + fire_global_event("s2sout-established", event_data); + hosts[from].events.fire_event("s2sout-established", event_data); + else + local host_session = hosts[to]; + session.send = function(stanza) + return host_session.events.fire_event("route/remote", { from_host = to, to_host = from, stanza = stanza }); + end; + + fire_global_event("s2sin-established", event_data); + hosts[to].events.fire_event("s2sin-established", event_data); + end + + if session.direction == "outgoing" then + if sendq then + session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host); + for i, data in ipairs(sendq) do + send(data[1]); + sendq[i] = nil; + end + session.sendq = nil; + end + + session.ip_hosts = nil; + session.srv_hosts = nil; + end +end + +function make_authenticated(event) + local session, host = event.session, event.host; + if not session.secure then + if require_encryption or (secure_auth and not(insecure_domains[host])) or secure_domains[host] then + session:close({ + condition = "policy-violation", + text = "Encrypted server-to-server communication is required but was not " + ..((session.direction == "outgoing" and "offered") or "used") + }); + end + end + if hosts[host] then + session:close({ condition = "undefined-condition", text = "Attempt to authenticate as a host we serve" }); + end + if session.type == "s2sout_unauthed" then + session.type = "s2sout"; + elseif session.type == "s2sin_unauthed" then + session.type = "s2sin"; + if host then + if not session.hosts[host] then session.hosts[host] = {}; end + session.hosts[host].authed = true; + end + elseif session.type == "s2sin" and host then + if not session.hosts[host] then session.hosts[host] = {}; end + session.hosts[host].authed = true; + else + return false; + end + session.log("debug", "connection %s->%s is now authenticated for %s", session.from_host, session.to_host, host); + + mark_connected(session); + + return true; +end + +--- Helper to check that a session peer's certificate is valid +local function check_cert_status(session) + local host = session.direction == "outgoing" and session.to_host or session.from_host + local conn = session.conn:socket() + local cert + if conn.getpeercertificate then + cert = conn:getpeercertificate() + end + + if cert then + local chain_valid, errors; + if conn.getpeerverification then + chain_valid, errors = conn:getpeerverification(); + elseif conn.getpeerchainvalid then -- COMPAT mw/luasec-hg + chain_valid, errors = conn:getpeerchainvalid(); + errors = (not chain_valid) and { { errors } } or nil; + else + chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } }; + end + -- Is there any interest in printing out all/the number of errors here? + if not chain_valid then + (session.log or log)("debug", "certificate chain validation result: invalid"); + for depth, t in pairs(errors or NULL) do + (session.log or log)("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", ")) + end + session.cert_chain_status = "invalid"; + else + (session.log or log)("debug", "certificate chain validation result: valid"); + session.cert_chain_status = "valid"; + + -- We'll go ahead and verify the asserted identity if the + -- connecting server specified one. + if host then + if cert_verify_identity(host, "xmpp-server", cert) then + session.cert_identity_status = "valid" + else + session.cert_identity_status = "invalid" + end + (session.log or log)("debug", "certificate identity validation result: %s", session.cert_identity_status); + end + end + end + return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert }); +end + +--- XMPP stream event handlers + +local stream_callbacks = { default_ns = "jabber:server", handlestanza = core_process_stanza }; + +local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; + +function stream_callbacks.streamopened(session, attr) + local send = session.sends2s; + + session.version = tonumber(attr.version) or 0; + + -- TODO: Rename session.secure to session.encrypted + if session.secure == false then + session.secure = true; + + local sock = session.conn:socket(); + if sock.info then + local info = sock:info(); + (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); + session.compressed = info.compression; + else + (session.log or log)("info", "Stream encrypted"); + session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg + end + end + + if session.direction == "incoming" then + -- Send a reply stream header + + -- Validate to/from + local to, from = nameprep(attr.to), nameprep(attr.from); + if not to and attr.to then -- COMPAT: Some servers do not reliably set 'to' (especially on stream restarts) + session:close({ condition = "improper-addressing", text = "Invalid 'to' address" }); + return; + end + if not from and attr.from then -- COMPAT: Some servers do not reliably set 'from' (especially on stream restarts) + session:close({ condition = "improper-addressing", text = "Invalid 'from' address" }); + return; + end + + -- Set session.[from/to]_host if they have not been set already and if + -- this session isn't already authenticated + if session.type == "s2sin_unauthed" and from and not session.from_host then + session.from_host = from; + elseif from ~= session.from_host then + session:close({ condition = "improper-addressing", text = "New stream 'from' attribute does not match original" }); + return; + end + if session.type == "s2sin_unauthed" and to and not session.to_host then + session.to_host = to; + elseif to ~= session.to_host then + session:close({ condition = "improper-addressing", text = "New stream 'to' attribute does not match original" }); + return; + end + + -- For convenience we'll put the sanitised values into these variables + to, from = session.to_host, session.from_host; + + session.streamid = uuid_gen(); + (session.log or log)("debug", "Incoming s2s received %s", st.stanza("stream:stream", attr):top_tag()); + if to then + if not hosts[to] then + -- Attempting to connect to a host we don't serve + session:close({ + condition = "host-unknown"; + text = "This host does not serve "..to + }); + return; + elseif not hosts[to].modules.s2s then + -- Attempting to connect to a host that disallows s2s + session:close({ + condition = "policy-violation"; + text = "Server-to-server communication is disabled for this host"; + }); + return; + end + end + + if hosts[from] then + session:close({ condition = "undefined-condition", text = "Attempt to connect from a host we serve" }); + return; + end + + if session.secure and not session.cert_chain_status then + if check_cert_status(session) == false then + return; + end + end + + session:open_stream(session.to_host, session.from_host) + if session.version >= 1.0 then + local features = st.stanza("stream:features"); + + if to then + hosts[to].events.fire_event("s2s-stream-features", { origin = session, features = features }); + else + (session.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", from or "unknown host"); + end + + log("debug", "Sending stream features: %s", tostring(features)); + send(features); + end + elseif session.direction == "outgoing" then + -- If we are just using the connection for verifying dialback keys, we won't try and auth it + if not attr.id then error("stream response did not give us a streamid!!!"); end + session.streamid = attr.id; + + if session.secure and not session.cert_chain_status then + if check_cert_status(session) == false then + return; + end + end + + -- Send unauthed buffer + -- (stanzas which are fine to send before dialback) + -- Note that this is *not* the stanza queue (which + -- we can only send if auth succeeds) :) + local send_buffer = session.send_buffer; + if send_buffer and #send_buffer > 0 then + log("debug", "Sending s2s send_buffer now..."); + for i, data in ipairs(send_buffer) do + session.sends2s(tostring(data)); + send_buffer[i] = nil; + end + end + session.send_buffer = nil; + + -- If server is pre-1.0, don't wait for features, just do dialback + if session.version < 1.0 then + if not session.dialback_verifying then + hosts[session.from_host].events.fire_event("s2sout-authenticate-legacy", { origin = session }); + else + mark_connected(session); + end + end + end + session.notopen = nil; +end + +function stream_callbacks.streamclosed(session) + (session.log or log)("debug", "Received </stream:stream>"); + session:close(false); +end + +function stream_callbacks.error(session, error, data) + if error == "no-stream" then + session:close("invalid-namespace"); + elseif error == "parse-error" then + session.log("debug", "Server-to-server XML parse error: %s", tostring(error)); + session:close("not-well-formed"); + elseif error == "stream-error" then + local condition, text = "undefined-condition"; + for child in data:children() do + if child.attr.xmlns == xmlns_xmpp_streams then + if child.name ~= "text" then + condition = child.name; + else + text = child:get_text(); + end + if condition ~= "undefined-condition" and text then + break; + end + end + end + text = condition .. (text and (" ("..text..")") or ""); + session.log("info", "Session closed by remote with error: %s", text); + session:close(nil, text); + end +end + +local function handleerr(err) log("error", "Traceback[s2s]: %s", traceback(tostring(err), 2)); end +function stream_callbacks.handlestanza(session, stanza) + if stanza.attr.xmlns == "jabber:client" then --COMPAT: Prosody pre-0.6.2 may send jabber:client + stanza.attr.xmlns = nil; + end + stanza = session.filter("stanzas/in", stanza); + if stanza then + return xpcall(function () return core_process_stanza(session, stanza) end, handleerr); + end +end + +local listener = {}; + +--- Session methods +local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; +local function session_close(session, reason, remote_reason) + local log = session.log or log; + if session.conn then + if session.notopen then + if session.direction == "incoming" then + session:open_stream(session.to_host, session.from_host); + else + session:open_stream(session.from_host, session.to_host); + end + end + if reason then -- nil == no err, initiated by us, false == initiated by remote + if type(reason) == "string" then -- assume stream error + log("debug", "Disconnecting %s[%s], <stream:error> is: %s", session.host or "(unknown host)", session.type, reason); + session.sends2s(st.stanza("stream:error"):tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' })); + elseif type(reason) == "table" then + if reason.condition then + local stanza = st.stanza("stream:error"):tag(reason.condition, stream_xmlns_attr):up(); + if reason.text then + stanza:tag("text", stream_xmlns_attr):text(reason.text):up(); + end + if reason.extra then + stanza:add_child(reason.extra); + end + log("debug", "Disconnecting %s[%s], <stream:error> is: %s", session.host or "(unknown host)", session.type, tostring(stanza)); + session.sends2s(stanza); + elseif reason.name then -- a stanza + log("debug", "Disconnecting %s->%s[%s], <stream:error> is: %s", session.from_host or "(unknown host)", session.to_host or "(unknown host)", session.type, tostring(reason)); + session.sends2s(reason); + end + end + end + + session.sends2s("</stream:stream>"); + function session.sends2s() return false; end + + local reason = remote_reason or (reason and (reason.text or reason.condition)) or reason; + session.log("info", "%s s2s stream %s->%s closed: %s", session.direction:gsub("^.", string.upper), session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed"); + + -- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote + local conn = session.conn; + if reason == nil and not session.notopen and session.type == "s2sin" then + add_task(stream_close_timeout, function () + if not session.destroyed then + session.log("warn", "Failed to receive a stream close response, closing connection anyway..."); + s2s_destroy_session(session, reason); + conn:close(); + end + end); + else + s2s_destroy_session(session, reason); + conn:close(); -- Close immediately, as this is an outgoing connection or is not authed + end + end +end + +function session_open_stream(session, from, to) + local attr = { + ["xmlns:stream"] = 'http://etherx.jabber.org/streams', + xmlns = 'jabber:server', + version = session.version and (session.version > 0 and "1.0" or nil), + ["xml:lang"] = 'en', + id = session.streamid, + from = from, to = to, + } + if not from or (hosts[from] and hosts[from].modules.dialback) then + attr["xmlns:db"] = 'jabber:server:dialback'; + end + + session.sends2s("<?xml version='1.0'?>"); + session.sends2s(st.stanza("stream:stream", attr):top_tag()); + return true; +end + +-- Session initialization logic shared by incoming and outgoing +local function initialize_session(session) + local stream = new_xmpp_stream(session, stream_callbacks); + session.stream = stream; + + session.notopen = true; + + function session.reset_stream() + session.notopen = true; + session.stream:reset(); + end + + session.open_stream = session_open_stream; + + local filter = session.filter; + function session.data(data) + data = filter("bytes/in", data); + if data then + local ok, err = stream:feed(data); + if ok then return; end + (session.log or log)("warn", "Received invalid XML: %s", data); + (session.log or log)("warn", "Problem was: %s", err); + session:close("not-well-formed"); + end + end + + session.close = session_close; + + local handlestanza = stream_callbacks.handlestanza; + function session.dispatch_stanza(session, stanza) + return handlestanza(session, stanza); + end + + add_task(connect_timeout, function () + if session.type == "s2sin" or session.type == "s2sout" then + return; -- Ok, we're connected + elseif session.type == "s2s_destroyed" then + return; -- Session already destroyed + end + -- Not connected, need to close session and clean up + (session.log or log)("debug", "Destroying incomplete session %s->%s due to inactivity", + session.from_host or "(unknown)", session.to_host or "(unknown)"); + session:close("connection-timeout"); + end); +end + +function listener.onconnect(conn) + conn:setoption("keepalive", opt_keepalives); + local session = sessions[conn]; + if not session then -- New incoming connection + session = s2s_new_incoming(conn); + sessions[conn] = session; + session.log("debug", "Incoming s2s connection"); + + local filter = initialize_filters(session); + local w = conn.write; + session.sends2s = function (t) + log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)")); + if t.name then + t = filter("stanzas/out", t); + end + if t then + t = filter("bytes/out", tostring(t)); + if t then + return w(conn, t); + end + end + end + + initialize_session(session); + else -- Outgoing session connected + session:open_stream(session.from_host, session.to_host); + end + session.ip = conn:ip(); +end + +function listener.onincoming(conn, data) + local session = sessions[conn]; + if session then + session.data(data); + end +end + +function listener.onstatus(conn, status) + if status == "ssl-handshake-complete" then + local session = sessions[conn]; + if session and session.direction == "outgoing" then + session.log("debug", "Sending stream header..."); + session:open_stream(session.from_host, session.to_host); + end + end +end + +function listener.ondisconnect(conn, err) + local session = sessions[conn]; + if session then + sessions[conn] = nil; + if err and session.direction == "outgoing" and session.notopen then + (session.log or log)("debug", "s2s connection attempt failed: %s", err); + if s2sout.attempt_connection(session, err) then + return; -- Session lives for now + end + end + (session.log or log)("debug", "s2s disconnected: %s->%s (%s)", tostring(session.from_host), tostring(session.to_host), tostring(err or "connection closed")); + s2s_destroy_session(session, err); + end +end + +function listener.onreadtimeout(conn) + local session = sessions[conn]; + if session then + return (hosts[session.host] or prosody).events.fire_event("s2s-read-timeout", { session = session }); + end +end + +function listener.register_outgoing(conn, session) + session.direction = "outgoing"; + sessions[conn] = session; + initialize_session(session); +end + +function check_auth_policy(event) + local host, session = event.host, event.session; + local must_secure = secure_auth; + + if not must_secure and secure_domains[host] then + must_secure = true; + elseif must_secure and insecure_domains[host] then + must_secure = false; + end + + if must_secure and (session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid") then + module:log("warn", "Forbidding insecure connection to/from %s", host); + if session.direction == "incoming" then + session:close({ condition = "not-authorized", text = "Your server's certificate is invalid, expired, or not trusted by "..session.to_host }); + else -- Close outgoing connections without warning + session:close(false); + end + return false; + end +end + +module:hook("s2s-check-certificate", check_auth_policy, -1); + +s2sout.set_listener(listener); + +module:hook("server-stopping", function(event) + local reason = event.reason; + for _, session in pairs(sessions) do + session:close{ condition = "system-shutdown", text = reason }; + end +end,500); + + + +module:provides("net", { + name = "s2s"; + listener = listener; + default_port = 5269; + encryption = "starttls"; + multiplex = { + pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:server%1.*>"; + }; +}); + diff --git a/plugins/mod_s2s/s2sout.lib.lua b/plugins/mod_s2s/s2sout.lib.lua new file mode 100644 index 00000000..ec8ea4d4 --- /dev/null +++ b/plugins/mod_s2s/s2sout.lib.lua @@ -0,0 +1,352 @@ +-- 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. +-- + +--- Module containing all the logic for connecting to a remote server + +local portmanager = require "core.portmanager"; +local wrapclient = require "net.server".wrapclient; +local initialize_filters = require "util.filters".initialize; +local idna_to_ascii = require "util.encodings".idna.to_ascii; +local new_ip = require "util.ip".new_ip; +local rfc6724_dest = require "util.rfc6724".destination; +local socket = require "socket"; +local adns = require "net.adns"; +local dns = require "net.dns"; +local t_insert, t_sort, ipairs = table.insert, table.sort, ipairs; +local local_addresses = require "util.net".local_addresses; + +local s2s_destroy_session = require "core.s2smanager".destroy_session; + +local log = module._log; + +local sources = {}; +local has_ipv4, has_ipv6; + +local dns_timeout = module:get_option_number("dns_timeout", 15); +dns.settimeout(dns_timeout); +local max_dns_depth = module:get_option_number("dns_max_depth", 3); + +local s2sout = {}; + +local s2s_listener; + + +function s2sout.set_listener(listener) + s2s_listener = listener; +end + +local function compare_srv_priorities(a,b) + return a.priority < b.priority or (a.priority == b.priority and a.weight > b.weight); +end + +function s2sout.initiate_connection(host_session) + initialize_filters(host_session); + host_session.version = 1; + + -- Kick the connection attempting machine into life + if not s2sout.attempt_connection(host_session) then + -- Intentionally not returning here, the + -- session is needed, connected or not + s2s_destroy_session(host_session); + end + + if not host_session.sends2s then + -- A sends2s which buffers data (until the stream is opened) + -- note that data in this buffer will be sent before the stream is authed + -- and will not be ack'd in any way, successful or otherwise + local buffer; + function host_session.sends2s(data) + if not buffer then + buffer = {}; + host_session.send_buffer = buffer; + end + log("debug", "Buffering data on unconnected s2sout to %s", tostring(host_session.to_host)); + buffer[#buffer+1] = data; + log("debug", "Buffered item %d: %s", #buffer, tostring(data)); + end + end +end + +function s2sout.attempt_connection(host_session, err) + local to_host = host_session.to_host; + local connect_host, connect_port = to_host and idna_to_ascii(to_host), 5269; + + if not connect_host then + return false; + end + + if not err then -- This is our first attempt + log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host); + host_session.connecting = true; + local handle; + handle = adns.lookup(function (answer) + handle = nil; + host_session.connecting = nil; + if answer and #answer > 0 then + log("debug", "%s has SRV records, handling...", to_host); + local srv_hosts = { answer = answer }; + host_session.srv_hosts = srv_hosts; + for _, record in ipairs(answer) do + t_insert(srv_hosts, record.srv); + end + if #srv_hosts == 1 and srv_hosts[1].target == "." then + log("debug", "%s does not provide a XMPP service", to_host); + s2s_destroy_session(host_session, err); -- Nothing to see here + return; + end + t_sort(srv_hosts, compare_srv_priorities); + + local srv_choice = srv_hosts[1]; + host_session.srv_choice = 1; + if srv_choice then + connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port; + log("debug", "Best record found, will connect to %s:%d", connect_host, connect_port); + end + else + log("debug", "%s has no SRV records, falling back to A/AAAA", to_host); + end + -- Try with SRV, or just the plain hostname if no SRV + local ok, err = s2sout.try_connect(host_session, connect_host, connect_port); + if not ok then + if not s2sout.attempt_connection(host_session, err) then + -- No more attempts will be made + s2s_destroy_session(host_session, err); + end + end + end, "_xmpp-server._tcp."..connect_host..".", "SRV"); + + return true; -- Attempt in progress + elseif host_session.ip_hosts then + return s2sout.try_connect(host_session, connect_host, connect_port, err); + elseif host_session.srv_hosts and #host_session.srv_hosts > host_session.srv_choice then -- Not our first attempt, and we also have SRV + host_session.srv_choice = host_session.srv_choice + 1; + local srv_choice = host_session.srv_hosts[host_session.srv_choice]; + connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port; + host_session.log("info", "Connection failed (%s). Attempt #%d: This time to %s:%d", tostring(err), host_session.srv_choice, connect_host, connect_port); + else + host_session.log("info", "Failed in all attempts to connect to %s", tostring(host_session.to_host)); + -- We're out of options + return false; + end + + if not (connect_host and connect_port) then + -- Likely we couldn't resolve DNS + log("warn", "Hmm, we're without a host (%s) and port (%s) to connect to for %s, giving up :(", tostring(connect_host), tostring(connect_port), tostring(to_host)); + return false; + end + + return s2sout.try_connect(host_session, connect_host, connect_port); +end + +function s2sout.try_next_ip(host_session) + host_session.connecting = nil; + host_session.ip_choice = host_session.ip_choice + 1; + local ip = host_session.ip_hosts[host_session.ip_choice]; + local ok, err= s2sout.make_connect(host_session, ip.ip, ip.port); + if not ok then + if not s2sout.attempt_connection(host_session, err or "closed") then + err = err and (": "..err) or ""; + s2s_destroy_session(host_session, "Connection failed"..err); + end + end +end + +function s2sout.try_connect(host_session, connect_host, connect_port, err) + host_session.connecting = true; + + if not err then + local IPs = {}; + host_session.ip_hosts = IPs; + local handle4, handle6; + local have_other_result = not(has_ipv4) or not(has_ipv6) or false; + + if has_ipv4 then + handle4 = adns.lookup(function (reply, err) + handle4 = nil; + + -- COMPAT: This is a compromise for all you CNAME-(ab)users :) + if not (reply and reply[#reply] and reply[#reply].a) then + local count = max_dns_depth; + reply = dns.peek(connect_host, "CNAME", "IN"); + while count > 0 and reply and reply[#reply] and not reply[#reply].a and reply[#reply].cname do + log("debug", "Looking up %s (DNS depth is %d)", tostring(reply[#reply].cname), count); + reply = dns.peek(reply[#reply].cname, "A", "IN") or dns.peek(reply[#reply].cname, "CNAME", "IN"); + count = count - 1; + end + end + -- end of CNAME resolving + + if reply and reply[#reply] and reply[#reply].a then + for _, ip in ipairs(reply) do + log("debug", "DNS reply for %s gives us %s", connect_host, ip.a); + IPs[#IPs+1] = new_ip(ip.a, "IPv4"); + end + end + + if have_other_result then + if #IPs > 0 then + rfc6724_dest(host_session.ip_hosts, sources); + for i = 1, #IPs do + IPs[i] = {ip = IPs[i], port = connect_port}; + end + host_session.ip_choice = 0; + s2sout.try_next_ip(host_session); + else + log("debug", "DNS lookup failed to get a response for %s", connect_host); + host_session.ip_hosts = nil; + if not s2sout.attempt_connection(host_session, "name resolution failed") then -- Retry if we can + log("debug", "No other records to try for %s - destroying", host_session.to_host); + err = err and (": "..err) or ""; + s2s_destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't + end + end + else + have_other_result = true; + end + end, connect_host, "A", "IN"); + else + have_other_result = true; + end + + if has_ipv6 then + handle6 = adns.lookup(function (reply, err) + handle6 = nil; + + if reply and reply[#reply] and reply[#reply].aaaa then + for _, ip in ipairs(reply) do + log("debug", "DNS reply for %s gives us %s", connect_host, ip.aaaa); + IPs[#IPs+1] = new_ip(ip.aaaa, "IPv6"); + end + end + + if have_other_result then + if #IPs > 0 then + rfc6724_dest(host_session.ip_hosts, sources); + for i = 1, #IPs do + IPs[i] = {ip = IPs[i], port = connect_port}; + end + host_session.ip_choice = 0; + s2sout.try_next_ip(host_session); + else + log("debug", "DNS lookup failed to get a response for %s", connect_host); + host_session.ip_hosts = nil; + if not s2sout.attempt_connection(host_session, "name resolution failed") then -- Retry if we can + log("debug", "No other records to try for %s - destroying", host_session.to_host); + err = err and (": "..err) or ""; + s2s_destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't + end + end + else + have_other_result = true; + end + end, connect_host, "AAAA", "IN"); + else + have_other_result = true; + end + return true; + elseif host_session.ip_hosts and #host_session.ip_hosts > host_session.ip_choice then -- Not our first attempt, and we also have IPs left to try + s2sout.try_next_ip(host_session); + else + host_session.ip_hosts = nil; + if not s2sout.attempt_connection(host_session, "out of IP addresses") then -- Retry if we can + log("debug", "No other records to try for %s - destroying", host_session.to_host); + err = err and (": "..err) or ""; + s2s_destroy_session(host_session, "Connecting failed"..err); -- End of the line, we can't + return false; + end + end + + return true; +end + +function s2sout.make_connect(host_session, connect_host, connect_port) + (host_session.log or log)("debug", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port); + + -- Reset secure flag in case this is another + -- connection attempt after a failed STARTTLS + host_session.secure = nil; + + local conn, handler; + local proto = connect_host.proto; + if proto == "IPv4" then + conn, handler = socket.tcp(); + elseif proto == "IPv6" and socket.tcp6 then + conn, handler = socket.tcp6(); + else + handler = "Unsupported protocol: "..tostring(proto); + end + + if not conn then + log("warn", "Failed to create outgoing connection, system error: %s", handler); + return false, handler; + end + + conn:settimeout(0); + local success, err = conn:connect(connect_host.addr, connect_port); + if not success and err ~= "timeout" then + log("warn", "s2s connect() to %s (%s:%d) failed: %s", host_session.to_host, connect_host.addr, connect_port, err); + return false, err; + end + + conn = wrapclient(conn, connect_host.addr, connect_port, s2s_listener, "*a"); + host_session.conn = conn; + + local filter = initialize_filters(host_session); + local w, log = conn.write, host_session.log; + host_session.sends2s = function (t) + log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?")); + if t.name then + t = filter("stanzas/out", t); + end + if t then + t = filter("bytes/out", tostring(t)); + if t then + return w(conn, tostring(t)); + end + end + end + + -- Register this outgoing connection so that xmppserver_listener knows about it + -- otherwise it will assume it is a new incoming connection + s2s_listener.register_outgoing(conn, host_session); + + log("debug", "Connection attempt in progress..."); + return true; +end + +module:hook_global("service-added", function (event) + if event.name ~= "s2s" then return end + + local s2s_sources = portmanager.get_active_services():get("s2s"); + if not s2s_sources then + module:log("warn", "s2s not listening on any ports, outgoing connections may fail"); + return; + end + for source, _ in pairs(s2s_sources) do + if source == "*" or source == "0.0.0.0" then + for _, addr in ipairs(local_addresses("ipv4", true)) do + sources[#sources + 1] = new_ip(addr, "IPv4"); + end + elseif source == "::" then + for _, addr in ipairs(local_addresses("ipv6", true)) do + sources[#sources + 1] = new_ip(addr, "IPv6"); + end + else + sources[#sources + 1] = new_ip(source, (source:find(":") and "IPv6") or "IPv4"); + end + end + for i = 1,#sources do + if sources[i].proto == "IPv6" then + has_ipv6 = true; + elseif sources[i].proto == "IPv4" then + has_ipv4 = true; + end + end +end); + +return s2sout; diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index 422bc187..f24eacf8 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -11,12 +11,10 @@ local st = require "util.stanza"; local sm_bind_resource = require "core.sessionmanager".bind_resource; local sm_make_authenticated = require "core.sessionmanager".make_authenticated; -local s2s_make_authenticated = require "core.s2smanager".make_authenticated; local base64 = require "util.encodings".base64; local cert_verify_identity = require "util.x509".verify_identity; -local nodeprep = require "util.encodings".stringprep.nodeprep; local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; local tostring = tostring; @@ -27,7 +25,6 @@ local log = module._log; 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 function build_reply(status, ret, err_msg) local reply = st.stanza(status, {xmlns = xmlns_sasl}); @@ -48,16 +45,17 @@ end local function handle_status(session, status, ret, err_msg) if status == "failure" then + module:fire_event("authentication-failure", { session = session, condition = ret, text = err_msg }); session.sasl_handler = session.sasl_handler:clean_clone(); elseif status == "success" then - local username = nodeprep(session.sasl_handler.username); - local ok, err = sm_make_authenticated(session, session.sasl_handler.username); if ok then + module:fire_event("authentication-success", { session = session }); session.sasl_handler = nil; session:reset_stream(); else module:log("warn", "SASL succeeded but username was invalid"); + module:fire_event("authentication-failure", { session = session, condition = "not-authorized", text = err }); session.sasl_handler = session.sasl_handler:clean_clone(); return "failure", "not-authorized", "User authenticated successfully, but username was invalid"; end @@ -89,13 +87,9 @@ module:hook_stanza(xmlns_sasl, "success", function (session, stanza) module:log("debug", "SASL EXTERNAL with %s succeeded", session.to_host); session.external_auth = "succeeded" session:reset_stream(); + session:open_stream(session.from_host, session.to_host); - 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()); - - s2s_make_authenticated(session, session.to_host); + module:fire_event("s2s-authenticated", { session = session, host = session.to_host }); return true; end) @@ -189,8 +183,10 @@ local function s2s_external_auth(session, stanza) session.from_host = text; end session.sends2s(build_reply("success")) - module:log("info", "Accepting SASL EXTERNAL identity from %s", text or session.from_host); - s2s_make_authenticated(session, text or session.from_host) + + local domain = text ~= "" and text or session.from_host; + module:log("info", "Accepting SASL EXTERNAL identity from %s", domain); + module:fire_event("s2s-authenticated", { session = session, host = domain }); session:reset_stream(); return true end @@ -207,7 +203,7 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event) 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); + session.sasl_handler = usermanager_get_sasl_handler(module.host, session); end local mechanism = stanza.attr.mechanism; if not session.secure and (secure_auth_only or (mechanism == "PLAIN" and not allow_unencrypted_plain_auth)) then @@ -245,7 +241,7 @@ module:hook("stream-features", function(event) if secure_auth_only and not origin.secure then return; end - origin.sasl_handler = usermanager_get_sasl_handler(module.host); + origin.sasl_handler = usermanager_get_sasl_handler(module.host, origin); if origin.secure then -- check wether LuaSec has the nifty binding to the function needed for tls-unique -- FIXME: would be nice to have this check only once and not for every socket @@ -256,13 +252,13 @@ module:hook("stream-features", function(event) origin.sasl_handler["userdata"] = origin.conn:socket(); end end - features:tag("mechanisms", mechanisms_attr); + local mechanisms = st.stanza("mechanisms", mechanisms_attr); 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(); + mechanisms:tag("mechanism"):text(mechanism):up(); end end - features:up(); + if mechanisms[1] then features:add_child(mechanisms); end else features:tag("bind", bind_attr):tag("required"):up():up(); features:tag("session", xmpp_session_attr):tag("optional"):up():up(); diff --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua new file mode 100644 index 00000000..972ecbee --- /dev/null +++ b/plugins/mod_storage_internal.lua @@ -0,0 +1,31 @@ +local datamanager = require "core.storagemanager".olddm; + +local host = module.host; + +local driver = {}; +local driver_mt = { __index = driver }; + +function driver:open(store, typ) + return setmetatable({ store = store, type = typ }, 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 + +function driver:stores(username) + return datamanager.stores(username, host); +end + +function driver:users() + return datamanager.users(host, self.store, self.type); +end + +function driver:purge(user) + return datamanager.purge(user, host); +end + +module:provides("storage", driver); diff --git a/plugins/mod_storage_none.lua b/plugins/mod_storage_none.lua new file mode 100644 index 00000000..8f2d2f56 --- /dev/null +++ b/plugins/mod_storage_none.lua @@ -0,0 +1,23 @@ +local driver = {}; +local driver_mt = { __index = driver }; + +function driver:open(store) + return setmetatable({ store = store }, driver_mt); +end +function driver:get(user) + return {}; +end + +function driver:set(user, data) + return nil, "Storage disabled"; +end + +function driver:stores(username) + return { "roster" }; +end + +function driver:purge(user) + return true; +end + +module:provides("storage", driver); diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua index 53f1ea0a..1f453d42 100644 --- a/plugins/mod_storage_sql.lua +++ b/plugins/mod_storage_sql.lua @@ -25,42 +25,154 @@ local tonumber = tonumber; local pairs = pairs; local next = next; local setmetatable = setmetatable; +local xpcall = xpcall; local json = require "util.json"; +local build_url = require"socket.url".build; -local connection = ...; +local DBI; +local connection; local host,user,store = module.host; local params = module:get_option("sql"); -do -- process options to get a db connection - local DBI = require "DBI"; +local dburi; +local connections = module:shared "/*/sql/connection-cache"; + +local function db2uri(params) + return build_url{ + scheme = params.driver, + user = params.username, + password = params.password, + host = params.host, + port = params.port, + path = params.database, + }; +end + + +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; + connections[dburi] = 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; + + connections[dburi] = dbh; + end + return connection; +end + +local function create_table() + if not module:get_option("sql_manage_tables", true) then + return; + end + 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, err = 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 + elseif params.driver == "MySQL" then -- 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 + module:log("info", "Upgrading database schema..."); + local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT"); + local ok, err = stmt:execute(); + local commit_ok = connection:commit(); + if ok and commit_ok then + module:log("info", "Database table automatically upgraded"); + else + module:log("error", "Failed to upgrade database schema (%s), please see " + .."http://prosody.im/doc/mysql for help", + err or "unknown error"); + end + end + repeat until not stmt:fetch(); + end + end + elseif params.driver ~= "SQLite3" then -- SQLite normally fails to prepare for existing table + module:log("warn", "Prosody was not able to automatically check/create the database table (%s), " + .."see http://prosody.im/doc/modules/mod_storage_sql#table_management for help.", + err or "unknown error"); + end +end - params = params or { driver = "SQLite3", database = "prosody.sqlite" }; - assert(params.driver and params.database, "invalid params"); - +do -- process options to get a db connection + local ok; prosody.unlock_globals(); - local dbh, err = DBI.Connect( - params.driver, params.database, - params.username, params.password, - params.host, params.port - ); + 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(); - assert(dbh, err); - - dbh:autocommit(false); -- don't commit automatically - connection = dbh; - - if params.driver == "SQLite3" then -- auto initialize - local stmt = assert(connection:prepare("SELECT COUNT(*) FROM `sqlite_master` WHERE `type`='table' AND `name`='Prosody';")); - local ok = assert(stmt:execute()); - local count = stmt:fetch()[1]; - if count == 0 then - local stmt = assert(connection:prepare("CREATE TABLE `Prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);")); - assert(stmt:execute()); - module:log("debug", "Initialized new SQLite3 database"); - end - assert(connection:commit()); - --print("===", json.encode()) + 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"); + + dburi = db2uri(params); + connection = connections[dburi]; + + assert(connect()); + + -- Automatically create table, ignore failure (table probably already exists) + create_table(); end local function serialize(value) @@ -85,19 +197,24 @@ local function deserialize(t, value) end end -local function getsql(sql, ...) +local function dosql(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 "", ...); + local ok, err = stmt:execute(...); + 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 getsql(sql, ...) + return dosql(sql, host or "", user or "", store or "", ...); +end local function setsql(sql, ...) local stmt, err = getsql(sql, ...); if not stmt then return stmt, err; end @@ -107,21 +224,19 @@ local function transact(...) -- ... end local function rollback(...) - connection:rollback(); -- FIXME check for rollback error? + 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 + local success,err = connection:commit(); + if not success then return nil, "SQL commit failed: "..tostring(err); end return ...; end -local keyval_store = {}; -keyval_store.__index = keyval_store; -function keyval_store:get(username) - user,store = username,self.store; - local stmt, err = getsql("SELECT * FROM `Prosody` WHERE `host`=? AND `user`=? AND `store`=?"); - if not stmt then return nil, err; 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 @@ -138,18 +253,17 @@ function keyval_store:get(username) end return commit(haveany and result or nil); end -function keyval_store:set(username, data) - user,store = username,self.store; - -- start transaction - local affected, err = setsql("DELETE FROM `Prosody` WHERE `host`=? AND `user`=? AND `store`=?"); - +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); + 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; @@ -158,20 +272,49 @@ function keyval_store:set(username, data) 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); + 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 map_store = {}; -map_store.__index = map_store; -function map_store:get(username, key) +local keyval_store = {}; +keyval_store.__index = keyval_store; +function keyval_store:get(username) user,store = username,self.store; - local stmt, err = getsql("SELECT * FROM `Prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); - if not stmt then return nil, err; end - + 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 +function keyval_store:users() + local stmt, err = dosql("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store); + if not stmt then + return rollback(nil, err); + end + local next = stmt:rows(); + return commit(function() + local row = next(); + return row and row[1]; + 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 @@ -188,16 +331,15 @@ function map_store:get(username, key) end return commit(haveany and result[key] or nil); end -function map_store:set(username, key, data) - user,store = username,self.store; - -- start transaction - local affected, err = setsql("DELETE FROM `Prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or ""); - +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); + 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 @@ -206,23 +348,36 @@ function map_store:set(username, key, data) 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`=?"; - + 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 ""); - + --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" }; +local driver = {}; function driver:open(store, typ) if not typ then -- default key-value store @@ -231,4 +386,29 @@ function driver:open(store, typ) return nil, "unsupported-store"; end -module:add_item("data-driver", driver); +function driver:stores(username) + local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" .. + (username == true and "!=?" or "=?"); + if username == true or not username then + username = ""; + end + local stmt, err = dosql(sql, host, username); + if not stmt then + return rollback(nil, err); + end + local next = stmt:rows(); + return commit(function() + local row = next(); + return row and row[1]; + end); +end + +function driver:purge(username) + local stmt, err = dosql("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username); + if not stmt then return rollback(stmt, err); end + local changed, err = stmt:affected(); + if not changed then return rollback(changed, err); end + return commit(true, changed); +end + +module:provides("storage", driver); diff --git a/plugins/mod_storage_sql2.lua b/plugins/mod_storage_sql2.lua new file mode 100644 index 00000000..3c5f9d20 --- /dev/null +++ b/plugins/mod_storage_sql2.lua @@ -0,0 +1,370 @@ + +local json = require "util.json"; +local xml_parse = require "util.xml".parse; +local uuid = require "util.uuid"; +local resolve_relative_path = require "core.configmanager".resolve_relative_path; + +local stanza_mt = require"util.stanza".stanza_mt; +local getmetatable = getmetatable; +local t_concat = table.concat; +local function is_stanza(x) return getmetatable(x) == stanza_mt; end + +local noop = function() end +local unpack = unpack +local function iterator(result) + return function(result) + local row = result(); + if row ~= nil then + return unpack(row); + end + end, result, nil; +end + +local mod_sql = module:require("sql"); +local params = module:get_option("sql"); + +local engine; -- TODO create engine + +local function create_table() + local Table,Column,Index = mod_sql.Table,mod_sql.Column,mod_sql.Index; + --[[ + local ProsodyTable = Table { + name="prosody"; + Column { name="host", type="TEXT", nullable=false }; + Column { name="user", type="TEXT", nullable=false }; + Column { name="store", type="TEXT", nullable=false }; + Column { name="key", type="TEXT", nullable=false }; + Column { name="type", type="TEXT", nullable=false }; + Column { name="value", type="TEXT", nullable=false }; + Index { name="prosody_index", "host", "user", "store", "key" }; + }; + engine:transaction(function() + ProsodyTable:create(engine); + end);]] + if not module:get_option("sql_manage_tables", true) then + return; + end + + 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") + :gsub(";$", " CHARACTER SET 'utf8' COLLATE 'utf8_bin';"); + end + + 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 success,err = engine:transaction(function() + engine:execute(create_sql); + engine:execute(index_sql); + end); + if not success then -- so we failed to create + if params.driver == "MySQL" then + success,err = engine:transaction(function() + local result = engine:execute("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'"); + if result:rowcount() > 0 then + module:log("info", "Upgrading database schema..."); + engine:execute("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT"); + module:log("info", "Database table automatically upgraded"); + end + return true; + end); + if not success then + module:log("error", "Failed to check/upgrade database schema (%s), please see " + .."http://prosody.im/doc/mysql for help", + err or "unknown error"); + end + end + end + local ProsodyArchiveTable = Table { + name="prosodyarchive"; + Column { name="sort_id", type="INTEGER PRIMARY KEY AUTOINCREMENT", nullable=false }; + Column { name="host", type="TEXT", nullable=false }; + Column { name="user", type="TEXT", nullable=false }; + Column { name="store", type="TEXT", nullable=false }; + Column { name="key", type="TEXT", nullable=false }; -- item id + Column { name="when", type="INTEGER", nullable=false }; -- timestamp + Column { name="with", type="TEXT", nullable=false }; -- related id + Column { name="type", type="TEXT", nullable=false }; + Column { name="value", type=params.driver == "MySQL" and "MEDIUMTEXT" or "TEXT", nullable=false }; + Index { name="prosodyarchive_index", "host", "user", "store", "key" }; + }; + engine:transaction(function() + ProsodyArchiveTable:create(engine); + end); +end +local function set_encoding() + if params.driver == "SQLite3" then return end + local set_names_query = "SET NAMES 'utf8';"; + if params.driver == "MySQL" then + set_names_query = set_names_query:gsub(";$", " COLLATE 'utf8_bin';"); + end + local success,err = engine:transaction(function() return engine:execute(set_names_query); end); + if not success then + module:log("error", "Failed to set database connection encoding to UTF8: %s", err); + return; + end + if params.driver == "MySQL" then + -- COMPAT w/pre-0.9: Upgrade tables to UTF-8 if not already + local check_encoding_query = "SELECT `COLUMN_NAME`,`COLUMN_TYPE` FROM `information_schema`.`columns` WHERE `TABLE_NAME`='prosody' AND ( `CHARACTER_SET_NAME`!='utf8' OR `COLLATION_NAME`!='utf8_bin' );"; + local success,err = engine:transaction(function() + local result = engine:execute(check_encoding_query); + local n_bad_columns = result:rowcount(); + if n_bad_columns > 0 then + module:log("warn", "Found %d columns in prosody table requiring encoding change, updating now...", n_bad_columns); + local fix_column_query1 = "ALTER TABLE `prosody` CHANGE `%s` `%s` BLOB;"; + local fix_column_query2 = "ALTER TABLE `prosody` CHANGE `%s` `%s` %s CHARACTER SET 'utf8' COLLATE 'utf8_bin';"; + for row in result:rows() do + local column_name, column_type = unpack(row); + engine:execute(fix_column_query1:format(column_name, column_name)); + engine:execute(fix_column_query2:format(column_name, column_name, column_type)); + end + module:log("info", "Database encoding upgrade complete!"); + end + end); + local success,err = engine:transaction(function() return engine:execute(check_encoding_query); end); + if not success then + module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error"); + end + end +end + +do -- process options to get a db connection + 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"); + + --local dburi = db2uri(params); + engine = mod_sql:create_engine(params); + + -- Encoding mess + set_encoding(); + + -- 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 is_stanza(value) then + return "xml", 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); + elseif t == "xml" then + return xml_parse(value); + end +end + +local host = module.host; +local user, store; + +local function keyval_store_get() + local haveany; + local result = {}; + for row in engine:select("SELECT `key`,`type`,`value` FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user or "", store) do + haveany = true; + local k = row[1]; + local v = deserialize(row[2], row[3]); + 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 + if haveany then + return result; + end +end +local function keyval_store_set(data) + engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user or "", store); + + 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); + assert(t, value); + engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user or "", store, key, t, value); + else + extradata[key] = value; + end + end + if next(extradata) ~= nil then + local t, extradata = serialize(extradata); + assert(t, extradata); + engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user or "", store, "", t, extradata); + end + end + return true; +end + +local keyval_store = {}; +keyval_store.__index = keyval_store; +function keyval_store:get(username) + user,store = username,self.store; + return select(2, engine:transaction(keyval_store_get)); +end +function keyval_store:set(username, data) + user,store = username,self.store; + return engine:transaction(function() + return keyval_store_set(data); + end); +end +function keyval_store:users() + local ok, result = engine:transaction(function() + return engine:select("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store); + end); + if not ok then return ok, result end + return iterator(result); +end + +local archive_store = {} +archive_store.__index = archive_store +function archive_store:append(username, when, with, value) + local user,store = username,self.store; + return engine:transaction(function() + local key = uuid.generate(); + local t, value = serialize(value); + engine:insert("INSERT INTO `prosodyarchive` (`host`, `user`, `store`, `when`, `with`, `key`, `type`, `value`) VALUES (?,?,?,?,?,?,?,?)", host, user or "", store, when, with, key, t, value); + return key; + end); +end +function archive_store:find(username, query) + query = query or {}; + local user,store = username,self.store; + local total; + local ok, result = engine:transaction(function() + local sql_query = "SELECT `key`, `type`, `value`, `when` FROM `prosodyarchive` WHERE %s ORDER BY `sort_id` %s%s;"; + local args = { host, user or "", store, }; + local where = { "`host` = ?", "`user` = ?", "`store` = ?", }; + + -- Time range, inclusive + if query.start then + args[#args+1] = query.start + where[#where+1] = "`when` >= ?" + end + if query["end"] then + args[#args+1] = query["end"]; + if query.start then + where[#where] = "`when` BETWEEN ? AND ?" -- is this inclusive? + else + where[#where+1] = "`when` >= ?" + end + end + + -- Related name + if query.with then + where[#where+1] = "`with` = ?"; + args[#args+1] = query.with + end + + -- Unique id + if query.key then + where[#where+1] = "`key` = ?"; + args[#args+1] = query.key + end + + -- Total matching + if query.total then + local stats = engine:select(sql_query:gsub("^(SELECT).-(FROM)", "%1 COUNT(*) %2"):format(t_concat(where, " AND "), "DESC", ""), unpack(args)); + if stats then + local _total = stats() + total = _total and _total[1]; + end + if query.limit == 0 then -- Skip the real query + return noop, total; + end + end + + -- Before or after specific item, exclusive + if query.after then + where[#where+1] = "`sort_id` > (SELECT `sort_id` FROM `prosodyarchive` WHERE `key` = ? LIMIT 1)" + args[#args+1] = query.after + end + if query.before then + where[#where+1] = "`sort_id` < (SELECT `sort_id` FROM `prosodyarchive` WHERE `key` = ? LIMIT 1)" + args[#args+1] = query.before + end + + if query.limit then + args[#args+1] = query.limit; + end + + sql_query = sql_query:format(t_concat(where, " AND "), query.reverse and "DESC" or "ASC", query.limit and " LIMIT ?" or ""); + module:log("debug", sql_query); + return engine:select(sql_query, unpack(args)); + end); + if not ok then return ok, result end + return function() + local row = result(); + if row ~= nil then + return row[1], deserialize(row[2], row[3]), row[4]; + end + end, total; +end + +local stores = { + keyval = keyval_store; + archive = archive_store; +}; + +local driver = {}; + +function driver:open(store, typ) + local store_mt = stores[typ or "keyval"]; + if store_mt then + return setmetatable({ store = store }, store_mt); + end + return nil, "unsupported-store"; +end + +function driver:stores(username) + local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" .. + (username == true and "!=?" or "=?"); + if username == true or not username then + username = ""; + end + local ok, result = engine:transaction(function() + return engine:select(sql, host, username); + end); + if not ok then return ok, result end + return iterator(result); +end + +function driver:purge(username) + return engine:transaction(function() + local stmt,err = engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username); + return true,err; + end); +end + +module:provides("storage", driver); + + diff --git a/plugins/mod_storage_sql_ejabberd.lua b/plugins/mod_storage_sql_ejabberd.lua deleted file mode 100644 index 74763c92..00000000 --- a/plugins/mod_storage_sql_ejabberd.lua +++ /dev/null @@ -1,232 +0,0 @@ - -local setmetatable = setmetatable; -local error = error; -local unpack = unpack; -local module = module; -local tostring = tostring; -local pairs, next = pairs, next; -local prosody = prosody; -local assert = assert; -local require = require; -local st = require "util.stanza"; -local DBI = require "DBI"; - --- connect to db -local params = module:get_option("sql_ejabberd") or error("No sql_ejabberd config option"); -local database; -do - module:log("debug", "Opening database: %s", "dbi:"..params.driver..":"..params.database); - prosody.unlock_globals(); - local dbh, err = DBI.Connect( - params.driver, params.database, - params.username, params.password, - params.host, params.port - ); - prosody.lock_globals(); - assert(dbh, err); - dbh:autocommit(true); - database = dbh; -end - --- initialize db -local ejabberd_init = module:require("ejabberd_init"); -ejabberd_init.init(database); - -local sqlcache = {}; -local function prepare(sql) - module:log("debug", "query: %s", sql); - local err; - local r = sqlcache[sql]; - if not r then - r, err = database:prepare(sql); - if not r then error("Unable to prepare SQL statement: "..err); end - sqlcache[sql] = r; - end - return r; -end - -local _parse_xml = module:require("xmlparse"); -local function parse_xml(str) - local s = _parse_xml(str); - if s and not s.gsub then - return st.preserialize(s); - end -end -local function unparse_xml(s) - return tostring(st.deserialize(s)); -end - - -local handlers = {}; - -handlers.accounts = { - get = function(self, user) - local select = self:query("select password from users where username=? and host=?", user, self.host); - local row = select and select:fetch(); - if row then return { password = row[1] }; end - end; - set = function(self, user, data) - if data and data.password then - return self:modify("update users set password=? where username=? and host=?", data.password, user, self.host) - or self:modify("insert into users (username, host, password) values (?, ?, ?)", user, self.host, data.password); - else - return self:modify("delete from users where username=? and host=?", user, self.host); - end - end; -}; -handlers.vcard = { - get = function(self, user) - local select = self:query("select vcard from vcard where username=? and host=?", user, self.host); - local row = select and select:fetch(); - if row then return parse_xml(row[1]); end - end; - set = function(self, user, data) - if data then - data = unparse_xml(data); - return self:modify("update vcard set vcard=? where username=? and host=?", data, user, self.host) - or self:modify("insert into vcard (username, host, vcard) values (?, ?, ?)", user, self.host, data); - else - return self:modify("delete from vcard where username=? and host=?", user, self.host); - end - end; -}; -handlers.private = { - get = function(self, user) - local select = self:query("select namespace,data from private_storage where username=? and host=?", user, self.host); - if select then - local data = {}; - for row in select:rows() do - data[row[1]] = parse_xml(row[2]); - end - return data; - end - end; - set = function(self, user, data) - if data then - self:modify("delete from private_storage where username=? and host=?", user, self.host); - for namespace,text in pairs(data) do - self:modify("insert into private_storage (username, host, namespace, data) values (?, ?, ?, ?)", user, self.host, namespace, unparse_xml(text)); - end - return true; - else - return self:modify("delete from private_storage where username=? and host=?", user, self.host); - end - end; - -- TODO map_set, map_get -}; -local subscription_map = { N = "none", B = "both", F = "from", T = "to" }; -local subscription_map_reverse = { none = "N", both = "B", from = "F", to = "T" }; -handlers.roster = { - get = function(self, user) - local select = self:query("select jid,nick,subscription,ask,server,subscribe,type from rosterusers where username=?", user); - if select then - local roster = { pending = {} }; - for row in select:rows() do - local jid,nick,subscription,ask,server,subscribe,typ = unpack(row); - local item = { groups = {} }; - if nick == "" then nick = nil; end - item.nick = nick; - item.subscription = subscription_map[subscription]; - if ask == "N" then ask = nil; - elseif ask == "O" then ask = "subscribe" - elseif ask == "I" then roster.pending[jid] = true; ask = nil; - elseif ask == "B" then roster.pending[jid] = true; ask = "subscribe"; - else module:log("debug", "bad roster_item.ask: %s", ask); ask = nil; end - item.ask = ask; - roster[jid] = item; - end - - select = self:query("select jid,grp from rostergroups where username=?", user); - if select then - for row in select:rows() do - local jid,grp = unpack(row); - if roster[jid] then roster[jid].groups[grp] = true; end - end - end - select = self:query("select version from roster_version where username=?", user); - local row = select and select:fetch(); - if row then - roster[false] = { version = row[1]; }; - end - return roster; - end - end; - set = function(self, user, data) - if data and next(data) ~= nil then - self:modify("delete from rosterusers where username=?", user); - self:modify("delete from rostergroups where username=?", user); - self:modify("delete from roster_version where username=?", user); - local done = {}; - local pending = data.pending or {}; - for jid,item in pairs(data) do - if jid and jid ~= "pending" then - local subscription = subscription_map_reverse[item.subscription]; - local ask; - if pending[jid] then - if item.ask then ask = "B"; else ask = "I"; end - else - if item.ask then ask = "O"; else ask = "N"; end - end - local r = self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?, '', '', '')", user, jid, item.nick or "", subscription, ask); - if not r then module:log("debug", "--- :( %s", tostring(r)); end - done[jid] = true; - for group in pairs(item.groups) do - self:modify("insert into rostergroups (username,jid,grp) values (?, ?, ?)", user, jid, group); - end - end - end - for jid in pairs(pending) do - if not done[jid] then - self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?. ''. ''. '')", user, jid, "", "N", "I"); - end - end - local version = data[false] and data[false].version; - if version then - self:modify("insert into roster_version (username,version) values (?, ?)", user, version); - end - return true; - else - self:modify("delete from rosterusers where username=?", user); - self:modify("delete from rostergroups where username=?", user); - self:modify("delete from roster_version where username=?", user); - end - end; -}; - ------------------------------ -local driver = {}; -driver.__index = driver; - -function driver:query(sql, ...) - local stmt,err = prepare(sql); - if not stmt then - module:log("error", "Failed to prepare SQL [[%s]], error: %s", sql, err); - return nil, err; - end - local ok, err = stmt:execute(...); - if not ok then - module:log("error", "Failed to execute SQL [[%s]], error: %s", sql, err); - return nil, err; - end - return stmt; -end -function driver:modify(sql, ...) - local stmt, err = self:query(sql, ...); - if stmt and stmt:affected() > 0 then return stmt; end - return nil, err; -end - -function driver:open(datastore, typ) - local instance = setmetatable({ host = module.host, datastore = datastore }, self); - 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/mod_time.lua b/plugins/mod_time.lua index cb69ebe7..ae7da916 100644 --- a/plugins/mod_time.lua +++ b/plugins/mod_time.lua @@ -1,7 +1,7 @@ -- 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. -- diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index c79227e1..bab2202e 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -23,19 +23,47 @@ local s2s_feature = st.stanza("starttls", starttls_attr); if secure_auth_only then c2s_feature:tag("required"):up(); end if secure_s2s_only then s2s_feature:tag("required"):up(); end -local global_ssl_ctx = prosody.global_ssl_ctx; - +local hosts = prosody.hosts; local host = hosts[module.host]; +local ssl_ctx_c2s, ssl_ctx_s2sout, ssl_ctx_s2sin; +do + local function get_ssl_cfg(typ) + local cfg_key = (typ and typ.."_" or "").."ssl"; + local ssl_config = config.rawget(module.host, cfg_key); + if not ssl_config then + local base_host = module.host:match("%.(.*)"); + ssl_config = config.get(base_host, cfg_key); + end + return ssl_config or typ and get_ssl_cfg(); + end + + local ssl_config, err = get_ssl_cfg("c2s"); + ssl_ctx_c2s, err = create_context(host.host, "server", ssl_config); -- for incoming client connections + if err then module:log("error", "Error creating context for c2s: %s", err); end + + ssl_config = get_ssl_cfg("s2s"); + ssl_ctx_s2sin, err = create_context(host.host, "server", ssl_config); -- for incoming server connections + ssl_ctx_s2sout = create_context(host.host, "client", ssl_config); -- for outgoing server connections + if err then module:log("error", "Error creating context for s2s: %s", err); end -- Both would have the same issue +end + local function can_do_tls(session) + if not session.conn.starttls then + return false; + elseif session.ssl_ctx then + return true; + end if session.type == "c2s_unauthed" then - return session.conn.starttls and host.ssl_ctx_in; + session.ssl_ctx = ssl_ctx_c2s; elseif session.type == "s2sin_unauthed" and allow_s2s_tls then - return session.conn.starttls and host.ssl_ctx_in; + session.ssl_ctx = ssl_ctx_s2sin; elseif session.direction == "outgoing" and allow_s2s_tls then - return session.conn.starttls and host.ssl_ctx; + session.ssl_ctx = ssl_ctx_s2sout; + else + return false; end - return false; + return session.ssl_ctx; end -- Hook <starttls/> @@ -44,10 +72,8 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event) if can_do_tls(origin) then (origin.sends2s or origin.send)(starttls_proceed); origin:reset_stream(); - 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.conn:starttls(origin.ssl_ctx); + 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); @@ -75,7 +101,7 @@ end); module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza) module:log("debug", "Received features element"); if can_do_tls(session) and stanza:child_with_ns(xmlns_starttls) then - module:log("%s is offering TLS, taking up the offer...", session.to_host); + module:log("debug", "%s is offering TLS, taking up the offer...", session.to_host); session.sends2s("<starttls xmlns='"..xmlns_starttls.."'/>"); return true; end @@ -84,24 +110,7 @@ end, 500); 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); + session.conn:starttls(session.ssl_ctx); session.secure = false; return true; end); - -function module.load() - local global_ssl_config = config.get("*", "core", "ssl"); - local ssl_config = config.get(module.host, "core", "ssl"); - local base_host = module.host:match("%.(.*)"); - if ssl_config == global_ssl_config and hosts[base_host] then - 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 52b33c74..2e369b16 100644 --- a/plugins/mod_uptime.lua +++ b/plugins/mod_uptime.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -9,7 +9,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); +module:hook_global("server-started", function() start_time = prosody.start_time end); -- XEP-0012: Last activity module:add_feature("jabber:iq:last"); diff --git a/plugins/mod_vcard.lua b/plugins/mod_vcard.lua index e2f1dfb8..72f92ef7 100644 --- a/plugins/mod_vcard.lua +++ b/plugins/mod_vcard.lua @@ -1,14 +1,15 @@ -- 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 st = require "util.stanza" local jid_split = require "util.jid".split; -local datamanager = require "util.datamanager" + +local vcards = module:open_store(); module:add_feature("vcard-temp"); @@ -19,9 +20,9 @@ local function handle_vcard(event) local vCard; if to then local node, host = jid_split(to); - vCard = st.deserialize(datamanager.load(node, host, "vcard")); -- load vCard for user or server + vCard = st.deserialize(vcards:get(node)); -- load vCard for user or server else - vCard = st.deserialize(datamanager.load(session.username, session.host, "vcard"));-- load user's own vCard + vCard = st.deserialize(vcards:get(session.username));-- load user's own vCard end if vCard then session.send(st.reply(stanza):add_child(vCard)); -- send vCard! @@ -30,7 +31,7 @@ local function handle_vcard(event) end else if not to then - if datamanager.store(session.username, session.host, "vcard", st.preserialize(stanza.tags[1])) then + if vcards:set(session.username, st.preserialize(stanza.tags[1])) then session.send(st.reply(stanza)); else -- TODO unable to write file, file may be locked, etc, what's the correct error? @@ -46,13 +47,8 @@ end module:hook("iq/bare/vcard-temp:vCard", handle_vcard); module:hook("iq/host/vcard-temp:vCard", handle_vcard); --- COMPAT: https://support.process-one.net/browse/EJAB-1045 -if module:get_option("vcard_compatibility") then - module:hook("iq/full", function(data) - local stanza = data.stanza; - local payload = stanza.tags[1]; - if stanza.attr.type == "get" and payload.name == "vCard" and payload.attr.xmlns == "vcard-temp" then - return handle_vcard(data); - end - end, 1); +-- COMPAT w/0.8 +if module:get_option("vcard_compatibility") ~= nil then + module:log("error", "The vcard_compatibility option has been removed, see".. + "mod_compat_vcard in prosody-modules if you still need this."); end diff --git a/plugins/mod_version.lua b/plugins/mod_version.lua index 52d8d290..be244beb 100644 --- a/plugins/mod_version.lua +++ b/plugins/mod_version.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -21,7 +21,7 @@ if not module:get_option("hide_os_type") then version = "Windows"; else local os_version_command = module:get_option("os_version_command"); - local ok pposix = pcall(require, "pposix"); + local ok, pposix = pcall(require, "util.pposix"); if not os_version_command and (ok and pposix and pposix.uname) then version = pposix.uname().sysname; end diff --git a/plugins/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua index ac1e6302..b7be5daf 100644 --- a/plugins/mod_watchregistrations.lua +++ b/plugins/mod_watchregistrations.lua @@ -1,32 +1,30 @@ -- 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 host = module:get_host(); +local jid_prep = require "util.jid".prep; -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"; +local registration_watchers = module:get_option_set("registration_watchers", module:get_option("admins", {})) / jid_prep; +local registration_notification = module:get_option("registration_notification", "User $username just registered on $host from $ip"); local st = require "util.stanza"; -module:hook("user-registered", - function (user) - module:log("debug", "Notifying of new registration"); - local message = st.message{ type = "chat", from = host } - :tag("body") - :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 - module:log("debug", "Notifying %s", jid); - message.attr.to = jid; - core_route_stanza(hosts[host], message); - end - end); +module:hook("user-registered", function (user) + module:log("debug", "Notifying of new registration"); + local message = st.message{ type = "chat", from = host } + :tag("body") + :text(registration_notification:gsub("%$(%w+)", function (v) + return user[v] or user.session and user.session[v] or nil; + end)); + for jid in registration_watchers do + module:log("debug", "Notifying %s", jid); + message.attr.to = jid; + module:send(message); + end +end); diff --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua index 8f9cca2a..9c0c821b 100644 --- a/plugins/mod_welcome.lua +++ b/plugins/mod_welcome.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -16,6 +16,6 @@ module:hook("user-registered", 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); + module:send(welcome_stanza); module:log("debug", "Welcomed user %s@%s", user.username, user.host); end); diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index 329b9270..cb967c90 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -1,11 +1,12 @@ -- 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 array = require "util.array"; if module:get_host_type() ~= "component" then error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0); @@ -16,33 +17,53 @@ 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 then - if restrict_room_creation == true 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 lock_rooms = module:get_option_boolean("muc_room_locking", false); +local lock_room_timeout = module:get_option_number("muc_room_lock_timeout", 300); + +local muclib = module:require "muc"; +local muc_new_room = muclib.new_room; local jid_split = require "util.jid".split; local jid_bare = require "util.jid".bare; local st = require "util.stanza"; local uuid_gen = require "util.uuid".generate; -local datamanager = require "util.datamanager"; local um_is_admin = require "core.usermanager".is_admin; +local hosts = prosody.hosts; rooms = {}; local rooms = rooms; -local persistent_rooms = datamanager.load(nil, muc_host, "persistent") or {}; -local component = hosts[module.host]; +local persistent_rooms_storage = module:open_store("persistent"); +local persistent_rooms = persistent_rooms_storage:get() or {}; +local room_configs = module:open_store("config"); -- Configurable options -local max_history_messages = module:get_option_number("max_history_messages"); +muclib.set_max_history_length(module:get_option_number("max_history_messages")); + +module:depends("disco"); +module:add_identity("conference", "text", muc_name); +module:add_feature("http://jabber.org/protocol/muc"); local function is_admin(jid) return um_is_admin(jid, module.host); end -local function room_route_stanza(room, stanza) core_post_stanza(component, stanza); end +local _set_affiliation = muc_new_room.room_mt.set_affiliation; +local _get_affiliation = muc_new_room.room_mt.get_affiliation; +function muclib.room_mt:get_affiliation(jid) + if is_admin(jid) then return "owner"; end + return _get_affiliation(self, jid); +end +function muclib.room_mt:set_affiliation(actor, jid, affiliation, callback, reason) + if is_admin(jid) then return nil, "modify", "not-acceptable"; end + return _set_affiliation(self, actor, jid, affiliation, callback, reason); +end + +local function room_route_stanza(room, stanza) module:send(stanza); end local function room_save(room, forced) local node = jid_split(room.jid); persistent_rooms[room.jid] = room._data.persistent; @@ -54,59 +75,74 @@ local function room_save(room, forced) _data = room._data; _affiliations = room._affiliations; }; - datamanager.store(node, muc_host, "config", data); + room_configs:set(node, data); room._data.history = history; elseif forced then - datamanager.store(node, muc_host, "config", nil); + room_configs:set(node, 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 + if forced then persistent_rooms_storage:set(nil, persistent_rooms); end end -for jid in pairs(persistent_rooms) do - local node = jid_split(jid); - local data = datamanager.load(node, muc_host, "config") or {}; - local room = muc_new_room(jid, { - 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; +function create_room(jid) + local room = muc_new_room(jid); room.route_stanza = room_route_stanza; room.save = room_save; rooms[jid] = room; + if lock_rooms then + room.locked = true; + if lock_room_timeout and lock_room_timeout > 0 then + module:add_timer(lock_room_timeout, function () + if room.locked then + room:destroy(); -- Not unlocked in time + end + end); + end + end + module:fire_event("muc-room-created", { room = room }); + return room; +end + +local persistent_errors = false; +for jid in pairs(persistent_rooms) do + local node = jid_split(jid); + local data = room_configs:get(node); + if data then + local room = create_room(jid); + room._data = data._data; + room._affiliations = data._affiliations; + else -- missing room data + persistent_rooms[jid] = nil; + module:log("error", "Missing data for room '%s', removing from persistent room list", jid); + persistent_errors = true; + end end +if persistent_errors then persistent_rooms_storage:set(nil, persistent_rooms); end -local host_room = muc_new_room(muc_host, { - history_length = max_history_messages; -}); +local host_room = muc_new_room(muc_host); host_room.route_stanza = room_route_stanza; host_room.save = room_save; -local function get_disco_info(stanza) - return st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info") - :tag("identity", {category='conference', type='text', name=muc_name}):up() - :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply -end -local function get_disco_items(stanza) - local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); +module:hook("host-disco-items", function(event) + local reply = event.reply; + module:log("debug", "host-disco-items called"); for jid, room in pairs(rooms) do - if not room:is_hidden() then + if not room:get_hidden() then reply:tag("item", {jid=jid, name=room:get_name()}):up(); end end - return reply; -- TODO cache disco reply -end +end); -local function handle_to_domain(origin, stanza) +local function handle_to_domain(event) + local origin, stanza = event.origin, event.stanza; local type = stanza.attr.type; if type == "error" or type == "result" then return; end if stanza.name == "iq" and type == "get" then local xmlns = stanza.tags[1].attr.xmlns; - if xmlns == "http://jabber.org/protocol/disco#info" then - origin.send(get_disco_info(stanza)); - elseif xmlns == "http://jabber.org/protocol/disco#items" then - origin.send(get_disco_items(stanza)); - elseif xmlns == "http://jabber.org/protocol/muc#unique" then + local node = stanza.tags[1].attr.node; + if xmlns == "http://jabber.org/protocol/muc#unique" then origin.send(st.reply(stanza):tag("unique", {xmlns = xmlns}):text(uuid_gen())); -- FIXME Random UUIDs can theoretically have collisions else origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc @@ -115,40 +151,32 @@ local function handle_to_domain(origin, stanza) host_room:handle_stanza(origin, stanza); --origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it")); end + return true; end 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 - (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; - end - end - if room then - room:handle_stanza(origin, stanza); - if not next(room._occupants) and not persistent_rooms[room.jid] then -- empty, non-persistent room - rooms[bare] = nil; -- discard room - end - else - origin.send(st.error_reply(stanza, "cancel", "not-allowed")); - end - else --[[not for us?]] end - return true; + local bare = jid_bare(stanza.attr.to); + local room = rooms[bare]; + if not room then + if stanza.name ~= "presence" then + origin.send(st.error_reply(stanza, "cancel", "item-not-found")); + return true; + end + 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 = create_room(bare); + end + end + if room then + room:handle_stanza(origin, stanza); + if not next(room._occupants) and not persistent_rooms[room.jid] then -- empty, non-persistent room + rooms[bare] = nil; -- discard room + end + else + origin.send(st.error_reply(stanza, "cancel", "not-allowed")); end - -- to the main muc domain - handle_to_domain(origin, stanza); return true; end module:hook("iq/bare", stanza_handler, -1); @@ -157,31 +185,92 @@ 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); +module:hook("iq/host", handle_to_domain, -1); +module:hook("message/host", handle_to_domain, -1); +module:hook("presence/host", handle_to_domain, -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); + module:send(stanza); else error("component.send only supports result and error stanzas at the moment"); end end -prosody.hosts[module:get_host()].muc = { rooms = rooms }; +hosts[module:get_host()].muc = { rooms = rooms }; +local saved = false; module.save = function() + saved = true; return {rooms = rooms}; end module.restore = function(data) for jid, oldroom in pairs(data.rooms or {}) do - local room = muc_new_room(jid); + local room = create_room(jid); room._jid_nick = oldroom._jid_nick; room._occupants = oldroom._occupants; room._data = oldroom._data; room._affiliations = oldroom._affiliations; - room.route_stanza = room_route_stanza; - room.save = room_save; - rooms[jid] = room; end - prosody.hosts[module:get_host()].muc = { rooms = rooms }; + hosts[module:get_host()].muc = { rooms = rooms }; end + +function shutdown_room(room, stanza) + for nick, occupant in pairs(room._occupants) do + stanza.attr.from = nick; + for jid in pairs(occupant.sessions) do + stanza.attr.to = jid; + room:_route_stanza(stanza); + room._jid_nick[jid] = nil; + end + room._occupants[nick] = nil; + end +end +function shutdown_component() + if not saved then + local stanza = st.presence({type = "unavailable"}) + :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) + :tag("item", { affiliation='none', role='none' }):up() + :tag("status", { code = "332"}):up(); + for roomjid, room in pairs(rooms) do + shutdown_room(room, stanza); + end + shutdown_room(host_room, stanza); + end +end +module.unload = shutdown_component; +module:hook_global("server-stopping", shutdown_component); + +-- Ad-hoc commands +module:depends("adhoc") +local t_concat = table.concat; +local keys = require "util.iterators".keys; +local adhoc_new = module:require "adhoc".new; +local adhoc_initial = require "util.adhoc".new_initial_data_form; +local dataforms_new = require "util.dataforms".new; + +local destroy_rooms_layout = dataforms_new { + title = "Destroy rooms"; + instructions = "Select the rooms to destroy"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#destroy" }; + { name = "rooms", type = "list-multi", required = true, label = "Rooms to destroy:"}; +}; + +local destroy_rooms_handler = adhoc_initial(destroy_rooms_layout, function() + return { rooms = array.collect(keys(rooms)):sort() }; +end, function(fields, errors) + if errors then + local errmsg = {}; + for name, err in pairs(errors) do + errmsg[#errmsg + 1] = name .. ": " .. err; + end + return { status = "completed", error = { message = t_concat(errmsg, "\n") } }; + end + for _, room in ipairs(fields.rooms) do + rooms[room]:destroy(); + rooms[room] = nil; + end + return { status = "completed", info = "The following rooms were destroyed:\n"..t_concat(fields.rooms, "\n") }; +end); +local destroy_rooms_desc = adhoc_new("Destroy Rooms", "http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin"); + +module:provides("adhoc", destroy_rooms_desc); diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index f3e2dd52..0565d692 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -9,7 +9,6 @@ local select = select; local pairs, ipairs = pairs, ipairs; -local datamanager = require "util.datamanager"; local datetime = require "util.datetime"; local dataform = require "util.dataforms"; @@ -19,38 +18,25 @@ local jid_bare = require "util.jid".bare; local jid_prep = require "util.jid".prep; local st = require "util.stanza"; local log = require "util.logger".init("mod_muc"); -local multitable_new = require "util.multitable".new; local t_insert, t_remove = table.insert, table.remove; local setmetatable = setmetatable; local base64 = require "util.encodings".base64; local md5 = require "util.hashes".md5; local muc_domain = nil; --module:get_host(); -local default_history_length = 20; +local default_history_length, max_history_length = 20, math.huge; ------------ -local function filter_xmlns_from_array(array, filters) - local count = 0; - for i=#array,1,-1 do - local attr = array[i].attr; - if filters[attr and attr.xmlns] then - t_remove(array, i); - count = count + 1; - end - end - return count; -end -local function filter_xmlns_from_stanza(stanza, filters) - if filters then - if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then - return stanza, filter_xmlns_from_array(stanza, filters); - end +local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; +local function presence_filter(tag) + if presence_filters[tag.attr.xmlns] then + return nil; end - return stanza, 0; + return tag; end -local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true}; + local function get_filtered_presence(stanza) - return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters); + return st.clone(stanza):maptags(presence_filter); end local kickable_error_conditions = { ["gone"] = true; @@ -74,30 +60,23 @@ local function is_kickable_error(stanza) local cond = get_error_condition(stanza); return kickable_error_conditions[cond] and cond; end -local function getUsingPath(stanza, path, getText) - local tag = stanza; - for _, name in ipairs(path) do - if type(tag) ~= 'table' then return; end - tag = tag:child_with_name(name); - end - if tag and getText then tag = table.concat(tag); end - return tag; -end -local function getTag(stanza, path) return getUsingPath(stanza, path); end -local function getText(stanza, path) return getUsingPath(stanza, path, true); end ----------- local room_mt = {}; room_mt.__index = room_mt; +function room_mt:__tostring() + return "MUC room ("..self.jid..")"; +end + function room_mt:get_default_role(affiliation) if affiliation == "owner" or affiliation == "admin" then return "moderator"; 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"; + if not self:get_members_only() then + return self:get_moderated() and "visitor" or "participant"; end end end @@ -133,7 +112,6 @@ function room_mt:broadcast_message(stanza, historic) stanza = st.clone(stanza); 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) local entry = { stanza = stanza, stamp = stamp }; @@ -169,10 +147,10 @@ function room_mt:send_history(to, stanza) if history then 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 @@ -185,8 +163,7 @@ function room_mt:send_history(to, stanza) local n = 0; local charcount = 0; - local stanzacount = 0; - + for i=#history,1,-1 do local entry = history[i]; if maxchars then @@ -213,18 +190,20 @@ function room_mt:send_history(to, stanza) end function room_mt:get_disco_info(stanza) + local count = 0; for _ in pairs(self._occupants) do count = count + 1; end return st.reply(stanza):query("http://jabber.org/protocol/disco#info") :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:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up() + :tag("feature", {var=self:get_members_only() and "muc_membersonly" or "muc_open"}):up() + :tag("feature", {var=self:get_persistent() and "muc_persistent" or "muc_temporary"}):up() + :tag("feature", {var=self:get_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"} + { name = "muc#roominfo_description", label = "Description"}, + { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) } }):form({["muc#roominfo_description"] = self:get_description()}, 'result')) ; end @@ -236,7 +215,6 @@ function room_mt:get_disco_items(stanza) return reply; end function room_mt:set_subject(current_nick, subject) - -- TODO check nick's authority if subject == "" then subject = nil; end self._data['subject'] = subject; self._data['subject_from'] = current_nick; @@ -294,7 +272,7 @@ function room_mt:set_moderated(moderated) if self.save then self:save(true); end end end -function room_mt:is_moderated() +function room_mt:get_moderated() return self._data.moderated; end function room_mt:set_members_only(members_only) @@ -304,7 +282,7 @@ function room_mt:set_members_only(members_only) if self.save then self:save(true); end end end -function room_mt:is_members_only() +function room_mt:get_members_only() return self._data.members_only; end function room_mt:set_persistent(persistent) @@ -314,7 +292,7 @@ function room_mt:set_persistent(persistent) if self.save then self:save(true); end end end -function room_mt:is_persistent() +function room_mt:get_persistent() return self._data.persistent; end function room_mt:set_hidden(hidden) @@ -324,9 +302,74 @@ function room_mt:set_hidden(hidden) if self.save then self:save(true); end end end -function room_mt:is_hidden() +function room_mt:get_hidden() return self._data.hidden; end +function room_mt:get_public() + return not self:get_hidden(); +end +function room_mt:set_public(public) + return self:set_hidden(not public); +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:get_historylength() + return self._data.history_length or default_history_length; +end +function room_mt:set_historylength(length) + length = math.min(tonumber(length) or default_history_length, max_history_length or math.huge); + if length == default_history_length then + length = nil; + end + self._data.history_length = length; +end + + +local valid_whois = { moderators = true, anyone = true }; + +function room_mt:set_whois(whois) + if valid_whois[whois] and self._data.whois ~= whois then + self._data.whois = whois; + if self.save then self:save(true); end + end +end + +function room_mt:get_whois() + return self._data.whois; +end + +local function construct_stanza_id(room, stanza) + local from_jid, to_nick = stanza.attr.from, stanza.attr.to; + local from_nick = room._jid_nick[from_jid]; + local occupant = room._occupants[to_nick]; + local to_jid = occupant.jid; + + return from_nick, to_jid, base64.encode(to_jid.."\0"..stanza.attr.id.."\0"..md5(from_jid)); +end +local function deconstruct_stanza_id(room, stanza) + local from_jid_possiblybare, to_nick = stanza.attr.from, stanza.attr.to; + local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(.+)%z(.*)%z(.+)$"); + local from_nick = room._jid_nick[from_jid]; + + if not(from_nick) then return; end + if not(from_jid_possiblybare == from_jid or from_jid_possiblybare == jid_bare(from_jid)) then return; end + + local occupant = room._occupants[to_nick]; + for to_jid in pairs(occupant and occupant.sessions or {}) do + if md5(to_jid) == to_jid_hash then + return from_nick, to_jid, id; + end + end +end + function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc local from, to = stanza.attr.from, stanza.attr.to; @@ -346,6 +389,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc elseif type == "unavailable" then -- unavailable if current_nick then log("debug", "%s leaving %s", current_nick, room); + self._jid_nick[from] = nil; local occupant = self._occupants[current_nick]; local new_jid = next(occupant.sessions); if new_jid == from then new_jid = next(occupant.sessions, new_jid); end @@ -356,7 +400,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]) @@ -370,7 +414,6 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc self:broadcast_presence(pr, from); self._occupants[current_nick] = nil; end - self._jid_nick[from] = nil; end elseif not type then -- available if current_nick then @@ -437,6 +480,12 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc log("debug", "%s joining as %s", from, to); if not next(self._affiliations) then -- new room, no owners self._affiliations[jid_bare(from)] = "owner"; + if self.locked and not stanza:get_child("x", "http://jabber.org/protocol/muc") then + self.locked = nil; -- Older groupchat protocol doesn't lock + end + elseif self.locked then -- Deny entry + origin.send(st.error_reply(stanza, "cancel", "item-not-found")); + return; end local affiliation = self:get_affiliation(from); local role = self:get_default_role(affiliation) @@ -454,10 +503,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc if not is_merge then self:broadcast_except_nick(pr, to); end - pr:tag("status", {code='110'}); + pr:tag("status", {code='110'}):up(); if self._data.whois == 'anyone' then pr:tag("status", {code='100'}):up(); end + if self.locked then + pr:tag("status", {code='201'}):up(); + end pr.attr.to = from; self:_route_stanza(pr); self:send_history(from, stanza); @@ -478,25 +530,14 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc end end elseif not current_nick then -- not in room - if type == "error" or type == "result" then - local id = stanza.name == "iq" and stanza.attr.id and base64.decode(stanza.attr.id); - local _nick, _id, _hash = (id or ""):match("^(.+)%z(.*)%z(.+)$"); - local occupant = self._occupants[stanza.attr.to]; - if occupant and _nick and self._jid_nick[_nick] and _id and _hash then - local id, _to = stanza.attr.id; - for jid in pairs(occupant.sessions) do - if md5(jid) == _hash then - _to = jid; - break; - end - end - if _to then - stanza.attr.to, stanza.attr.from, stanza.attr.id = _to, self._jid_nick[_nick], _id; - self:_route_stanza(stanza); - stanza.attr.to, stanza.attr.from, stanza.attr.id = to, from, id; - end + if (type == "error" or type == "result") and stanza.name == "iq" then + local id = stanza.attr.id; + stanza.attr.from, stanza.attr.to, stanza.attr.id = deconstruct_stanza_id(self, stanza); + if stanza.attr.id then + self:_route_stanza(stanza); end - else + stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; + elseif type ~= "error" then origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); end elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM @@ -508,16 +549,28 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc local o_data = self._occupants[to]; if o_data then log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid); - local jid = o_data.jid; - local bare = jid_bare(jid); - stanza.attr.to, stanza.attr.from = jid, current_nick; - local id = stanza.attr.id; - if stanza.name=='iq' and type=='get' and stanza.tags[1].attr.xmlns == 'vcard-temp' and bare ~= jid then - stanza.attr.to = bare; - stanza.attr.id = base64.encode(jid.."\0"..id.."\0"..md5(from)); + if stanza.name == "iq" then + local id = stanza.attr.id; + if stanza.attr.type == "get" or stanza.attr.type == "set" then + stanza.attr.from, stanza.attr.to, stanza.attr.id = construct_stanza_id(self, stanza); + else + stanza.attr.from, stanza.attr.to, stanza.attr.id = deconstruct_stanza_id(self, stanza); + end + if type == 'get' and stanza.tags[1].attr.xmlns == 'vcard-temp' then + stanza.attr.to = jid_bare(stanza.attr.to); + end + if stanza.attr.id then + self:_route_stanza(stanza); + end + stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id; + else -- message + stanza.attr.from = current_nick; + for jid in pairs(o_data.sessions) do + stanza.attr.to = jid; + self:_route_stanza(stanza); + end + stanza.attr.from, stanza.attr.to = from, to; end - self:_route_stanza(stanza); - stanza.attr.to, stanza.attr.from, stanza.attr.id = to, from, id; elseif type ~= "error" and type ~= "result" then -- recipient not in room origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room")); end @@ -526,15 +579,14 @@ end function room_mt:send_form(origin, stanza) origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") - :add_child(self:get_form_layout():form()) + :add_child(self:get_form_layout(stanza.attr.from):form()) ); end -function room_mt:get_form_layout() - local title = "Configuration for "..self.jid; - return dataform.new({ - title = title, - instructions = title, +function room_mt:get_form_layout(actor) + local form = dataform.new({ + title = "Configuration for "..self.jid, + instructions = "Complete and submit this form to configure the room.", { name = 'FORM_TYPE', type = 'hidden', @@ -556,13 +608,19 @@ function room_mt:get_form_layout() name = 'muc#roomconfig_persistentroom', type = 'boolean', label = 'Make Room Persistent?', - value = self:is_persistent() + value = self:get_persistent() }, { name = 'muc#roomconfig_publicroom', type = 'boolean', label = 'Make Room Publicly Searchable?', - value = not self:is_hidden() + value = not self:get_hidden() + }, + { + name = 'muc#roomconfig_changesubject', + type = 'boolean', + label = 'Allow Occupants to Change Subject?', + value = self:get_changesubject() }, { name = 'muc#roomconfig_whois', @@ -583,22 +641,24 @@ function room_mt:get_form_layout() name = 'muc#roomconfig_moderatedroom', type = 'boolean', label = 'Make Room Moderated?', - value = self:is_moderated() + value = self:get_moderated() }, { name = 'muc#roomconfig_membersonly', type = 'boolean', label = 'Make Room Members-Only?', - value = self:is_members_only() + value = self:get_members_only() + }, + { + name = 'muc#roomconfig_historylength', + type = 'text-single', + label = 'Maximum Number of History Messages Returned by Room', + value = tostring(self:get_historylength()) } }); + return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form; end -local valid_whois = { - moderators = true, - anyone = true, -} - function room_mt:process_form(origin, stanza) local query = stanza.tags[1]; local form; @@ -607,69 +667,50 @@ function room_mt:process_form(origin, stanza) 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", "Not a submitted form")); return; end - local fields = self:get_form_layout():data(form); + local fields = self:get_form_layout(stanza.attr.from):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 changed = {}; - local description = fields['muc#roomconfig_roomdesc']; - if description ~= self:get_description() then - self:set_description(description); + local function handle_option(name, field, allowed) + local new = fields[field]; + if new == nil then return; end + if allowed and not allowed[new] then return; end + if new == self["get_"..name](self) then return; end + changed[name] = true; + self["set_"..name](self, new); end - local persistent = fields['muc#roomconfig_persistentroom']; - 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']; - dirty = dirty or (self:is_hidden() ~= (not public and true or nil)) - - local whois = fields['muc#roomconfig_whois']; - if not valid_whois[whois] then - 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', whois) + local event = { room = self, fields = fields, changed = changed, stanza = stanza, origin = origin, update_option = handle_option }; + module:fire_event("muc-config-submitted", event); - 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); + handle_option("name", "muc#roomconfig_roomname"); + handle_option("description", "muc#roomconfig_roomdesc"); + handle_option("persistent", "muc#roomconfig_persistentroom"); + handle_option("moderated", "muc#roomconfig_moderatedroom"); + handle_option("members_only", "muc#roomconfig_membersonly"); + handle_option("public", "muc#roomconfig_publicroom"); + handle_option("changesubject", "muc#roomconfig_changesubject"); + handle_option("historylength", "muc#roomconfig_historylength"); + handle_option("whois", "muc#roomconfig_whois", valid_whois); + handle_option("password", "muc#roomconfig_roomsecret"); if self.save then self:save(true); end + if self.locked then + module:fire_event("muc-room-unlocked", { room = self }); + self.locked = nil; + end origin.send(st.reply(stanza)); - if dirty or whois_changed then + if next(changed) then 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'}):up(); - end - if whois_changed then - local code = (whois == 'moderators') and "173" or "172"; + :tag('status', {code = '104'}):up(); + if changed.whois then + local code = (self:get_whois() == 'moderators') and "173" or "172"; msg.tags[1]:tag('status', {code = code}):up(); end - self:broadcast_message(msg, false) end end @@ -691,15 +732,16 @@ function room_mt:destroy(newjid, reason, password) self._occupants[nick] = nil; end self:set_persistent(false); + module:fire_event("muc-room-destroyed", { room = self }); end function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc local type = stanza.attr.type; local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; if stanza.name == "iq" then - if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" then + if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" and not stanza.tags[1].attr.node then origin.send(self:get_disco_info(stanza)); - elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" then + elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" and not stanza.tags[1].attr.node then origin.send(self:get_disco_items(stanza)); elseif xmlns == "http://jabber.org/protocol/muc#admin" then local actor = stanza.attr.from; @@ -777,13 +819,13 @@ 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 local child = stanza.tags[1].tags[1]; if not child then - origin.send(st.error_reply(stanza, "auth", "bad-request")); + origin.send(st.error_reply(stanza, "modify", "bad-request")); elseif child.name == "destroy" then local newjid = child.attr.jid; local reason, password; @@ -804,27 +846,27 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end elseif stanza.name == "message" and type == "groupchat" then - local from, to = stanza.attr.from, stanza.attr.to; - local room = jid_bare(to); + local from = stanza.attr.from; local current_nick = self._jid_nick[from]; local occupant = self._occupants[current_nick]; if not occupant then -- not in room origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); elseif occupant.role == "visitor" then - origin.send(st.error_reply(stanza, "cancel", "forbidden")); + origin.send(st.error_reply(stanza, "auth", "forbidden")); else local from = stanza.attr.from; stanza.attr.from = current_nick; - local subject = getText(stanza, {"subject"}); + local subject = stanza:get_child_text("subject"); if subject then - if occupant.role == "moderator" then - self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza + if occupant.role == "moderator" or + ( self._data.changesubject and occupant.role == "participant" ) then -- and participant + self:set_subject(current_nick, subject); else stanza.attr.from = from; - origin.send(st.error_reply(stanza, "cancel", "forbidden")); + origin.send(st.error_reply(stanza, "auth", "forbidden")); end else - self:broadcast_message(stanza, true); + self:broadcast_message(stanza, self:get_historylength() > 0 and stanza:get_child("body")); end stanza.attr.from = from; end @@ -842,8 +884,8 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha elseif type ~= "error" and type ~= "result" then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end - elseif stanza.name == "message" and not stanza.attr.type and #stanza.tags == 1 and self._jid_nick[stanza.attr.from] - and stanza.tags[1].name == "x" and stanza.tags[1].attr.xmlns == "http://jabber.org/protocol/muc#user" then + elseif stanza.name == "message" and not(type == "chat" or type == "error" or type == "groupchat" or type == "headline") and #stanza.tags == 1 + and self._jid_nick[stanza.attr.from] and stanza.tags[1].name == "x" and stanza.tags[1].attr.xmlns == "http://jabber.org/protocol/muc#user" then local x = stanza.tags[1]; local payload = (#x.tags == 1 and x.tags[1]); if payload and payload.name == "invite" and payload.attr.to then @@ -866,7 +908,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha :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 + if self:get_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 @@ -907,8 +949,25 @@ 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 + if actor ~= true then + 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 affiliation == "owner" or affiliation == "admin" or 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 + end self._affiliations[jid] = affiliation; local role = self:get_default_role(affiliation); local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) @@ -960,11 +1019,12 @@ function room_mt:get_role(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 not occupant or not actor_jid then return nil, "modify", "not-acceptable"; end + if actor_jid == true then return true; end + + local actor = self._occupants[self._jid_nick[actor_jid]]; if actor.role == "moderator" then if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then if actor.affiliation == "owner" or actor.affiliation == "admin" then @@ -1061,10 +1121,17 @@ function _M.new_room(jid, config) _occupants = {}; _data = { whois = 'moderators'; - history_length = (config and config.history_length); + history_length = math.min((config and config.history_length) + or default_history_length, max_history_length); }; _affiliations = {}; }, room_mt); end +function _M.set_max_history_length(_max_history_length) + max_history_length = _max_history_length or math.huge; +end + +_M.room_mt = room_mt; + return _M; diff --git a/plugins/sql.lib.lua b/plugins/sql.lib.lua new file mode 100644 index 00000000..005ee45d --- /dev/null +++ b/plugins/sql.lib.lua @@ -0,0 +1,9 @@ +local cache = module:shared("/*/sql.lib/util.sql"); + +if not cache._M then + prosody.unlock_globals(); + cache._M = require "util.sql"; + prosody.lock_globals(); +end + +return cache._M; diff --git a/plugins/storage/ejabberd_init.lib.lua b/plugins/storage/ejabberd_init.lib.lua deleted file mode 100644 index 91f8563b..00000000 --- a/plugins/storage/ejabberd_init.lib.lua +++ /dev/null @@ -1,252 +0,0 @@ - -local t_concat = table.concat; -local t_insert = table.insert; -local pairs = pairs; -local DBI = require "DBI"; - -local sqlite = true; -local q = {}; - -local function set(key, val) --- t_insert(q, "SET "..key.."="..val..";\n") -end -local function create_table(name, fields) - t_insert(q, "CREATE TABLE ".."IF NOT EXISTS "..name.." (\n"); - for _, field in pairs(fields) do - t_insert(q, "\t"); - field = t_concat(field, " "); - if sqlite then - if field:lower():match("^primary key *%(") then field = field:gsub("%(%d+%)", ""); end - end - t_insert(q, field); - if _ ~= #fields then t_insert(q, ",\n"); end - t_insert(q, "\n"); - end - if sqlite then - t_insert(q, ");\n"); - else - t_insert(q, ") CHARACTER SET utf8;\n"); - end -end -local function create_index(name, index) - --t_insert(q, "CREATE INDEX "..name.." ON "..index..";\n"); -end -local function create_unique_index(name, index) - --t_insert(q, "CREATE UNIQUE INDEX "..name.." ON "..index..";\n"); -end -local function insert(target, value) - t_insert(q, "INSERT INTO "..target.."\nVALUES "..value..";\n"); -end -local function foreign_key(name, fkey, fname, fcol) - t_insert(q, "ALTER TABLE `"..name.."` ADD FOREIGN KEY (`"..fkey.."`) REFERENCES `"..fname.."` (`"..fcol.."`) ON DELETE CASCADE;\n"); -end - -function build_query() - q = {}; - set('table_type', 'InnoDB'); - create_table('hosts', { - {'clusterid','integer','NOT','NULL'}; - {'host','varchar(250)','NOT','NULL','PRIMARY','KEY'}; - {'config','text','NOT','NULL'}; - }); - insert("hosts (clusterid, host, config)", "(1, 'localhost', '')"); - create_table('users', { - {'host','varchar(250)','NOT','NULL'}; - {'username','varchar(250)','NOT','NULL'}; - {'password','text','NOT','NULL'}; - {'created_at','timestamp','NOT','NULL','DEFAULT','CURRENT_TIMESTAMP'}; - {'PRIMARY','KEY','(host, username)'}; - }); - create_table('last', { - {'host','varchar(250)','NOT','NULL'}; - {'username','varchar(250)','NOT','NULL'}; - {'seconds','text','NOT','NULL'}; - {'state','text','NOT','NULL'}; - {'PRIMARY','KEY','(host, username)'}; - }); - create_table('rosterusers', { - {'host','varchar(250)','NOT','NULL'}; - {'username','varchar(250)','NOT','NULL'}; - {'jid','varchar(250)','NOT','NULL'}; - {'nick','text','NOT','NULL'}; - {'subscription','character(1)','NOT','NULL'}; - {'ask','character(1)','NOT','NULL'}; - {'askmessage','text','NOT','NULL'}; - {'server','character(1)','NOT','NULL'}; - {'subscribe','text','NOT','NULL'}; - {'type','text'}; - {'created_at','timestamp','NOT','NULL','DEFAULT','CURRENT_TIMESTAMP'}; - {'PRIMARY','KEY','(host(75), username(75), jid(75))'}; - }); - create_index('i_rosteru_username', 'rosterusers(username)'); - create_index('i_rosteru_jid', 'rosterusers(jid)'); - create_table('rostergroups', { - {'host','varchar(250)','NOT','NULL'}; - {'username','varchar(250)','NOT','NULL'}; - {'jid','varchar(250)','NOT','NULL'}; - {'grp','text','NOT','NULL'}; - {'PRIMARY','KEY','(host(75), username(75), jid(75))'}; - }); - --[[create_table('spool', { - {'host','varchar(250)','NOT','NULL'}; - {'username','varchar(250)','NOT','NULL'}; - {'xml','text','NOT','NULL'}; - {'seq','BIGINT','UNSIGNED','NOT','NULL','AUTO_INCREMENT','UNIQUE'}; - {'created_at','timestamp','NOT','NULL','DEFAULT','CURRENT_TIMESTAMP'}; - {'PRIMARY','KEY','(host, username, seq)'}; - });]] - create_table('vcard', { - {'host','varchar(250)','NOT','NULL'}; - {'username','varchar(250)','NOT','NULL'}; - {'vcard','text','NOT','NULL'}; - {'created_at','timestamp','NOT','NULL','DEFAULT','CURRENT_TIMESTAMP'}; - {'PRIMARY','KEY','(host, username)'}; - }); - create_table('vcard_search', { - {'host','varchar(250)','NOT','NULL'}; - {'username','varchar(250)','NOT','NULL'}; - {'lusername','varchar(250)','NOT','NULL'}; - {'fn','text','NOT','NULL'}; - {'lfn','varchar(250)','NOT','NULL'}; - {'family','text','NOT','NULL'}; - {'lfamily','varchar(250)','NOT','NULL'}; - {'given','text','NOT','NULL'}; - {'lgiven','varchar(250)','NOT','NULL'}; - {'middle','text','NOT','NULL'}; - {'lmiddle','varchar(250)','NOT','NULL'}; - {'nickname','text','NOT','NULL'}; - {'lnickname','varchar(250)','NOT','NULL'}; - {'bday','text','NOT','NULL'}; - {'lbday','varchar(250)','NOT','NULL'}; - {'ctry','text','NOT','NULL'}; - {'lctry','varchar(250)','NOT','NULL'}; - {'locality','text','NOT','NULL'}; - {'llocality','varchar(250)','NOT','NULL'}; - {'email','text','NOT','NULL'}; - {'lemail','varchar(250)','NOT','NULL'}; - {'orgname','text','NOT','NULL'}; - {'lorgname','varchar(250)','NOT','NULL'}; - {'orgunit','text','NOT','NULL'}; - {'lorgunit','varchar(250)','NOT','NULL'}; - {'PRIMARY','KEY','(host, lusername)'}; - }); - create_index('i_vcard_search_lfn ', 'vcard_search(lfn)'); - create_index('i_vcard_search_lfamily ', 'vcard_search(lfamily)'); - create_index('i_vcard_search_lgiven ', 'vcard_search(lgiven)'); - create_index('i_vcard_search_lmiddle ', 'vcard_search(lmiddle)'); - create_index('i_vcard_search_lnickname', 'vcard_search(lnickname)'); - create_index('i_vcard_search_lbday ', 'vcard_search(lbday)'); - create_index('i_vcard_search_lctry ', 'vcard_search(lctry)'); - create_index('i_vcard_search_llocality', 'vcard_search(llocality)'); - create_index('i_vcard_search_lemail ', 'vcard_search(lemail)'); - create_index('i_vcard_search_lorgname ', 'vcard_search(lorgname)'); - create_index('i_vcard_search_lorgunit ', 'vcard_search(lorgunit)'); - create_table('privacy_default_list', { - {'host','varchar(250)','NOT','NULL'}; - {'username','varchar(250)'}; - {'name','varchar(250)','NOT','NULL'}; - {'PRIMARY','KEY','(host, username)'}; - }); - --[[create_table('privacy_list', { - {'host','varchar(250)','NOT','NULL'}; - {'username','varchar(250)','NOT','NULL'}; - {'name','varchar(250)','NOT','NULL'}; - {'id','BIGINT','UNSIGNED','NOT','NULL','AUTO_INCREMENT','UNIQUE'}; - {'created_at','timestamp','NOT','NULL','DEFAULT','CURRENT_TIMESTAMP'}; - {'PRIMARY','KEY','(host, username, name)'}; - });]] - create_table('privacy_list_data', { - {'id','bigint'}; - {'t','character(1)','NOT','NULL'}; - {'value','text','NOT','NULL'}; - {'action','character(1)','NOT','NULL'}; - {'ord','NUMERIC','NOT','NULL'}; - {'match_all','boolean','NOT','NULL'}; - {'match_iq','boolean','NOT','NULL'}; - {'match_message','boolean','NOT','NULL'}; - {'match_presence_in','boolean','NOT','NULL'}; - {'match_presence_out','boolean','NOT','NULL'}; - }); - create_table('private_storage', { - {'host','varchar(250)','NOT','NULL'}; - {'username','varchar(250)','NOT','NULL'}; - {'namespace','varchar(250)','NOT','NULL'}; - {'data','text','NOT','NULL'}; - {'created_at','timestamp','NOT','NULL','DEFAULT','CURRENT_TIMESTAMP'}; - {'PRIMARY','KEY','(host(75), username(75), namespace(75))'}; - }); - create_index('i_private_storage_username USING BTREE', 'private_storage(username)'); - create_table('roster_version', { - {'username','varchar(250)','PRIMARY','KEY'}; - {'version','text','NOT','NULL'}; - }); - --[[create_table('pubsub_node', { - {'host','text'}; - {'node','text'}; - {'parent','text'}; - {'type','text'}; - {'nodeid','bigint','auto_increment','primary','key'}; - }); - create_index('i_pubsub_node_parent', 'pubsub_node(parent(120))'); - create_unique_index('i_pubsub_node_tuple', 'pubsub_node(host(20), node(120))'); - create_table('pubsub_node_option', { - {'nodeid','bigint'}; - {'name','text'}; - {'val','text'}; - }); - create_index('i_pubsub_node_option_nodeid', 'pubsub_node_option(nodeid)'); - foreign_key('pubsub_node_option', 'nodeid', 'pubsub_node', 'nodeid'); - create_table('pubsub_node_owner', { - {'nodeid','bigint'}; - {'owner','text'}; - }); - create_index('i_pubsub_node_owner_nodeid', 'pubsub_node_owner(nodeid)'); - foreign_key('pubsub_node_owner', 'nodeid', 'pubsub_node', 'nodeid'); - create_table('pubsub_state', { - {'nodeid','bigint'}; - {'jid','text'}; - {'affiliation','character(1)'}; - {'subscriptions','text'}; - {'stateid','bigint','auto_increment','primary','key'}; - }); - create_index('i_pubsub_state_jid', 'pubsub_state(jid(60))'); - create_unique_index('i_pubsub_state_tuple', 'pubsub_state(nodeid, jid(60))'); - foreign_key('pubsub_state', 'nodeid', 'pubsub_node', 'nodeid'); - create_table('pubsub_item', { - {'nodeid','bigint'}; - {'itemid','text'}; - {'publisher','text'}; - {'creation','text'}; - {'modification','text'}; - {'payload','text'}; - }); - create_index('i_pubsub_item_itemid', 'pubsub_item(itemid(36))'); - create_unique_index('i_pubsub_item_tuple', 'pubsub_item(nodeid, itemid(36))'); - foreign_key('pubsub_item', 'nodeid', 'pubsub_node', 'nodeid'); - create_table('pubsub_subscription_opt', { - {'subid','text'}; - {'opt_name','varchar(32)'}; - {'opt_value','text'}; - }); - create_unique_index('i_pubsub_subscription_opt', 'pubsub_subscription_opt(subid(32), opt_name(32))');]] - return t_concat(q); -end - -local function init(dbh) - local q = build_query(); - for statement in q:gmatch("[^;]*;") do - statement = statement:gsub("\n", ""):gsub("\t", " "); - if sqlite then - statement = statement:gsub("AUTO_INCREMENT", "AUTOINCREMENT"); - statement = statement:gsub("auto_increment", "autoincrement"); - end - local result, err = DBI.Do(dbh, statement); - if not result then - print("X", result, err); - print("Y", statement); - end - end -end - -local _M = { init = init }; -return _M; diff --git a/plugins/storage/ejabberdstore.lib.lua b/plugins/storage/ejabberdstore.lib.lua deleted file mode 100644 index 7e8592a8..00000000 --- a/plugins/storage/ejabberdstore.lib.lua +++ /dev/null @@ -1,190 +0,0 @@ -
-local handlers = {};
-
-handlers.accounts = {
- get = function(self, user)
- local select = self:query("select password from users where username=?", user);
- local row = select and select:fetch();
- if row then return { password = row[1] }; end
- end;
- set = function(self, user, data)
- if data and data.password then
- return self:modify("update users set password=? where username=?", data.password, user)
- or self:modify("insert into users (username, password) values (?, ?)", user, data.password);
- else
- return self:modify("delete from users where username=?", user);
- end
- end;
-};
-handlers.vcard = {
- get = function(self, user)
- local select = self:query("select vcard from vcard where username=?", user);
- local row = select and select:fetch();
- if row then return parse_xml(row[1]); end
- end;
- set = function(self, user, data)
- if data then
- data = unparse_xml(data);
- return self:modify("update vcard set vcard=? where username=?", data, user)
- or self:modify("insert into vcard (username, vcard) values (?, ?)", user, data);
- else
- return self:modify("delete from vcard where username=?", user);
- end
- end;
-};
-handlers.private = {
- get = function(self, user)
- local select = self:query("select namespace,data from private_storage where username=?", user);
- if select then
- local data = {};
- for row in select:rows() do
- data[row[1]] = parse_xml(row[2]);
- end
- return data;
- end
- end;
- set = function(self, user, data)
- if data then
- self:modify("delete from private_storage where username=?", user);
- for namespace,text in pairs(data) do
- self:modify("insert into private_storage (username, namespace, data) values (?, ?, ?)", user, namespace, unparse_xml(text));
- end
- return true;
- else
- return self:modify("delete from private_storage where username=?", user);
- end
- end;
- -- TODO map_set, map_get
-};
-local subscription_map = { N = "none", B = "both", F = "from", T = "to" };
-local subscription_map_reverse = { none = "N", both = "B", from = "F", to = "T" };
-handlers.roster = {
- get = function(self, user)
- local select = self:query("select jid,nick,subscription,ask,server,subscribe,type from rosterusers where username=?", user);
- if select then
- local roster = { pending = {} };
- for row in select:rows() do
- local jid,nick,subscription,ask,server,subscribe,typ = unpack(row);
- local item = { groups = {} };
- if nick == "" then nick = nil; end
- item.nick = nick;
- item.subscription = subscription_map[subscription];
- if ask == "N" then ask = nil;
- elseif ask == "O" then ask = "subscribe"
- elseif ask == "I" then roster.pending[jid] = true; ask = nil;
- elseif ask == "B" then roster.pending[jid] = true; ask = "subscribe";
- else module:log("debug", "bad roster_item.ask: %s", ask); ask = nil; end
- item.ask = ask;
- roster[jid] = item;
- end
-
- select = self:query("select jid,grp from rostergroups where username=?", user);
- if select then
- for row in select:rows() do
- local jid,grp = unpack(rows);
- if roster[jid] then roster[jid].groups[grp] = true; end
- end
- end
- select = self:query("select version from roster_version where username=?", user);
- local row = select and select:fetch();
- if row then
- roster[false] = { version = row[1]; };
- end
- return roster;
- end
- end;
- set = function(self, user, data)
- if data and next(data) ~= nil then
- self:modify("delete from rosterusers where username=?", user);
- self:modify("delete from rostergroups where username=?", user);
- self:modify("delete from roster_version where username=?", user);
- local done = {};
- local pending = data.pending or {};
- for jid,item in pairs(data) do
- if jid and jid ~= "pending" then
- local subscription = subscription_map_reverse[item.subscription];
- local ask;
- if pending[jid] then
- if item.ask then ask = "B"; else ask = "I"; end
- else
- if item.ask then ask = "O"; else ask = "N"; end
- end
- local r = self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?, '', '', '')", user, jid, item.nick or "", subscription, ask);
- if not r then module:log("debug", "--- :( %s", tostring(r)); end
- done[jid] = true;
- for group in pairs(item.groups) do
- self:modify("insert into rostergroups (username,jid,grp) values (?, ?, ?)", user, jid, group);
- end
- end
- end
- for jid in pairs(pending) do
- if not done[jid] then
- self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?. ''. ''. '')", user, jid, "", "N", "I");
- end
- end
- local version = data[false] and data[false].version;
- if version then
- self:modify("insert into roster_version (username,version) values (?, ?)", user, version);
- end
- return true;
- else
- self:modify("delete from rosterusers where username=?", user);
- self:modify("delete from rostergroups where username=?", user);
- self:modify("delete from roster_version where username=?", user);
- end
- end;
-};
-
------------------------------
-local driver = {};
-driver.__index = driver;
-
-function driver:prepare(sql)
- module:log("debug", "query: %s", sql);
- local err;
- if not self.sqlcache then self.sqlcache = {}; end
- local r = self.sqlcache[sql];
- if r then return r; end
- r, err = self.database:prepare(sql);
- if not r then error("Unable to prepare SQL statement: "..err); end
- self.sqlcache[sql] = r;
- return r;
-end
-
-function driver:query(sql, ...)
- local stmt = self:prepare(sql);
- if stmt:execute(...) then return stmt; end
-end
-function driver:modify(sql, ...)
- local stmt = self:query(sql, ...);
- if stmt and stmt:affected() > 0 then return stmt; end
-end
-
-function driver:open(host, datastore, typ)
- local cache_key = host.." "..datastore;
- if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
- local instance = setmetatable({}, self);
- instance.host = host;
- instance.datastore = datastore;
- local handler = handlers[datastore];
- if not handler then return nil; end
- for key,val in pairs(handler) do
- instance[key] = val;
- end
- if instance.init then instance:init(); end
- self.ds_cache[cache_key] = instance;
- return instance;
-end
-
------------------------------
-local _M = {};
-
-function _M.new(dbtype, dbname, ...)
- local instance = setmetatable({}, driver);
- instance.__index = instance;
- instance.database = get_database(dbtype, dbname, ...);
- instance.ds_cache = {};
- return instance;
-end
-
-return _M;
diff --git a/plugins/storage/mod_xep0227.lua b/plugins/storage/mod_xep0227.lua index b6d2e627..5d07a2ea 100644 --- a/plugins/storage/mod_xep0227.lua +++ b/plugins/storage/mod_xep0227.lua @@ -8,7 +8,7 @@ local os_remove = os.remove; local io_open = io.open; local st = require "util.stanza"; -local parse_xml_real = module:require("xmlparse"); +local parse_xml_real = require "util.xml".parse; local function getXml(user, host) local jid = user.."@"..host; @@ -160,4 +160,4 @@ function driver:open(host, datastore, typ) return instance; end -module:add_item("data-driver", driver); +module:provides("storage", driver); diff --git a/plugins/storage/sqlbasic.lib.lua b/plugins/storage/sqlbasic.lib.lua deleted file mode 100644 index f1202287..00000000 --- a/plugins/storage/sqlbasic.lib.lua +++ /dev/null @@ -1,97 +0,0 @@ - --- 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/xmlparse.lib.lua b/plugins/storage/xmlparse.lib.lua deleted file mode 100644 index 91063995..00000000 --- a/plugins/storage/xmlparse.lib.lua +++ /dev/null @@ -1,56 +0,0 @@ -
-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;
|