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