aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/adhoc/adhoc.lib.lua87
-rw-r--r--plugins/adhoc/mod_adhoc.lua103
-rw-r--r--plugins/mod_admin_adhoc.lua759
-rw-r--r--plugins/mod_admin_telnet.lua1038
-rw-r--r--plugins/mod_announce.lua101
-rw-r--r--plugins/mod_auth_anonymous.lua71
-rw-r--r--plugins/mod_auth_cyrus.lua84
-rw-r--r--plugins/mod_auth_internal_hashed.lua148
-rw-r--r--plugins/mod_auth_internal_plain.lua81
-rw-r--r--plugins/mod_bosh.lua455
-rw-r--r--plugins/mod_c2s.lua297
-rw-r--r--plugins/mod_component.lua318
-rw-r--r--plugins/mod_compression.lua195
-rw-r--r--plugins/mod_dialback.lua181
-rw-r--r--plugins/mod_disco.lua159
-rw-r--r--plugins/mod_groups.lua128
-rw-r--r--plugins/mod_http.lua146
-rw-r--r--plugins/mod_http_errors.lua75
-rw-r--r--plugins/mod_http_files.lua153
-rw-r--r--plugins/mod_iq.lua76
-rw-r--r--plugins/mod_lastactivity.lua52
-rw-r--r--plugins/mod_legacyauth.lua127
-rw-r--r--plugins/mod_message.lua85
-rw-r--r--plugins/mod_motd.lua30
-rw-r--r--plugins/mod_net_multiplex.lua70
-rw-r--r--plugins/mod_offline.lua51
-rw-r--r--plugins/mod_pep.lua280
-rw-r--r--plugins/mod_ping.lua35
-rw-r--r--plugins/mod_posix.lua193
-rw-r--r--plugins/mod_presence.lua382
-rw-r--r--plugins/mod_privacy.lua448
-rw-r--r--plugins/mod_private.lua52
-rw-r--r--plugins/mod_proxy65.lua192
-rw-r--r--plugins/mod_pubsub.lua466
-rw-r--r--plugins/mod_register.lua287
-rw-r--r--plugins/mod_roster.lua170
-rw-r--r--plugins/mod_s2s/mod_s2s.lua677
-rw-r--r--plugins/mod_s2s/s2sout.lib.lua361
-rw-r--r--plugins/mod_saslauth.lua390
-rw-r--r--plugins/mod_storage_internal.lua31
-rw-r--r--plugins/mod_storage_none.lua23
-rw-r--r--plugins/mod_storage_sql.lua414
-rw-r--r--plugins/mod_storage_sql2.lua237
-rw-r--r--plugins/mod_time.lua44
-rw-r--r--plugins/mod_tls.lua107
-rw-r--r--plugins/mod_uptime.lua48
-rw-r--r--plugins/mod_vcard.lua54
-rw-r--r--plugins/mod_version.lua48
-rw-r--r--plugins/mod_watchregistrations.lua30
-rw-r--r--plugins/mod_welcome.lua21
-rw-r--r--plugins/muc/mod_muc.lua229
-rw-r--r--plugins/muc/muc.lib.lua1171
-rw-r--r--plugins/sql.lib.lua9
-rw-r--r--plugins/storage/mod_xep0227.lua163
-rw-r--r--plugins/storage/sqlbasic.lib.lua97
-rw-r--r--plugins/storage/xep227store.lib.lua168
56 files changed, 11670 insertions, 227 deletions
diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua
new file mode 100644
index 00000000..ecddcd1d
--- /dev/null
+++ b/plugins/adhoc/adhoc.lib.lua
@@ -0,0 +1,87 @@
+-- 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 = {};
+
+local function _cmdtag(desc, status, sessionid, action)
+ local cmd = st.stanza("command", { xmlns = xmlns_cmd, node = desc.node, status = status });
+ if sessionid then cmd.attr.sessionid = sessionid; end
+ if action then cmd.attr.action = action; end
+
+ 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);
+ local cmdtag;
+ 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);
+ data.actions = data.actions or { "complete" };
+ 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", { execute = content.default });
+ for _, action in ipairs(content) do
+ if (action == "prev") or (action == "next") or (action == "complete") then
+ actions:tag(action):up();
+ else
+ module:log("error", "Command %q at node %q provided an invalid action %q",
+ command.name, command.node, 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..69b2c8da
--- /dev/null
+++ b/plugins/adhoc/mod_adhoc.lua
@@ -0,0 +1,103 @@
+-- Copyright (C) 2009 Thilo Cestonaro
+-- Copyright (C) 2009-2011 Florian Zeitz
+--
+-- This file is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza";
+local 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 admin = is_admin(stanza.attr.from, stanza.attr.to);
+ local global_admin = is_admin(stanza.attr.from);
+ 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 admin)
+ or (command.permission == "global_admin" and global_admin)
+ 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 admin = is_admin(stanza.attr.from, stanza.attr.to);
+ local global_admin = is_admin(stanza.attr.from);
+ if (commands[node].permission == "admin" and not admin)
+ or (commands[node].permission == "global_admin" and not global_admin) 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 adhoc_added(event)
+ local item = event.item;
+ commands[item.node] = item;
+end
+
+local function adhoc_removed(event)
+ commands[event.item.node] = nil;
+end
+
+module:handle_items("adhoc", adhoc_added, adhoc_removed);
+module:handle_items("adhoc-provider", adhoc_added, adhoc_removed);
diff --git a/plugins/mod_admin_adhoc.lua b/plugins/mod_admin_adhoc.lua
new file mode 100644
index 00000000..31c4bde4
--- /dev/null
+++ b/plugins/mod_admin_adhoc.lua
@@ -0,0 +1,759 @@
+-- Copyright (C) 2009-2011 Florian Zeitz
+--
+-- This file is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local _G = _G;
+
+local prosody = _G.prosody;
+local hosts = prosody.hosts;
+local t_concat = table.concat;
+
+local module_host = module:get_host();
+
+local keys = require "util.iterators".keys;
+local usermanager_user_exists = require "core.usermanager".user_exists;
+local usermanager_create_user = require "core.usermanager".create_user;
+local usermanager_delete_user = require "core.usermanager".delete_user;
+local usermanager_get_password = require "core.usermanager".get_password;
+local usermanager_set_password = require "core.usermanager".set_password;
+local hostmanager_activate = require "core.hostmanager".activate;
+local hostmanager_deactivate = require "core.hostmanager".deactivate;
+local rm_load_roster = require "core.rostermanager".load_roster;
+local st, jid = require "util.stanza", require "util.jid";
+local timer_add_task = require "util.timer".add_task;
+local dataforms_new = require "util.dataforms".new;
+local array = require "util.array";
+local modulemanager = require "modulemanager";
+local core_post_stanza = prosody.core_post_stanza;
+local adhoc_simple = require "util.adhoc".new_simple_form;
+local adhoc_initial = require "util.adhoc".new_initial_data_form;
+
+module:depends("adhoc");
+local adhoc_new = module:require "adhoc".new;
+
+local function generate_error_message(errors)
+ local errmsg = {};
+ for name, err in pairs(errors) do
+ errmsg[#errmsg + 1] = name .. ": " .. err;
+ end
+ return { status = "completed", error = { message = t_concat(errmsg, "\n") } };
+end
+
+-- Adding a new user
+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" };
+};
+
+local add_user_command_handler = adhoc_simple(add_user_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+ local username, host, resource = jid.split(fields.accountjid);
+ if module_host ~= host then
+ return { status = "completed", error = { message = "Trying to add a user on " .. host .. " but command was sent to " .. module_host}};
+ end
+ if (fields["password"] == fields["password-verify"]) and username and host then
+ if usermanager_user_exists(username, host) then
+ return { status = "completed", error = { message = "Account already exists" } };
+ else
+ if usermanager_create_user(username, fields.password, host) then
+ module:log("info", "Created new account %s@%s", username, host);
+ return { status = "completed", info = "Account successfully created" };
+ else
+ return { status = "completed", error = { message = "Failed to write data to disk" } };
+ end
+ end
+ else
+ module:log("debug", "Invalid data, password mismatch or empty username while creating account for %s", fields.accountjid or "<nil>");
+ return { status = "completed", error = { message = "Invalid data.\nPassword mismatch, or empty username" } };
+ end
+end);
+
+-- Changing a user's password
+local change_user_password_layout = dataforms_new{
+ title = "Changing a User Password";
+ instructions = "Fill out this form to change a user's password.";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
+ { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for this account" };
+ { name = "password", type = "text-private", required = true, label = "The password for this account" };
+};
+
+local change_user_password_command_handler = adhoc_simple(change_user_password_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+ local username, host, resource = jid.split(fields.accountjid);
+ if module_host ~= host then
+ return { status = "completed", error = { message = "Trying to change the password of a user on " .. host .. " but command was sent to " .. module_host}};
+ end
+ if usermanager_user_exists(username, host) and usermanager_set_password(username, fields.password, host) then
+ return { status = "completed", info = "Password successfully changed" };
+ else
+ return { status = "completed", error = { message = "User does not exist" } };
+ end
+end);
+
+-- Reloading the config
+local function config_reload_handler(self, data, state)
+ local ok, err = prosody.reload_config();
+ if ok then
+ return { status = "completed", info = "Configuration reloaded (modules may need to be reloaded for this to have an effect)" };
+ else
+ return { status = "completed", error = { message = "Failed to reload config: " .. tostring(err) } };
+ end
+end
+
+-- Deleting a user's account
+local delete_user_layout = dataforms_new{
+ title = "Deleting a User";
+ instructions = "Fill out this form to delete a user.";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
+ { name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) to delete" };
+};
+
+local delete_user_command_handler = adhoc_simple(delete_user_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+ local failed = {};
+ local succeeded = {};
+ for _, aJID in ipairs(fields.accountjids) do
+ local username, host, resource = jid.split(aJID);
+ if (host == module_host) and usermanager_user_exists(username, host) and usermanager_delete_user(username, host) then
+ module:log("debug", "User %s has been deleted", aJID);
+ succeeded[#succeeded+1] = aJID;
+ else
+ module:log("debug", "Tried to delete non-existant user %s", aJID);
+ failed[#failed+1] = aJID;
+ end
+ 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 "") };
+end);
+
+-- Ending a user's session
+local function disconnect_user(match_jid)
+ local node, hostname, givenResource = jid.split(match_jid);
+ local host = hosts[hostname];
+ local sessions = host.sessions[node] and host.sessions[node].sessions;
+ for resource, session in pairs(sessions or {}) do
+ if not givenResource or (resource == givenResource) then
+ module:log("debug", "Disconnecting %s@%s/%s", node, hostname, resource);
+ session:close();
+ end
+ end
+ return true;
+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" };
+};
+
+local end_user_session_handler = adhoc_simple(end_user_session_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+ local failed = {};
+ local succeeded = {};
+ for _, aJID in ipairs(fields.accountjids) do
+ local username, host, resource = jid.split(aJID);
+ if (host == module_host) and usermanager_user_exists(username, host) and disconnect_user(aJID) then
+ succeeded[#succeeded+1] = aJID;
+ else
+ failed[#failed+1] = aJID;
+ end
+ end
+ return {status = "completed", info = (#succeeded ~= 0 and
+ "The following accounts were successfully disconnected:\n"..t_concat(succeeded, "\n").."\n" or "")..
+ (#failed ~= 0 and
+ "The following accounts could not be disconnected:\n"..t_concat(failed, "\n") or "") };
+end);
+
+-- Getting a user's password
+local get_user_password_layout = dataforms_new{
+ title = "Getting User's Password";
+ instructions = "Fill out this form to get a user's password.";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
+ { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for which to retrieve the password" };
+};
+
+local get_user_password_result_layout = dataforms_new{
+ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
+ { name = "accountjid", type = "jid-single", label = "JID" };
+ { name = "password", type = "text-single", label = "Password" };
+};
+
+local get_user_password_handler = adhoc_simple(get_user_password_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+ local user, host, resource = jid.split(fields.accountjid);
+ local accountjid = "";
+ local password = "";
+ if host ~= module_host then
+ return { status = "completed", error = { message = "Tried to get password for a user on " .. host .. " but command was sent to " .. module_host } };
+ elseif usermanager_user_exists(user, host) then
+ accountjid = fields.accountjid;
+ password = usermanager_get_password(user, host);
+ else
+ return { status = "completed", error = { message = "User does not exist" } };
+ end
+ return { status = "completed", result = { layout = get_user_password_result_layout, values = {accountjid = accountjid, password = password} } };
+end);
+
+-- Getting a user's roster
+local get_user_roster_layout = dataforms_new{
+ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
+ { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for which to retrieve the roster" };
+};
+
+local 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" };
+};
+
+local get_user_roster_handler = adhoc_simple(get_user_roster_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+
+ local user, host, resource = jid.split(fields.accountjid);
+ if host ~= module_host then
+ return { status = "completed", error = { message = "Tried to get roster for a user on " .. host .. " but command was sent to " .. module_host } };
+ elseif not usermanager_user_exists(user, host) then
+ return { status = "completed", error = { message = "User does not exist" } };
+ end
+ local roster = rm_load_roster(user, host);
+
+ local query = st.stanza("query", { xmlns = "jabber:iq:roster" });
+ for jid in pairs(roster) do
+ if jid ~= "pending" and jid then
+ query:tag("item", {
+ jid = jid,
+ subscription = roster[jid].subscription,
+ ask = roster[jid].ask,
+ name = roster[jid].name,
+ });
+ for group in pairs(roster[jid].groups) do
+ query:tag("group"):text(group):up();
+ end
+ query:up();
+ end
+ end
+
+ local query_text = tostring(query):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 };
+end);
+
+-- Getting user statistics
+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" };
+};
+
+local get_user_stats_handler = adhoc_simple(get_user_stats_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+
+ local user, host, resource = jid.split(fields.accountjid);
+ if host ~= module_host then
+ return { status = "completed", error = { message = "Tried to get stats for a user on " .. host .. " but command was sent to " .. module_host } };
+ elseif not usermanager_user_exists(user, host) then
+ return { status = "completed", error = { message = "User does not exist" } };
+ end
+ local roster = rm_load_roster(user, host);
+ local rostersize = 0;
+ local IPs = "";
+ local resources = "";
+ for jid in pairs(roster) do
+ if jid ~= "pending" and jid then
+ rostersize = rostersize + 1;
+ end
+ 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}} };
+end);
+
+-- Getting a list of online users
+local get_online_users_layout = dataforms_new{
+ title = "Getting List of Online Users";
+ instructions = "How many users should be returned at most?";
+
+ { 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" };
+};
+
+local get_online_users_command_handler = adhoc_simple(get_online_users_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+
+ local max_items = nil
+ if fields.max_items ~= "all" then
+ max_items = tonumber(fields.max_items);
+ end
+ local count = 0;
+ local users = {};
+ for username, user in pairs(hosts[module_host].sessions or {}) do
+ if (max_items ~= nil) and (count >= max_items) then
+ break;
+ end
+ users[#users+1] = username.."@"..module_host;
+ count = count + 1;
+ if fields.details then
+ for resource, session in pairs(user.sessions or {}) do
+ local status, priority = "unavailable", tostring(session.priority or "-");
+ if session.presence then
+ status = session.presence:child_with_name("show");
+ if status then
+ status = status:get_text() or "[invalid!]";
+ else
+ status = "available";
+ end
+ 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")}} };
+end);
+
+-- Getting a list of loaded modules
+local list_modules_result = dataforms_new {
+ title = "List of loaded modules";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#list" };
+ { name = "modules", type = "text-multi", label = "The following modules are loaded:" };
+};
+
+local function list_modules_handler(self, data, state)
+ local modules = array.collect(keys(hosts[module_host].modules)):sort():concat("\n");
+ return { status = "completed", result = { layout = list_modules_result; values = { modules = modules } } };
+end
+
+-- Loading a module
+local load_module_layout = dataforms_new {
+ title = "Load module";
+ instructions = "Specify the module to be loaded";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#load" };
+ { name = "module", type = "text-single", required = true, label = "Module to be loaded:"};
+};
+
+local load_module_handler = adhoc_simple(load_module_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+ if modulemanager.is_loaded(module_host, fields.module) then
+ return { status = "completed", info = "Module already loaded" };
+ end
+ local ok, err = modulemanager.load(module_host, fields.module);
+ if ok then
+ return { status = "completed", info = 'Module "'..fields.module..'" successfully loaded on host "'..module_host..'".' };
+ else
+ return { status = "completed", error = { message = 'Failed to load module "'..fields.module..'" on host "'..module_host..
+ '". Error was: "'..tostring(err or "<unspecified>")..'"' } };
+ end
+end);
+
+-- Globally loading a module
+local globally_load_module_layout = dataforms_new {
+ title = "Globally load module";
+ instructions = "Specify the module to be loaded on all hosts";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#global-load" };
+ { name = "module", type = "text-single", required = true, label = "Module to globally load:"};
+};
+
+local globally_load_module_handler = adhoc_simple(globally_load_module_layout, function(fields, err)
+ local ok_list, err_list = {}, {};
+
+ if err then
+ return generate_error_message(err);
+ end
+
+ local ok, err = modulemanager.load(module_host, fields.module);
+ if ok then
+ ok_list[#ok_list + 1] = module_host;
+ else
+ err_list[#err_list + 1] = module_host .. " (Error: " .. tostring(err) .. ")";
+ end
+
+ -- Is this a global module?
+ if modulemanager.is_loaded("*", fields.module) and not modulemanager.is_loaded(module_host, fields.module) then
+ return { status = "completed", info = 'Global module '..fields.module..' loaded.' };
+ end
+
+ -- This is either a shared or "normal" module, load it on all other hosts
+ for host_name, host in pairs(hosts) do
+ if host_name ~= module_host and host.type == "local" then
+ local ok, err = modulemanager.load(host_name, fields.module);
+ if ok then
+ ok_list[#ok_list + 1] = host_name;
+ else
+ err_list[#err_list + 1] = host_name .. " (Error: " .. tostring(err) .. ")";
+ end
+ end
+ end
+
+ local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully loaded onto the hosts:\n"..t_concat(ok_list, "\n")) or "")
+ .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") ..
+ (#err_list > 0 and ("Failed to load the module "..fields.module.." onto the hosts:\n"..t_concat(err_list, "\n")) or "");
+ return { status = "completed", info = info };
+end);
+
+-- Reloading modules
+local reload_modules_layout = dataforms_new {
+ title = "Reload modules";
+ instructions = "Select the modules to be reloaded";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#reload" };
+ { name = "modules", type = "list-multi", required = true, label = "Modules to be reloaded:"};
+};
+
+local reload_modules_handler = adhoc_initial(reload_modules_layout, function()
+ return { modules = array.collect(keys(hosts[module_host].modules)):sort() };
+end, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+ local ok_list, err_list = {}, {};
+ for _, module in ipairs(fields.modules) do
+ local ok, err = modulemanager.reload(module_host, module);
+ if ok then
+ 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 "..module_host..":\n"..t_concat(ok_list, "\n")) or "")
+ .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") ..
+ (#err_list > 0 and ("Failed to reload the following modules on host "..module_host..":\n"..t_concat(err_list, "\n")) or "");
+ return { status = "completed", info = info };
+end);
+
+-- Globally reloading a module
+local globally_reload_module_layout = dataforms_new {
+ title = "Globally reload module";
+ instructions = "Specify the module to reload on all hosts";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#global-reload" };
+ { name = "module", type = "list-single", required = true, label = "Module to globally reload:"};
+};
+
+local globally_reload_module_handler = adhoc_initial(globally_reload_module_layout, function()
+ local loaded_modules = array(keys(modulemanager.get_modules("*")));
+ for _, host in pairs(hosts) do
+ loaded_modules:append(array(keys(host.modules)));
+ end
+ loaded_modules = array(keys(set.new(loaded_modules):items())):sort();
+ return { module = loaded_modules };
+end, function(fields, err)
+ local is_global = false;
+
+ if err then
+ return generate_error_message(err);
+ end
+
+ if modulemanager.is_loaded("*", fields.module) then
+ local ok, err = modulemanager.reload("*", fields.module);
+ if not ok then
+ return { status = "completed", info = 'Global module '..fields.module..' failed to reload: '..err };
+ end
+ is_global = true;
+ end
+
+ local ok_list, err_list = {}, {};
+ for host_name, host in pairs(hosts) do
+ if modulemanager.is_loaded(host_name, fields.module) then
+ local ok, err = modulemanager.reload(host_name, fields.module);
+ if ok then
+ ok_list[#ok_list + 1] = host_name;
+ else
+ err_list[#err_list + 1] = host_name .. " (Error: " .. tostring(err) .. ")";
+ end
+ end
+ end
+
+ if #ok_list == 0 and #err_list == 0 then
+ if is_global then
+ return { status = "completed", info = 'Successfully reloaded global module '..fields.module };
+ else
+ return { status = "completed", info = 'Module '..fields.module..' not loaded on any host.' };
+ end
+ end
+
+ local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully reloaded on the hosts:\n"..t_concat(ok_list, "\n")) or "")
+ .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") ..
+ (#err_list > 0 and ("Failed to reload the module "..fields.module.." on the hosts:\n"..t_concat(err_list, "\n")) or "");
+ return { status = "completed", info = info };
+end);
+
+local function send_to_online(message, server)
+ if server then
+ sessions = { [server] = hosts[server] };
+ else
+ 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
+
+-- Shutting down the service
+local shut_down_service_layout = dataforms_new{
+ title = "Shutting Down the Service";
+ instructions = "Fill out this form to shut down the service.";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
+ { name = "delay", type = "list-single", label = "Time delay before shutting down",
+ value = { {label = "30 seconds", value = "30"},
+ {label = "60 seconds", value = "60"},
+ {label = "90 seconds", value = "90"},
+ {label = "2 minutes", value = "120"},
+ {label = "3 minutes", value = "180"},
+ {label = "4 minutes", value = "240"},
+ {label = "5 minutes", value = "300"},
+ };
+ };
+ { name = "announcement", type = "text-multi", label = "Announcement" };
+};
+
+local shut_down_service_handler = adhoc_simple(shut_down_service_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+
+ if fields.announcement and #fields.announcement > 0 then
+ local message = st.message({type = "headline"}, fields.announcement):up()
+ :tag("subject"):text("Server is shutting down");
+ send_to_online(message);
+ end
+
+ timer_add_task(tonumber(fields.delay or "5"), function(time) prosody.shutdown("Shutdown by adhoc command") end);
+
+ return { status = "completed", info = "Server is about to shut down" };
+end);
+
+-- Unloading modules
+local unload_modules_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:"};
+};
+
+local unload_modules_handler = adhoc_initial(unload_modules_layout, function()
+ return { modules = array.collect(keys(hosts[module_host].modules)):sort() };
+end, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+ local ok_list, err_list = {}, {};
+ for _, module in ipairs(fields.modules) do
+ local ok, err = modulemanager.unload(module_host, module);
+ if ok then
+ ok_list[#ok_list + 1] = module;
+ else
+ err_list[#err_list + 1] = module .. "(Error: " .. tostring(err) .. ")";
+ end
+ end
+ local info = (#ok_list > 0 and ("The following modules were successfully unloaded on host "..module_host..":\n"..t_concat(ok_list, "\n")) or "")
+ .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") ..
+ (#err_list > 0 and ("Failed to unload the following modules on host "..module_host..":\n"..t_concat(err_list, "\n")) or "");
+ return { status = "completed", info = info };
+end);
+
+-- Globally unloading a module
+local globally_unload_module_layout = dataforms_new {
+ title = "Globally unload module";
+ instructions = "Specify a module to unload on all hosts";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#global-unload" };
+ { name = "module", type = "list-single", required = true, label = "Module to globally unload:"};
+};
+
+local globally_unload_module_handler = adhoc_initial(globally_unload_module_layout, function()
+ local loaded_modules = array(keys(modulemanager.get_modules("*")));
+ for _, host in pairs(hosts) do
+ loaded_modules:append(array(keys(host.modules)));
+ end
+ loaded_modules = array(keys(set.new(loaded_modules):items())):sort();
+ return { module = loaded_modules };
+end, function(fields, err)
+ local is_global = false;
+ if err then
+ return generate_error_message(err);
+ end
+
+ if modulemanager.is_loaded("*", fields.module) then
+ local ok, err = modulemanager.unload("*", fields.module);
+ if not ok then
+ return { status = "completed", info = 'Global module '..fields.module..' failed to unload: '..err };
+ end
+ is_global = true;
+ end
+
+ local ok_list, err_list = {}, {};
+ for host_name, host in pairs(hosts) do
+ if modulemanager.is_loaded(host_name, fields.module) then
+ local ok, err = modulemanager.unload(host_name, fields.module);
+ if ok then
+ ok_list[#ok_list + 1] = host_name;
+ else
+ err_list[#err_list + 1] = host_name .. " (Error: " .. tostring(err) .. ")";
+ end
+ end
+ end
+
+ if #ok_list == 0 and #err_list == 0 then
+ if is_global then
+ return { status = "completed", info = 'Successfully unloaded global module '..fields.module };
+ else
+ return { status = "completed", info = 'Module '..fields.module..' not loaded on any host.' };
+ end
+ end
+
+ local info = (#ok_list > 0 and ("The module "..fields.module.." was successfully unloaded on the hosts:\n"..t_concat(ok_list, "\n")) or "")
+ .. ((#ok_list > 0 and #err_list > 0) and "\n" or "") ..
+ (#err_list > 0 and ("Failed to unload the module "..fields.module.." on the hosts:\n"..t_concat(err_list, "\n")) or "");
+ return { status = "completed", info = info };
+end);
+
+-- Activating a host
+local activate_host_layout = dataforms_new {
+ title = "Activate host";
+ instructions = "";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/hosts#activate" };
+ { name = "host", type = "text-single", required = true, label = "Host:"};
+};
+
+local activate_host_handler = adhoc_simple(activate_host_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+ local ok, err = hostmanager_activate(fields.host);
+
+ if ok then
+ return { status = "completed", info = fields.host .. " activated" };
+ else
+ return { status = "canceled", error = err }
+ end
+end);
+
+-- Deactivating a host
+local deactivate_host_layout = dataforms_new {
+ title = "Deactivate host";
+ instructions = "";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/hosts#activate" };
+ { name = "host", type = "text-single", required = true, label = "Host:"};
+};
+
+local deactivate_host_handler = adhoc_simple(deactivate_host_layout, function(fields, err)
+ if err then
+ return generate_error_message(err);
+ end
+ local ok, err = hostmanager_deactivate(fields.host);
+
+ if ok then
+ return { status = "completed", info = fields.host .. " deactivated" };
+ else
+ return { status = "canceled", error = err }
+ end
+end);
+
+
+local add_user_desc = adhoc_new("Add User", "http://jabber.org/protocol/admin#add-user", add_user_command_handler, "admin");
+local change_user_password_desc = adhoc_new("Change User Password", "http://jabber.org/protocol/admin#change-user-password", change_user_password_command_handler, "admin");
+local config_reload_desc = adhoc_new("Reload configuration", "http://prosody.im/protocol/config#reload", config_reload_handler, "global_admin");
+local delete_user_desc = adhoc_new("Delete User", "http://jabber.org/protocol/admin#delete-user", delete_user_command_handler, "admin");
+local end_user_session_desc = adhoc_new("End User Session", "http://jabber.org/protocol/admin#end-user-session", end_user_session_handler, "admin");
+local get_user_password_desc = adhoc_new("Get User Password", "http://jabber.org/protocol/admin#get-user-password", get_user_password_handler, "admin");
+local get_user_roster_desc = adhoc_new("Get User Roster","http://jabber.org/protocol/admin#get-user-roster", get_user_roster_handler, "admin");
+local get_user_stats_desc = adhoc_new("Get User Statistics","http://jabber.org/protocol/admin#user-stats", get_user_stats_handler, "admin");
+local get_online_users_desc = adhoc_new("Get List of Online Users", "http://jabber.org/protocol/admin#get-online-users", get_online_users_command_handler, "admin");
+local list_modules_desc = adhoc_new("List loaded modules", "http://prosody.im/protocol/modules#list", list_modules_handler, "admin");
+local load_module_desc = adhoc_new("Load module", "http://prosody.im/protocol/modules#load", load_module_handler, "admin");
+local globally_load_module_desc = adhoc_new("Globally load module", "http://prosody.im/protocol/modules#global-load", globally_load_module_handler, "global_admin");
+local reload_modules_desc = adhoc_new("Reload modules", "http://prosody.im/protocol/modules#reload", reload_modules_handler, "admin");
+local globally_reload_module_desc = adhoc_new("Globally reload module", "http://prosody.im/protocol/modules#global-reload", globally_reload_module_handler, "global_admin");
+local shut_down_service_desc = adhoc_new("Shut Down Service", "http://jabber.org/protocol/admin#shutdown", shut_down_service_handler, "global_admin");
+local unload_modules_desc = adhoc_new("Unload modules", "http://prosody.im/protocol/modules#unload", unload_modules_handler, "admin");
+local globally_unload_module_desc = adhoc_new("Globally unload module", "http://prosody.im/protocol/modules#global-unload", globally_unload_module_handler, "global_admin");
+local activate_host_desc = adhoc_new("Activate host", "http://prosody.im/protocol/hosts#activate", activate_host_handler, "global_admin");
+local deactivate_host_desc = adhoc_new("Deactivate host", "http://prosody.im/protocol/hosts#deactivate", deactivate_host_handler, "global_admin");
+
+module:provides("adhoc", add_user_desc);
+module:provides("adhoc", change_user_password_desc);
+module:provides("adhoc", config_reload_desc);
+module:provides("adhoc", delete_user_desc);
+module:provides("adhoc", end_user_session_desc);
+module:provides("adhoc", get_user_password_desc);
+module:provides("adhoc", get_user_roster_desc);
+module:provides("adhoc", get_user_stats_desc);
+module:provides("adhoc", get_online_users_desc);
+module:provides("adhoc", list_modules_desc);
+module:provides("adhoc", load_module_desc);
+module:provides("adhoc", globally_load_module_desc);
+module:provides("adhoc", reload_modules_desc);
+module:provides("adhoc", globally_reload_module_desc);
+module:provides("adhoc", shut_down_service_desc);
+module:provides("adhoc", unload_modules_desc);
+module:provides("adhoc", globally_unload_module_desc);
+module:provides("adhoc", activate_host_desc);
+module:provides("adhoc", deactivate_host_desc);
diff --git a/plugins/mod_admin_telnet.lua b/plugins/mod_admin_telnet.lua
new file mode 100644
index 00000000..2622a5f9
--- /dev/null
+++ b/plugins/mod_admin_telnet.lua
@@ -0,0 +1,1038 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+module:set_global();
+
+local hostmanager = require "core.hostmanager";
+local modulemanager = require "core.modulemanager";
+local s2smanager = require "core.s2smanager";
+local portmanager = require "core.portmanager";
+
+local _G = _G;
+
+local prosody = _G.prosody;
+local hosts = prosody.hosts;
+local incoming_s2s = prosody.incoming_s2s;
+
+local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" };
+
+local iterators = require "util.iterators";
+local keys, values = iterators.keys, iterators.values;
+local jid = require "util.jid";
+local jid_bare, jid_split = jid.bare, jid.split;
+local set, array = require "util.set", require "util.array";
+local cert_verify_identity = require "util.x509".verify_identity;
+local envload = require "util.envload".envload;
+local envloadfile = require "util.envload".envloadfile;
+
+local commands = module:shared("commands")
+local def_env = module:shared("env");
+local default_env_mt = { __index = def_env };
+local core_post_stanza = prosody.core_post_stanza;
+
+local function redirect_output(_G, session)
+ local env = setmetatable({ print = session.print }, { __index = function (t, k) return rawget(_G, k); end });
+ env.dofile = function(name)
+ local f, err = envloadfile(name, env);
+ if not f then return f, err; end
+ return f();
+ end;
+ return env;
+end
+
+console = {};
+
+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 (...)
+ 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);
+
+ -- Load up environment with helper objects
+ for name, t in pairs(def_env) do
+ if type(t) == "table" then
+ session.env[name] = setmetatable({ session = session }, { __index = t });
+ end
+ end
+
+ return session;
+end
+
+function console:process_line(session, line)
+ local useglobalenv;
+
+ if line:match("^>") then
+ line = line:gsub("^>", "");
+ useglobalenv = true;
+ elseif line == "\004" then
+ commands["bye"](session, line);
+ return;
+ else
+ local command = line:match("^%w+") or line:match("%p");
+ if commands[command] then
+ commands[command](session, line);
+ return;
+ end
+ end
+
+ session.env._ = line;
+
+ local chunkname = "=console";
+ local env = (useglobalenv and redirect_output(_G, session)) or session.env or nil
+ local chunk, err = envload("return "..line, chunkname, env);
+ if not chunk then
+ chunk, err = envload(line, chunkname, env);
+ if not chunk then
+ err = err:gsub("^%[string .-%]:%d+: ", "");
+ err = err:gsub("^:%d+: ", "");
+ err = err:gsub("'<eof>'", "the end of the line");
+ session.print("Sorry, I couldn't understand that... "..err);
+ return;
+ end
+ end
+
+ local ranok, taskok, message = pcall(chunk);
+
+ if not (ranok or message or useglobalenv) and commands[line:lower()] then
+ commands[line:lower()](session, line);
+ return;
+ end
+
+ if not ranok then
+ session.print("Fatal error while running command, it did not complete");
+ session.print("Error: "..taskok);
+ return;
+ end
+
+ if not message then
+ session.print("Result: "..tostring(taskok));
+ return;
+ elseif (not taskok) and message then
+ session.print("Command completed with a problem");
+ session.print("Message: "..tostring(message));
+ return;
+ end
+
+ session.print("OK: "..tostring(message));
+end
+
+local sessions = {};
+
+function console_listener.onconnect(conn)
+ -- Handle new connection
+ local session = console:new_session(conn);
+ sessions[conn] = session;
+ printbanner(session);
+ session.send(string.char(0));
+end
+
+function console_listener.onincoming(conn, data)
+ local session = sessions[conn];
+
+ local partial = session.partial_data;
+ if partial then
+ data = partial..data;
+ end
+
+ for line in data:gmatch("[^\n]*[\n\004]") do
+ if session.closed then return end
+ console:process_line(session, line);
+ session.send(string.char(0));
+ end
+ session.partial_data = data:match("[^\n]+$");
+end
+
+function console_listener.ondisconnect(conn, err)
+ local session = sessions[conn];
+ if session then
+ session.disconnect();
+ sessions[conn] = nil;
+ end
+end
+
+-- Console commands --
+-- These are simple commands, not valid standalone in Lua
+
+function commands.bye(session)
+ session.print("See you! :)");
+ session.closed = true;
+ session.disconnect();
+end
+commands.quit, commands.exit = commands.bye, commands.bye;
+
+commands["!"] = function (session, data)
+ if data:match("^!!") and session.env._ then
+ session.print("!> "..session.env._);
+ return console_listener.onincoming(session.conn, session.env._);
+ end
+ local old, new = data:match("^!(.-[^\\])!(.-)!$");
+ if old and new then
+ local ok, res = pcall(string.gsub, session.env._, old, new);
+ if not ok then
+ session.print(res)
+ return;
+ end
+ session.print("!> "..res);
+ return console_listener.onincoming(session.conn, res);
+ end
+ session.print("Sorry, not sure what you want");
+end
+
+
+function commands.help(session, data)
+ local print = session.print;
+ local section = data:match("^help (%w+)");
+ if not section then
+ print [[Commands are divided into multiple sections. For help on a particular section, ]]
+ print [[type: help SECTION (for example, 'help c2s'). Sections are: ]]
+ print [[]]
+ 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 [[user - Commands to create and delete users, and change their passwords]]
+ print [[server - Uptime, version, shutting down, etc.]]
+ print [[port - Commands to manage ports the server is listening on]]
+ print [[config - Reloading the configuration, etc.]]
+ print [[console - Help regarding the console itself]]
+ elseif section == "c2s" then
+ print [[c2s:show(jid) - Show all client sessions with the specified JID (or all if no JID given)]]
+ print [[c2s:show_insecure() - Show all unencrypted client connections]]
+ print [[c2s:show_secure() - Show all encrypted client connections]]
+ print [[c2s:close(jid) - Close all sessions for the specified JID]]
+ elseif section == "s2s" then
+ print [[s2s:show(domain) - Show all s2s connections for the given domain (or all if no domain given)]]
+ print [[s2s:close(from, to) - Close a connection from one domain to another]]
+ print [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]]
+ elseif section == "module" then
+ print [[module:load(module, host) - Load the specified module on the specified host (or all hosts if none given)]]
+ print [[module:reload(module, host) - The same, but unloads and loads the module (saving state if the module supports it)]]
+ 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 == "user" then
+ print [[user:create(jid, password) - Create the specified user account]]
+ print [[user:password(jid, password) - Set the password for the specified user account]]
+ print [[user:delete(jid) - Permanently remove the specified user account]]
+ print [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]]
+ elseif section == "server" then
+ print [[server:version() - Show the server's version number]]
+ print [[server:uptime() - Show how long the server has been running]]
+ print [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]]
+ elseif section == "port" then
+ print [[port:list() - Lists all network ports prosody currently listens on]]
+ print [[port:close(port, interface) - Close a port]]
+ elseif section == "config" then
+ print [[config:reload() - Reload the server configuration. Modules may need to be reloaded for changes to take effect.]]
+ elseif section == "console" then
+ print [[Hey! Welcome to Prosody's admin console.]]
+ print [[First thing, if you're ever wondering how to get out, simply type 'quit'.]]
+ print [[Secondly, note that we don't support the full telnet protocol yet (it's coming)]]
+ print [[so you may have trouble using the arrow keys, etc. depending on your system.]]
+ print [[]]
+ print [[For now we offer a couple of handy shortcuts:]]
+ print [[!! - Repeat the last command]]
+ print [[!old!new! - repeat the last command, but with 'old' replaced by 'new']]
+ print [[]]
+ print [[For those well-versed in Prosody's internals, or taking instruction from those who are,]]
+ print [[you can prefix a command with > to escape the console sandbox, and access everything in]]
+ print [[the running server. Great fun, but be careful not to break anything :)]]
+ end
+ print [[]]
+end
+
+-- Session environment --
+-- Anything in def_env will be accessible within the session as a global variable
+
+def_env.server = {};
+
+function def_env.server:insane_reload()
+ prosody.unlock_globals();
+ dofile "prosody"
+ prosody = _G.prosody;
+ return true, "Server reloaded";
+end
+
+function def_env.server:version()
+ return true, tostring(prosody.version or "unknown");
+end
+
+function def_env.server:uptime()
+ 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 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
+
+function def_env.server:shutdown(reason)
+ prosody.shutdown(reason);
+ return true, "Shutdown initiated";
+end
+
+def_env.module = {};
+
+local function get_hosts_set(hosts, module)
+ if type(hosts) == "table" then
+ if hosts[1] then
+ return set.new(hosts);
+ elseif hosts._items then
+ return hosts;
+ end
+ elseif type(hosts) == "string" then
+ return set.new { hosts };
+ elseif hosts == nil then
+ local mm = require "modulemanager";
+ local hosts_set = set.new(array.collect(keys(prosody.hosts)))
+ / function (host) return (prosody.hosts[host].type == "local" or module and mm.is_loaded(host, module)) and host or nil; end;
+ if module and mm.get_module("*", module) then
+ hosts_set:add("*");
+ end
+ return hosts_set;
+ end
+end
+
+function def_env.module:load(name, hosts, config)
+ local mm = require "modulemanager";
+
+ hosts = get_hosts_set(hosts);
+
+ -- Load the module for each host
+ local ok, err, count, mod = true, nil, 0, nil;
+ for host in hosts do
+ if (not mm.is_loaded(host, name)) then
+ mod, err = mm.load(host, name, config);
+ if not mod then
+ ok = false;
+ if err == "global-module-already-loaded" then
+ if count > 0 then
+ ok, err, count = true, nil, 1;
+ end
+ break;
+ end
+ self.session.print(err or "Unknown error loading module");
+ else
+ count = count + 1;
+ self.session.print("Loaded for "..mod.module.host);
+ end
+ end
+ end
+
+ return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
+end
+
+function def_env.module:unload(name, hosts)
+ local mm = require "modulemanager";
+
+ hosts = get_hosts_set(hosts, name);
+
+ -- Unload the module for each host
+ local ok, err, count = true, nil, 0;
+ for host in hosts do
+ if mm.is_loaded(host, name) then
+ ok, err = mm.unload(host, name);
+ if not ok then
+ ok = false;
+ self.session.print(err or "Unknown error unloading module");
+ else
+ count = count + 1;
+ self.session.print("Unloaded from "..host);
+ end
+ end
+ end
+ return ok, (ok and "Module unloaded from "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
+end
+
+function def_env.module:reload(name, hosts)
+ local mm = require "modulemanager";
+
+ hosts = array.collect(get_hosts_set(hosts, name)):sort(function (a, b)
+ if a == "*" then return true
+ elseif b == "*" then return false
+ else return a < b; end
+ end);
+
+ -- Reload the module for each host
+ local ok, err, count = true, nil, 0;
+ for _, host in ipairs(hosts) do
+ if mm.is_loaded(host, name) then
+ ok, err = mm.reload(host, name);
+ if not ok then
+ ok = false;
+ self.session.print(err or "Unknown error reloading module");
+ else
+ count = count + 1;
+ if ok == nil then
+ ok = true;
+ end
+ self.session.print("Reloaded on "..host);
+ end
+ end
+ end
+ return ok, (ok and "Module reloaded on "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
+end
+
+function def_env.module:list(hosts)
+ if hosts == nil then
+ hosts = array.collect(keys(prosody.hosts));
+ table.insert(hosts, 1, "*");
+ end
+ if type(hosts) == "string" then
+ hosts = { hosts };
+ end
+ if type(hosts) ~= "table" then
+ return false, "Please supply a host or a list of hosts you would like to see";
+ end
+
+ local print = self.session.print;
+ for _, host in ipairs(hosts) do
+ print((host == "*" and "Global" or host)..":");
+ local modules = array.collect(keys(modulemanager.get_modules(host) or {})):sort();
+ if #modules == 0 then
+ if prosody.hosts[host] then
+ print(" No modules loaded");
+ else
+ print(" Host not found");
+ end
+ else
+ for _, name in ipairs(modules) do
+ print(" "..name);
+ end
+ end
+ end
+end
+
+def_env.config = {};
+function def_env.config:load(filename, format)
+ local config_load = require "core.configmanager".load;
+ local ok, err = config_load(filename, format);
+ if not ok then
+ return false, err or "Unknown error loading config";
+ end
+ return true, "Config loaded";
+end
+
+function def_env.config:get(host, section, key)
+ local config_get = require "core.configmanager".get
+ return true, tostring(config_get(host, section, key));
+end
+
+function def_env.config:reload()
+ local ok, err = prosody.reload_config();
+ return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err);
+end
+
+def_env.hosts = {};
+function def_env.hosts:list()
+ for host, host_session in pairs(hosts) do
+ self.session.print(host);
+ end
+ return true, "Done";
+end
+
+function def_env.hosts:add(name)
+end
+
+def_env.c2s = {};
+
+local function show_c2s(callback)
+ for hostname, host in pairs(hosts) do
+ for username, user in pairs(host.sessions or {}) do
+ for resource, session in pairs(user.sessions or {}) do
+ local jid = username.."@"..hostname.."/"..resource;
+ callback(jid, session);
+ end
+ end
+ end
+end
+
+function def_env.c2s:count(match_jid)
+ local count = 0;
+ show_c2s(function (jid, session)
+ if (not match_jid) or jid:match(match_jid) then
+ count = count + 1;
+ end
+ end);
+ return true, "Total: "..count.." clients";
+end
+
+function def_env.c2s:show(match_jid)
+ local print, count = self.session.print, 0;
+ local curr_host;
+ show_c2s(function (jid, session)
+ if curr_host ~= session.host then
+ curr_host = session.host;
+ print(curr_host);
+ end
+ if (not match_jid) or jid:match(match_jid) then
+ count = count + 1;
+ local status, priority = "unavailable", tostring(session.priority or "-");
+ 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
+ print(" "..jid.." - "..status.."("..priority..")");
+ end
+ end);
+ return true, "Total: "..count.." clients";
+end
+
+function def_env.c2s:show_insecure(match_jid)
+ local print, count = self.session.print, 0;
+ show_c2s(function (jid, session)
+ if ((not match_jid) or jid:match(match_jid)) and not session.secure then
+ count = count + 1;
+ print(jid);
+ end
+ end);
+ return true, "Total: "..count.." insecure client connections";
+end
+
+function def_env.c2s:show_secure(match_jid)
+ local print, count = self.session.print, 0;
+ show_c2s(function (jid, session)
+ if ((not match_jid) or jid:match(match_jid)) and session.secure then
+ count = count + 1;
+ print(jid);
+ end
+ end);
+ return true, "Total: "..count.." secure client connections";
+end
+
+function def_env.c2s:close(match_jid)
+ local count = 0;
+ show_c2s(function (jid, session)
+ if jid == match_jid or jid_bare(jid) == match_jid then
+ count = count + 1;
+ session:close();
+ end
+ end);
+ return true, "Total: "..count.." sessions closed";
+end
+
+local function session_flags(session, line)
+ if session.cert_identity_status == "valid" then
+ line[#line+1] = "(secure)";
+ elseif session.secure then
+ line[#line+1] = "(encrypted)";
+ end
+ if session.compressed then
+ line[#line+1] = "(compressed)";
+ end
+ if session.smacks then
+ line[#line+1] = "(sm)";
+ end
+ if session.conn and session.conn:ip():match(":") then
+ line[#line+1] = "(IPv6)";
+ end
+ return table.concat(line, " ");
+end
+
+def_env.s2s = {};
+function def_env.s2s:show(match_jid)
+ local _print = self.session.print;
+ local print = self.session.print;
+
+ local count_in, count_out = 0,0;
+
+ for host, host_session in pairs(hosts) do
+ print = function (...) _print(host); _print(...); print = _print; end
+ for remotehost, session in pairs(host_session.s2sout) do
+ if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then
+ count_out = count_out + 1;
+ print(session_flags(session, {" ", host, "->", remotehost}));
+ if session.sendq then
+ print(" There are "..#session.sendq.." queued outgoing stanzas for this connection");
+ end
+ if session.type == "s2sout_unauthed" then
+ if session.connecting then
+ print(" Connection not yet established");
+ if not session.srv_hosts then
+ if not session.conn then
+ print(" We do not yet have a DNS answer for this host's SRV records");
+ else
+ print(" This host has no SRV records, using A record instead");
+ end
+ elseif session.srv_choice then
+ print(" We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts);
+ local srv_choice = session.srv_hosts[session.srv_choice];
+ print(" Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269));
+ end
+ elseif session.notopen then
+ print(" The <stream> has not yet been opened");
+ elseif not session.dialback_key then
+ print(" Dialback has not been initiated yet");
+ elseif session.dialback_key then
+ print(" Dialback has been requested, but no result received");
+ end
+ end
+ end
+ end
+ local subhost_filter = function (h)
+ return (match_jid and h:match(match_jid));
+ end
+ for session in pairs(incoming_s2s) do
+ if session.to_host == host and ((not match_jid) or host:match(match_jid)
+ or (session.from_host and session.from_host:match(match_jid))
+ -- Pft! is what I say to list comprehensions
+ or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then
+ count_in = count_in + 1;
+ print(session_flags(session, {" ", host, "<-", session.from_host or "(unknown)"}));
+ if session.type == "s2sin_unauthed" then
+ print(" Connection not yet authenticated");
+ end
+ for name in pairs(session.hosts) do
+ if name ~= session.from_host then
+ print(" also hosts "..tostring(name));
+ end
+ end
+ end
+ end
+
+ print = _print;
+ end
+
+ for session in pairs(incoming_s2s) do
+ if not session.to_host and ((not match_jid) or session.from_host and session.from_host:match(match_jid)) then
+ count_in = count_in + 1;
+ print("Other incoming s2s connections");
+ print(" (unknown) <- "..(session.from_host or "(unknown)"));
+ end
+ end
+
+ return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections";
+end
+
+local function print_subject(print, subject)
+ for _, entry in ipairs(subject) do
+ print(
+ (" %s: %q"):format(
+ entry.name or entry.oid,
+ entry.value:gsub("[\r\n%z%c]", " ")
+ )
+ );
+ end
+end
+
+-- As much as it pains me to use the 0-based depths that OpenSSL does,
+-- I think there's going to be more confusion among operators if we
+-- break from that.
+local function print_errors(print, errors)
+ for depth, t in ipairs(errors) do
+ print(
+ (" %d: %s"):format(
+ depth-1,
+ table.concat(t, "\n| ")
+ )
+ );
+ end
+end
+
+function def_env.s2s:showcert(domain)
+ local ser = require "util.serialization".serialize;
+ local print = self.session.print;
+ local domain_sessions = set.new(array.collect(keys(incoming_s2s)))
+ /function(session) return session.from_host == domain and session or nil; end;
+ for local_host in values(prosody.hosts) do
+ local s2sout = local_host.s2sout;
+ if s2sout and s2sout[domain] then
+ domain_sessions:add(s2sout[domain]);
+ end
+ end
+ local cert_set = {};
+ for session in domain_sessions do
+ local conn = session.conn;
+ conn = conn and conn:socket();
+ if not conn.getpeerchain then
+ if conn.dohandshake then
+ error("This version of LuaSec does not support certificate viewing");
+ end
+ else
+ local certs = conn:getpeerchain();
+ local cert = certs[1];
+ if cert then
+ local digest = cert:digest("sha1");
+ if not cert_set[digest] then
+ local chain_valid, chain_errors = conn:getpeerverification();
+ cert_set[digest] = {
+ {
+ from = session.from_host,
+ to = session.to_host,
+ direction = session.direction
+ };
+ chain_valid = chain_valid;
+ chain_errors = chain_errors;
+ certs = certs;
+ };
+ else
+ table.insert(cert_set[digest], {
+ from = session.from_host,
+ to = session.to_host,
+ direction = session.direction
+ });
+ end
+ end
+ end
+ end
+ local domain_certs = array.collect(values(cert_set));
+ -- Phew. We now have a array of unique certificates presented by domain.
+ local n_certs = #domain_certs;
+
+ if n_certs == 0 then
+ return "No certificates found for "..domain;
+ end
+
+ local function _capitalize_and_colon(byte)
+ return string.upper(byte)..":";
+ end
+ local function pretty_fingerprint(hash)
+ return hash:gsub("..", _capitalize_and_colon):sub(1, -2);
+ end
+
+ for cert_info in values(domain_certs) do
+ local certs = cert_info.certs;
+ local cert = certs[1];
+ print("---")
+ print("Fingerprint (SHA1): "..pretty_fingerprint(cert:digest("sha1")));
+ print("");
+ local n_streams = #cert_info;
+ print("Currently used on "..n_streams.." stream"..(n_streams==1 and "" or "s")..":");
+ for _, stream in ipairs(cert_info) do
+ if stream.direction == "incoming" then
+ print(" "..stream.to.." <- "..stream.from);
+ else
+ print(" "..stream.from.." -> "..stream.to);
+ end
+ end
+ print("");
+ local chain_valid, errors = cert_info.chain_valid, cert_info.chain_errors;
+ local valid_identity = cert_verify_identity(domain, "xmpp-server", cert);
+ if chain_valid then
+ print("Trusted certificate: Yes");
+ else
+ print("Trusted certificate: No");
+ print_errors(print, errors);
+ end
+ print("");
+ print("Issuer: ");
+ print_subject(print, cert:issuer());
+ print("");
+ print("Valid for "..domain..": "..(valid_identity and "Yes" or "No"));
+ print("Subject:");
+ print_subject(print, cert:subject());
+ end
+ print("---");
+ return ("Showing "..n_certs.." certificate"
+ ..(n_certs==1 and "" or "s")
+ .." presented by "..domain..".");
+end
+
+function def_env.s2s:close(from, to)
+ local print, count = self.session.print, 0;
+
+ if not (from and to) then
+ return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'";
+ elseif from == to then
+ return false, "Both from and to are the same... you can't do that :)";
+ end
+
+ if hosts[from] and not hosts[to] then
+ -- Is an outgoing connection
+ local session = hosts[from].s2sout[to];
+ if not session then
+ print("No outgoing connection from "..from.." to "..to)
+ else
+ (session.close or s2smanager.destroy_session)(session);
+ count = count + 1;
+ print("Closed outgoing session from "..from.." to "..to);
+ end
+ elseif hosts[to] and not hosts[from] then
+ -- Is an incoming connection
+ for session in pairs(incoming_s2s) do
+ if session.to_host == to and session.from_host == from then
+ (session.close or s2smanager.destroy_session)(session);
+ count = count + 1;
+ end
+ end
+
+ if count == 0 then
+ print("No incoming connections from "..from.." to "..to);
+ else
+ print("Closed "..count.." incoming session"..((count == 1 and "") or "s").." from "..from.." to "..to);
+ end
+ elseif hosts[to] and hosts[from] then
+ return false, "Both of the hostnames you specified are local, there are no s2s sessions to close";
+ else
+ return false, "Neither of the hostnames you specified are being used on this server";
+ end
+
+ return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s");
+end
+
+function def_env.s2s:closeall(host)
+ local count = 0;
+
+ if not host or type(host) ~= "string" then return false, "wrong syntax: please use s2s:closeall('hostname.tld')"; end
+ if hosts[host] then
+ for session in pairs(incoming_s2s) do
+ if session.to_host == host then
+ (session.close or s2smanager.destroy_session)(session);
+ count = count + 1;
+ end
+ end
+ for _, session in pairs(hosts[host].s2sout) do
+ (session.close or s2smanager.destroy_session)(session);
+ count = count + 1;
+ end
+ else
+ for session in pairs(incoming_s2s) do
+ if session.from_host == host then
+ (session.close or s2smanager.destroy_session)(session);
+ count = count + 1;
+ end
+ end
+ for _, h in pairs(hosts) do
+ if h.s2sout[host] then
+ (h.s2sout[host].close or s2smanager.destroy_session)(h.s2sout[host]);
+ count = count + 1;
+ end
+ end
+ end
+
+ if count == 0 then return false, "No sessions to close.";
+ else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end
+end
+
+def_env.host = {}; def_env.hosts = def_env.host;
+
+function def_env.host:activate(hostname, config)
+ return hostmanager.activate(hostname, config);
+end
+function def_env.host:deactivate(hostname, reason)
+ return hostmanager.deactivate(hostname, reason);
+end
+
+function def_env.host:list()
+ local print = self.session.print;
+ local i = 0;
+ for host in values(array.collect(keys(prosody.hosts)):sort()) do
+ i = i + 1;
+ print(host);
+ end
+ return true, i.." hosts";
+end
+
+def_env.port = {};
+
+function def_env.port:list()
+ local print = self.session.print;
+ local services = portmanager.get_active_services().data;
+ local ordered_services, n_ports = {}, 0;
+ for service, interfaces in pairs(services) do
+ table.insert(ordered_services, service);
+ end
+ table.sort(ordered_services);
+ for _, service in ipairs(ordered_services) do
+ local ports_list = {};
+ for interface, ports in pairs(services[service]) do
+ for port in pairs(ports) do
+ table.insert(ports_list, "["..interface.."]:"..port);
+ end
+ end
+ n_ports = n_ports + #ports_list;
+ print(service..": "..table.concat(ports_list, ", "));
+ end
+ return true, #ordered_services.." services listening on "..n_ports.." ports";
+end
+
+function def_env.port:close(close_port, close_interface)
+ close_port = assert(tonumber(close_port), "Invalid port number");
+ local n_closed = 0;
+ local services = portmanager.get_active_services().data;
+ for service, interfaces in pairs(services) do
+ for interface, ports in pairs(interfaces) do
+ if not close_interface or close_interface == interface then
+ if ports[close_port] then
+ self.session.print("Closing ["..interface.."]:"..close_port.."...");
+ local ok, err = portmanager.close(interface, close_port)
+ if not ok then
+ self.session.print("Failed to close "..interface.." "..close_port..": "..err);
+ else
+ n_closed = n_closed + 1;
+ end
+ end
+ end
+ end
+ end
+ return true, "Closed "..n_closed.." ports";
+end
+
+def_env.muc = {};
+
+local console_room_mt = {
+ __index = function (self, k) return self.room[k]; end;
+ __tostring = function (self)
+ return "MUC room <"..self.room.jid..">";
+ end;
+};
+
+local function check_muc(jid)
+ local room_name, host = jid_split(jid);
+ if not hosts[host] then
+ return nil, "No such host: "..host;
+ elseif not hosts[host].modules.muc then
+ return nil, "Host '"..host.."' is not a MUC service";
+ end
+ return room_name, host;
+end
+
+function def_env.muc:create(room_jid)
+ local room, host = check_muc(room_jid);
+ return hosts[host].modules.muc.create_room(room_jid);
+end
+
+function def_env.muc:room(room_jid)
+ local room_name, host = check_muc(room_jid);
+ local room_obj = hosts[host].modules.muc.rooms[room_jid];
+ if not room_obj then
+ return nil, "No such room: "..room_jid;
+ end
+ return setmetatable({ room = room_obj }, console_room_mt);
+end
+
+local um = require"core.usermanager";
+
+def_env.user = {};
+function def_env.user:create(jid, password)
+ local username, host = jid_split(jid);
+ if um.user_exists(username, host) then
+ return nil, "User exists";
+ end
+ local ok, err = um.create_user(username, password, host);
+ if ok then
+ return true, "User created";
+ else
+ return nil, "Could not create user: "..err;
+ end
+end
+
+function def_env.user:delete(jid)
+ local username, host = jid_split(jid);
+ if not um.user_exists(username, host) then
+ return nil, "No such user";
+ end
+ local ok, err = um.delete_user(username, host);
+ if ok then
+ return true, "User deleted";
+ else
+ return nil, "Could not delete user: "..err;
+ end
+end
+
+function def_env.user:password(jid, password)
+ local username, host = jid_split(jid);
+ if not um.user_exists(username, host) then
+ return nil, "No such user";
+ end
+ local ok, err = um.set_password(username, password, host);
+ if ok then
+ return true, "User password changed";
+ else
+ return nil, "Could not change password for user: "..err;
+ end
+end
+
+function def_env.user:list(host, pat)
+ if not host then
+ return nil, "No host given";
+ elseif not hosts[host] then
+ return nil, "No such host";
+ end
+ local print = self.session.print;
+ local total, matches = 0, 0;
+ for user in um.users(host) do
+ if not pat or user:match(pat) then
+ print(user.."@"..host);
+ matches = matches + 1;
+ end
+ total = total + 1;
+ end
+ return true, "Showing "..(pat and (matches.." of ") or "all " )..total.." users";
+end
+
+def_env.xmpp = {};
+
+local st = require "util.stanza";
+function def_env.xmpp:ping(localhost, remotehost)
+ if hosts[localhost] then
+ core_post_stanza(hosts[localhost],
+ st.iq{ from=localhost, to=remotehost, type="get", id="ping" }
+ :tag("ping", {xmlns="urn:xmpp:ping"}));
+ return true, "Sent ping";
+ else
+ return nil, "No such host";
+ end
+end
+
+-------------
+
+function printbanner(session)
+ local option = module:get_option("console_banner");
+ if option == nil or option == "full" or option == "graphic" then
+ session.print [[
+ ____ \ / _
+ | _ \ _ __ ___ ___ _-_ __| |_ _
+ | |_) | '__/ _ \/ __|/ _ \ / _` | | | |
+ | __/| | | (_) \__ \ |_| | (_| | |_| |
+ |_| |_| \___/|___/\___/ \__,_|\__, |
+ A study in simplicity |___/
+
+]]
+ end
+ if option == nil or option == "short" or option == "full" then
+ session.print("Welcome to the Prosody administration console. For a list of commands, type: help");
+ session.print("You may find more help on using this console in our online documentation at ");
+ session.print("http://prosody.im/doc/console\n");
+ end
+ if option and option ~= "short" and option ~= "full" and option ~= "graphic" then
+ if type(option) == "string" then
+ session.print(option)
+ elseif type(option) == "function" then
+ module:log("warn", "Using functions as value for the console_banner option is no longer supported");
+ end
+ end
+end
+
+module:provides("net", {
+ name = "console";
+ listener = console_listener;
+ default_port = 5582;
+ private = true;
+});
diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua
new file mode 100644
index 00000000..96976d6f
--- /dev/null
+++ b/plugins/mod_announce.lua
@@ -0,0 +1,101 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st, jid = require "util.stanza", require "util.jid";
+
+local hosts = prosody.hosts;
+local is_admin = require "core.usermanager".is_admin;
+
+function send_to_online(message, host)
+ 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;
+ module:send(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
+ end
+
+ if not is_admin(stanza.attr.from) then
+ -- Not an admin? Not allowed!
+ module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from);
+ return;
+ end
+
+ module:log("info", "Sending server announcement to all online users");
+ local message = st.clone(stanza);
+ message.attr.type = "headline";
+ message.attr.from = host;
+
+ local c = send_to_online(message, host);
+ module:log("info", "Announcement sent to %d online users", c);
+ return true;
+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", actions = {"next", "complete", default = "complete"}, 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:provides("adhoc", announce_desc);
+
diff --git a/plugins/mod_auth_anonymous.lua b/plugins/mod_auth_anonymous.lua
new file mode 100644
index 00000000..c877d532
--- /dev/null
+++ b/plugins/mod_auth_anonymous.lua
@@ -0,0 +1,71 @@
+-- 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 new_sasl = require "util.sasl".new;
+local datamanager = require "util.datamanager";
+local hosts = prosody.hosts;
+
+-- define auth provider
+local provider = {};
+
+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
+
+function provider.users()
+ return next, hosts[host].sessions, nil;
+end
+
+-- datamanager callback to disable writes
+local function dm_callback(username, host, datastore, data)
+ if host == module.host then
+ return false;
+ end
+ return username, host, datastore, data;
+end
+
+if not module:get_option_boolean("allow_anonymous_s2s", false) then
+ module:hook("route/remote", function (event)
+ return false; -- Block outgoing s2s from anonymous users
+ end, 300);
+end
+
+function module.load()
+ datamanager.add_callback(dm_callback);
+end
+function module.unload()
+ datamanager.remove_callback(dm_callback);
+end
+
+module:provides("auth", provider);
+
diff --git a/plugins/mod_auth_cyrus.lua b/plugins/mod_auth_cyrus.lua
new file mode 100644
index 00000000..7668f8c4
--- /dev/null
+++ b/plugins/mod_auth_cyrus.lua
@@ -0,0 +1,84 @@
+-- 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;
+local host_fqdn = module:get_option("cyrus_server_fqdn");
+
+prosody.unlock_globals(); --FIXME: Figure out why this is needed and
+ -- why cyrussasl isn't caught by the sandbox
+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",
+ host_fqdn
+ );
+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
+
+local host = module.host;
+
+-- define auth provider
+local provider = {};
+log("debug", "initializing default authentication provider for host '%s'", host);
+
+function provider.test_password(username, password)
+ return nil, "Legacy auth not supported with Cyrus SASL.";
+end
+
+function provider.get_password(username)
+ return nil, "Passwords unavailable for Cyrus SASL.";
+end
+
+function provider.set_password(username, password)
+ return nil, "Passwords unavailable for Cyrus SASL.";
+end
+
+function provider.user_exists(username)
+ if require_provisioning then
+ return usermanager_user_exists(username, host);
+ end
+ return true;
+end
+
+function provider.create_user(username, password)
+ return nil, "Account creation/modification not available with Cyrus SASL.";
+end
+
+function provider.get_sasl_handler()
+ local handler = new_sasl(host);
+ if require_provisioning then
+ function handler.require_provisioning(username)
+ return usermanager_user_exists(username, host);
+ end
+ end
+ return handler;
+end
+
+module:provides("auth", provider);
+
diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua
new file mode 100644
index 00000000..2b041e43
--- /dev/null
+++ b/plugins/mod_auth_internal_hashed.lua
@@ -0,0 +1,148 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- Copyright (C) 2010 Jeff Mitchell
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local log = require "util.logger".init("auth_internal_hashed");
+local getAuthenticationDatabaseSHA1 = require "util.sasl.scram".getAuthenticationDatabaseSHA1;
+local usermanager = require "core.usermanager";
+local generate_uuid = require "util.uuid".generate;
+local new_sasl = require "util.sasl".new;
+
+local accounts = module:open_store("accounts");
+
+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
+
+
+-- Default; can be set per-user
+local iteration_count = 4096;
+
+local host = module.host;
+-- define auth provider
+local provider = {};
+log("debug", "initializing internal_hashed authentication provider for host '%s'", host);
+
+function provider.test_password(username, password)
+ local credentials = accounts:get(username) 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
+
+ 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 = accounts:get(username);
+ if account then
+ account.salt = account.salt or generate_uuid();
+ account.iteration_count = account.iteration_count or iteration_count;
+ local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, account.salt, account.iteration_count);
+ local stored_key_hex = to_hex(stored_key);
+ local server_key_hex = to_hex(server_key);
+
+ account.stored_key = stored_key_hex
+ account.server_key = server_key_hex
+
+ account.password = nil;
+ return accounts:set(username, account);
+ end
+ return nil, "Account not available.";
+end
+
+function provider.user_exists(username)
+ local account = accounts:get(username);
+ if not account then
+ log("debug", "account not found for username '%s' at host '%s'", username, host);
+ return nil, "Auth failed. Invalid username";
+ end
+ return true;
+end
+
+function provider.users()
+ return accounts:users();
+end
+
+function provider.create_user(username, password)
+ if password == nil then
+ return accounts:set(username, {});
+ end
+ local salt = generate_uuid();
+ local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, iteration_count);
+ local stored_key_hex = to_hex(stored_key);
+ local server_key_hex = to_hex(server_key);
+ return accounts:set(username, {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = iteration_count});
+end
+
+function provider.delete_user(username)
+ return accounts:set(username, nil);
+end
+
+function provider.get_sasl_handler()
+ local testpass_authentication_profile = {
+ plain_test = function(sasl, username, password, realm)
+ return usermanager.test_password(username, realm, password), true;
+ end,
+ scram_sha_1 = function(sasl, username, realm)
+ local credentials = accounts:get(username);
+ if not credentials then return; end
+ if credentials.password then
+ usermanager.set_password(username, credentials.password, host);
+ credentials = accounts:get(username);
+ if not credentials then return; end
+ 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(host, testpass_authentication_profile);
+end
+
+module:provides("auth", provider);
+
diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua
new file mode 100644
index 00000000..d226fdbe
--- /dev/null
+++ b/plugins/mod_auth_internal_plain.lua
@@ -0,0 +1,81 @@
+-- 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 usermanager = require "core.usermanager";
+local new_sasl = require "util.sasl".new;
+
+local log = module._log;
+local host = module.host;
+
+local accounts = module:open_store("accounts");
+
+-- define auth provider
+local provider = {};
+log("debug", "initializing internal_plain authentication provider for host '%s'", host);
+
+function provider.test_password(username, password)
+ log("debug", "test password for user %s at host %s", username, host);
+ local credentials = accounts:get(username) 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, host);
+ return (accounts:get(username) or {}).password;
+end
+
+function provider.set_password(username, password)
+ local account = accounts:get(username);
+ if account then
+ account.password = password;
+ return accounts:set(username, account);
+ end
+ return nil, "Account not available.";
+end
+
+function provider.user_exists(username)
+ local account = accounts:get(username);
+ if not account then
+ log("debug", "account not found for username '%s' at host '%s'", username, host);
+ return nil, "Auth failed. Invalid username";
+ end
+ return true;
+end
+
+function provider.users()
+ return accounts:users();
+end
+
+function provider.create_user(username, password)
+ return accounts:set(username, {password = password});
+end
+
+function provider.delete_user(username)
+ return accounts:set(username, nil);
+end
+
+function provider.get_sasl_handler()
+ local getpass_authentication_profile = {
+ plain = function(sasl, username, realm)
+ local password = usermanager.get_password(username, realm);
+ if not password then
+ return "", nil;
+ end
+ return password, true;
+ end
+ };
+ return new_sasl(host, getpass_authentication_profile);
+end
+
+module:provides("auth", provider);
+
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua
new file mode 100644
index 00000000..19f191c8
--- /dev/null
+++ b/plugins/mod_bosh.lua
@@ -0,0 +1,455 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+module:set_global(); -- Global module
+
+local hosts = _G.hosts;
+local new_xmpp_stream = require "util.xmppstream".new;
+local sm = require "core.sessionmanager";
+local sm_destroy_session = sm.destroy_session;
+local new_uuid = require "util.uuid".generate;
+local fire_event = prosody.events.fire_event;
+local core_process_stanza = prosody.core_process_stanza;
+local st = require "util.stanza";
+local logger = require "util.logger";
+local log = logger.init("mod_bosh");
+local initialize_filters = require "util.filters".initialize;
+local math_min = math.min;
+
+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 = xmlns_bosh, stream_tag = "body", default_ns = "jabber:client" };
+
+local BOSH_DEFAULT_HOLD = module:get_option_number("bosh_default_hold", 1);
+local BOSH_DEFAULT_INACTIVITY = module:get_option_number("bosh_max_inactivity", 60);
+local BOSH_DEFAULT_POLLING = module:get_option_number("bosh_max_polling", 5);
+local BOSH_DEFAULT_REQUESTS = module:get_option_number("bosh_max_requests", 2);
+local bosh_max_wait = module:get_option_number("bosh_max_wait", 120);
+
+local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure");
+
+local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8", ["Connection"] = "keep-alive" };
+
+local cross_domain = module:get_option("cross_domain_bosh", false);
+if cross_domain then
+ default_headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
+ default_headers["Access-Control-Allow-Headers"] = "Content-Type";
+ default_headers["Access-Control-Max-Age"] = "7200";
+
+ if cross_domain == true then
+ default_headers["Access-Control-Allow-Origin"] = "*";
+ elseif type(cross_domain) == "table" then
+ cross_domain = table.concat(cross_domain, ", ");
+ end
+ if type(cross_domain) == "string" then
+ default_headers["Access-Control-Allow-Origin"] = cross_domain;
+ end
+end
+
+local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items;
+
+local function get_ip_from_request(request)
+ local ip = request.conn:ip();
+ local forwarded_for = request.headers.x_forwarded_for;
+ if forwarded_for then
+ forwarded_for = forwarded_for..", "..ip;
+ for forwarded_ip in forwarded_for:gmatch("[^%s,]+") do
+ 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;
+
+-- All sessions, and sessions that have no requests open
+local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions");
+
+-- Used to respond to idle sessions (those with waiting requests)
+local waiting_requests = {};
+function on_destroy_request(request)
+ log("debug", "Request destroyed: %s", tostring(request));
+ waiting_requests[request] = nil;
+ local session = sessions[request.context.sid];
+ if session then
+ local requests = session.requests;
+ for i, r in ipairs(requests) do
+ if r == request then
+ t_remove(requests, i);
+ break;
+ end
+ end
+
+ -- If this session now has no requests open, mark it as inactive
+ local max_inactive = session.bosh_max_inactive;
+ if max_inactive and #requests == 0 then
+ inactive_sessions[session] = os_time() + max_inactive;
+ (session.log or log)("debug", "BOSH session marked as inactive (for %ds)", max_inactive);
+ end
+ end
+end
+
+function handle_OPTIONS(request)
+ local headers = {};
+ for k,v in pairs(default_headers) do headers[k] = v; end
+ headers["Content-Type"] = nil;
+ return { headers = headers, body = "" };
+end
+
+function handle_POST(event)
+ log("debug", "Handling new request %s: %s\n----------", tostring(event.request), tostring(event.request.body));
+
+ local request, response = event.request, event.response;
+ response.on_destroy = on_destroy_request;
+ local body = request.body;
+
+ local context = { request = request, response = response, notopen = true };
+ local stream = new_xmpp_stream(context, stream_callbacks);
+ response.context = context;
+
+ -- stream:feed() calls the stream_callbacks, so all stanzas in
+ -- the body are processed in this next line before it returns.
+ -- In particular, the streamopened() stream callback is where
+ -- much of the session logic happens, because it's where we first
+ -- get to see the 'sid' of this request.
+ stream:feed(body);
+
+ -- Stanzas (if any) in the request have now been processed, and
+ -- we take care of the high-level BOSH logic here, including
+ -- giving a response or putting the request "on hold".
+ local session = sessions[context.sid];
+ if session then
+ -- Session was marked as inactive, since we have
+ -- a request open now, unmark it
+ if inactive_sessions[session] and #session.requests > 0 then
+ inactive_sessions[session] = nil;
+ end
+
+ local r = session.requests;
+ log("debug", "Session %s has %d out of %d requests open", context.sid, #r, session.bosh_hold);
+ log("debug", "and there are %d things in the send_buffer:", #session.send_buffer);
+ for i, thing in ipairs(session.send_buffer) do
+ log("debug", " %s", tostring(thing));
+ end
+ if #r > session.bosh_hold then
+ -- We are holding too many requests, send what's in the buffer,
+ log("debug", "We are holding too many requests, so...");
+ if #session.send_buffer > 0 then
+ log("debug", "...sending what is in the buffer")
+ session.send(t_concat(session.send_buffer));
+ session.send_buffer = {};
+ else
+ -- or an empty response
+ log("debug", "...sending an empty response");
+ session.send("");
+ end
+ elseif #session.send_buffer > 0 then
+ log("debug", "Session has data in the send buffer, will send now..");
+ local resp = t_concat(session.send_buffer);
+ session.send_buffer = {};
+ session.send(resp);
+ end
+
+ if not response.finished then
+ -- We're keeping this request open, to respond later
+ log("debug", "Have nothing to say, so leaving request unanswered for now");
+ if session.bosh_wait then
+ waiting_requests[response] = os_time() + session.bosh_wait;
+ end
+ end
+
+ if session.bosh_terminate then
+ session.log("debug", "Closing session with %d requests open", #session.requests);
+ session:close();
+ return nil;
+ else
+ return true; -- Inform http server we shall reply later
+ end
+ end
+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");
+
+ local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+ ["xmlns:stream"] = 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 response_body = tostring(close_reply);
+ for _, held_request in ipairs(session.requests) do
+ held_request.headers = default_headers;
+ held_request:send(response_body);
+ end
+ sessions[session.sid] = nil;
+ inactive_sessions[session] = nil;
+ sm_destroy_session(session);
+end
+
+-- Handle the <body> tag in the request payload.
+function stream_callbacks.streamopened(context, attr)
+ local request, response = context.request, context.response;
+ local sid = attr.sid;
+ log("debug", "BOSH body open (sid: %s)", sid or "<none>");
+ if not sid then
+ -- New session request
+ context.notopen = nil; -- Signals that we accept this opening tag
+
+ -- TODO: Sanity checks here (rid, to, known host, etc.)
+ if not hosts[attr.to] then
+ -- Unknown host
+ log("debug", "BOSH client tried to connect to unknown host: %s", tostring(attr.to));
+ local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+ ["xmlns:stream"] = xmlns_streams, condition = "host-unknown" });
+ response:send(tostring(close_reply));
+ return;
+ end
+
+ -- New session
+ sid = new_uuid();
+ local session = {
+ type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid)-1, host = attr.to,
+ bosh_version = attr.ver, bosh_wait = math_min(attr.wait, bosh_max_wait), streamid = sid,
+ bosh_hold = BOSH_DEFAULT_HOLD, bosh_max_inactive = BOSH_DEFAULT_INACTIVITY,
+ requests = { }, send_buffer = {}, reset_stream = bosh_reset_stream,
+ close = bosh_close_stream, dispatch_stanza = core_process_stanza, notopen = true,
+ log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure,
+ ip = get_ip_from_request(request);
+ };
+ sessions[sid] = session;
+
+ local filter = initialize_filters(session);
+
+ session.log("debug", "BOSH session created for request from %s", session.ip);
+ log("info", "New BOSH session, assigned it sid '%s'", sid);
+
+ -- Send creation response
+ local creating_session = true;
+
+ local r = session.requests;
+ function session.send(s)
+ -- We need to ensure that outgoing stanzas have the jabber:client xmlns
+ if s.attr and not s.attr.xmlns then
+ s = st.clone(s);
+ s.attr.xmlns = "jabber:client";
+ end
+ s = filter("stanzas/out", s);
+ --log("debug", "Sending BOSH data: %s", tostring(s));
+ t_insert(session.send_buffer, tostring(s));
+
+ local oldest_request = r[1];
+ if oldest_request and not session.bosh_processing then
+ log("debug", "We have an open request, so sending on that");
+ oldest_request.headers = default_headers;
+ local body_attr = { xmlns = "http://jabber.org/protocol/httpbind",
+ ["xmlns:stream"] = "http://etherx.jabber.org/streams";
+ type = session.bosh_terminate and "terminate" or nil;
+ sid = sid;
+ };
+ if creating_session then
+ body_attr.inactivity = tostring(BOSH_DEFAULT_INACTIVITY);
+ body_attr.polling = tostring(BOSH_DEFAULT_POLLING);
+ body_attr.requests = tostring(BOSH_DEFAULT_REQUESTS);
+ body_attr.wait = tostring(session.bosh_wait);
+ body_attr.hold = tostring(session.bosh_hold);
+ body_attr.authid = sid;
+ body_attr.secure = "true";
+ body_attr.ver = '1.6'; from = session.host;
+ body_attr["xmlns:xmpp"] = "urn:xmpp:xbosh";
+ body_attr["xmpp:version"] = "1.0";
+ end
+ oldest_request:send(st.stanza("body", body_attr):top_tag()..t_concat(session.send_buffer).."</body>");
+ session.send_buffer = {};
+ end
+ return true;
+ end
+ request.sid = sid;
+ end
+
+ local session = sessions[sid];
+ if not session then
+ -- Unknown sid
+ log("info", "Client tried to use sid '%s' which we don't know about", sid);
+ response.headers = default_headers;
+ response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" })));
+ context.notopen = nil;
+ return;
+ end
+
+ if session.rid then
+ local rid = tonumber(attr.rid);
+ local diff = rid - session.rid;
+ if diff > 1 then
+ session.log("warn", "rid too large (means a request was lost). Last rid: %d New rid: %s", session.rid, attr.rid);
+ elseif diff <= 0 then
+ -- Repeated, ignore
+ session.log("debug", "rid repeated (on request %s), ignoring: %s (diff %d)", request.id, session.rid, diff);
+ context.notopen = nil;
+ context.ignore = true;
+ context.sid = sid;
+ t_insert(session.requests, response);
+ return;
+ end
+ session.rid = rid;
+ end
+
+ if attr.type == "terminate" then
+ -- Client wants to end this session, which we'll do
+ -- after processing any stanzas in this request
+ session.bosh_terminate = true;
+ end
+
+ context.notopen = nil; -- Signals that we accept this opening tag
+ t_insert(session.requests, response);
+ context.sid = sid;
+ session.bosh_processing = true; -- Used to suppress replies until processing of this request is done
+
+ if session.notopen then
+ local features = st.stanza("stream:features");
+ hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
+ fire_event("stream-features", session, features);
+ table.insert(session.send_buffer, tostring(features));
+ session.notopen = nil;
+ end
+end
+
+function stream_callbacks.handlestanza(context, stanza)
+ if context.ignore then return; end
+ log("debug", "BOSH stanza received: %s\n", stanza:top_tag());
+ local session = sessions[context.sid];
+ if session then
+ if stanza.attr.xmlns == xmlns_bosh then
+ stanza.attr.xmlns = nil;
+ end
+ stanza = session.filter("stanzas/in", stanza);
+ core_process_stanza(session, stanza);
+ end
+end
+
+function stream_callbacks.streamclosed(request)
+ local session = sessions[request.sid];
+ if session then
+ session.bosh_processing = false;
+ if #session.send_buffer > 0 then
+ session.send("");
+ end
+ end
+end
+
+function stream_callbacks.error(context, error)
+ log("debug", "Error parsing BOSH request payload; %s", error);
+ if not context.sid then
+ local response = context.response;
+ response.headers = default_headers;
+ response.status_code = 400;
+ response:send();
+ return;
+ end
+
+ local session = sessions[context.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...");
+ -- Identify requests timing out within the next few seconds
+ local now = os_time() + 3;
+ for request, reply_before in pairs(waiting_requests) do
+ if reply_before <= now then
+ log("debug", "%s was soon to timeout (at %d, now %d), sending empty response", tostring(request), reply_before, now);
+ -- Send empty response to let the
+ -- client know we're still here
+ if request.conn then
+ sessions[request.context.sid].send("");
+ end
+ end
+ end
+
+ now = now - 3;
+ local n_dead_sessions = 0;
+ for session, close_after in pairs(inactive_sessions) do
+ if close_after < now then
+ (session.log or log)("debug", "BOSH client inactive too long, destroying session at %d", now);
+ sessions[session.sid] = nil;
+ inactive_sessions[session] = nil;
+ n_dead_sessions = n_dead_sessions + 1;
+ dead_sessions[n_dead_sessions] = session;
+ end
+ end
+
+ for i=1,n_dead_sessions do
+ local session = dead_sessions[i];
+ dead_sessions[i] = nil;
+ sm_destroy_session(session, "BOSH client silent for over "..session.bosh_max_inactive.." seconds");
+ end
+ return 1;
+end
+module:add_timer(1, on_timer);
+
+
+local GET_response = {
+ headers = {
+ content_type = "text/html";
+ };
+ body = [[<html><body>
+ <p>It works! Now point your BOSH client to this URL to connect to Prosody.</p>
+ <p>For more information see <a href="http://prosody.im/doc/setting_up_bosh">Prosody: Setting up BOSH</a>.</p>
+ </body></html>]];
+};
+
+function module.add_host(module)
+ module:depends("http");
+ module:provides("http", {
+ default_path = "/http-bind";
+ route = {
+ ["GET"] = GET_response;
+ ["GET /"] = GET_response;
+ ["OPTIONS"] = handle_OPTIONS;
+ ["OPTIONS /"] = handle_OPTIONS;
+ ["POST"] = handle_POST;
+ ["POST /"] = handle_POST;
+ };
+ });
+end
diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua
new file mode 100644
index 00000000..efef8763
--- /dev/null
+++ b/plugins/mod_c2s.lua
@@ -0,0 +1,297 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+module:set_global();
+
+local add_task = require "util.timer".add_task;
+local new_xmpp_stream = require "util.xmppstream".new;
+local nameprep = require "util.encodings".stringprep.nameprep;
+local sessionmanager = require "core.sessionmanager";
+local st = require "util.stanza";
+local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session;
+local uuid_generate = require "util.uuid".generate;
+
+local xpcall, tostring, type = xpcall, tostring, type;
+local traceback = debug.traceback;
+
+local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
+
+local log = module._log;
+
+local c2s_timeout = module:get_option_number("c2s_timeout");
+local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5);
+local opt_keepalives = module:get_option_boolean("tcp_keepalives", false);
+
+local sessions = module:shared("sessions");
+local core_process_stanza = prosody.core_process_stanza;
+local hosts = prosody.hosts;
+
+local stream_callbacks = { default_ns = "jabber:client", handlestanza = core_process_stanza };
+local listener = {};
+
+--- Stream events handlers
+local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
+local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
+
+function stream_callbacks.streamopened(session, attr)
+ local send = session.send;
+ session.host = nameprep(attr.to);
+ if not session.host then
+ session:close{ condition = "improper-addressing",
+ text = "A valid 'to' attribute is required on stream headers" };
+ return;
+ end
+ session.version = tonumber(attr.version) or 0;
+ session.streamid = uuid_generate();
+ (session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host);
+
+ if not hosts[session.host] then
+ -- We don't serve this host...
+ session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)};
+ return;
+ end
+
+ send("<?xml version='1.0'?>"..st.stanza("stream:stream", {
+ xmlns = 'jabber:client', ["xmlns:stream"] = 'http://etherx.jabber.org/streams';
+ id = session.streamid, from = session.host, version = '1.0', ["xml:lang"] = 'en' }):top_tag());
+
+ (session.log or log)("debug", "Sent reply <stream:stream> to client");
+ session.notopen = nil;
+
+ -- If session.secure is *false* (not nil) then it means we /were/ encrypting
+ -- since we now have a new stream header, session is secured
+ if session.secure == false then
+ session.secure = true;
+
+ -- Check if TLS compression is used
+ local sock = session.conn:socket();
+ if sock.info then
+ session.compressed = sock:info"compression";
+ elseif sock.compression then
+ session.compressed = sock:compression(); --COMPAT mw/luasec-hg
+ end
+ end
+
+ local features = st.stanza("stream:features");
+ hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
+ module:fire_event("stream-features", session, features);
+
+ send(features);
+end
+
+function stream_callbacks.streamclosed(session)
+ session.log("debug", "Received </stream:stream>");
+ session:close(false);
+end
+
+function stream_callbacks.error(session, error, data)
+ if error == "no-stream" then
+ session.log("debug", "Invalid opening stream header");
+ session:close("invalid-namespace");
+ elseif error == "parse-error" then
+ (session.log or log)("debug", "Client XML parse error: %s", tostring(data));
+ session:close("not-well-formed");
+ elseif error == "stream-error" then
+ local condition, text = "undefined-condition";
+ for child in data:children() do
+ if child.attr.xmlns == xmlns_xmpp_streams then
+ if child.name ~= "text" then
+ condition = child.name;
+ else
+ text = child:get_text();
+ end
+ if condition ~= "undefined-condition" and text then
+ break;
+ end
+ end
+ end
+ text = condition .. (text and (" ("..text..")") or "");
+ session.log("info", "Session closed by remote with error: %s", text);
+ session:close(nil, text);
+ end
+end
+
+local function handleerr(err) log("error", "Traceback[c2s]: %s", traceback(tostring(err), 2)); end
+function stream_callbacks.handlestanza(session, stanza)
+ stanza = session.filter("stanzas/in", stanza);
+ if stanza then
+ return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
+ end
+end
+
+--- Session methods
+local function session_close(session, reason)
+ local log = session.log or log;
+ if session.conn then
+ if session.notopen then
+ session.send("<?xml version='1.0'?>");
+ session.send(st.stanza("stream:stream", default_stream_attr):top_tag());
+ end
+ if reason then -- nil == no err, initiated by us, false == initiated by client
+ local stream_error = st.stanza("stream:error");
+ if type(reason) == "string" then -- assume stream error
+ stream_error:tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' });
+ elseif type(reason) == "table" then
+ if reason.condition then
+ stream_error:tag(reason.condition, stream_xmlns_attr):up();
+ if reason.text then
+ stream_error:tag("text", stream_xmlns_attr):text(reason.text):up();
+ end
+ if reason.extra then
+ stream_error:add_child(reason.extra);
+ end
+ elseif reason.name then -- a stanza
+ stream_error = reason;
+ end
+ end
+ stream_error = tostring(stream_error);
+ log("debug", "Disconnecting client, <stream:error> is: %s", stream_error);
+ session.send(stream_error);
+ end
+
+ session.send("</stream:stream>");
+ function session.send() return false; end
+
+ local reason = (reason and (reason.text or reason.condition)) or reason;
+ session.log("info", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed");
+
+ -- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
+ local conn = session.conn;
+ if reason == nil and not session.notopen and session.type == "c2s" then
+ -- Grace time to process data from authenticated cleanly-closed stream
+ add_task(stream_close_timeout, function ()
+ if not session.destroyed then
+ session.log("warn", "Failed to receive a stream close response, closing connection anyway...");
+ sm_destroy_session(session, reason);
+ conn:close();
+ end
+ end);
+ else
+ sm_destroy_session(session, reason);
+ conn:close();
+ end
+ end
+end
+
+module:hook_global("user-deleted", function(event)
+ local username, host = event.username, event.host;
+ local user = hosts[host].sessions[username];
+ if user and user.sessions then
+ for jid, session in pairs(user.sessions) do
+ session:close{ condition = "not-authorized", text = "Account deleted" };
+ end
+ end
+end, 200);
+
+--- Port listener
+function listener.onconnect(conn)
+ local session = sm_new_session(conn);
+ sessions[conn] = session;
+
+ session.log("info", "Client connected");
+
+ -- Client is using legacy SSL (otherwise mod_tls sets this flag)
+ if conn:ssl() then
+ session.secure = true;
+
+ -- Check if TLS compression is used
+ local sock = conn:socket();
+ if sock.info then
+ session.compressed = sock:info"compression";
+ elseif sock.compression then
+ session.compressed = sock:compression(); --COMPAT mw/luasec-hg
+ end
+ end
+
+ if opt_keepalives then
+ conn:setoption("keepalive", opt_keepalives);
+ end
+
+ session.close = session_close;
+
+ local stream = new_xmpp_stream(session, stream_callbacks);
+ session.stream = stream;
+ session.notopen = true;
+
+ function session.reset_stream()
+ session.notopen = true;
+ session.stream:reset();
+ end
+
+ local filter = session.filter;
+ function session.data(data)
+ data = filter("bytes/in", data);
+ if data then
+ local ok, err = stream:feed(data);
+ if ok then return; end
+ log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
+ session:close("not-well-formed");
+ end
+ end
+
+
+ if c2s_timeout then
+ add_task(c2s_timeout, function ()
+ if session.type == "c2s_unauthed" then
+ session:close("connection-timeout");
+ end
+ end);
+ end
+
+ session.dispatch_stanza = stream_callbacks.handlestanza;
+end
+
+function listener.onincoming(conn, data)
+ local session = sessions[conn];
+ if session then
+ session.data(data);
+ end
+end
+
+function listener.ondisconnect(conn, err)
+ local session = sessions[conn];
+ if session then
+ (session.log or log)("info", "Client disconnected: %s", err or "connection closed");
+ sm_destroy_session(session, err);
+ sessions[conn] = nil;
+ end
+end
+
+function listener.associate_session(conn, session)
+ sessions[conn] = session;
+end
+
+module:hook("server-stopping", function(event)
+ local reason = event.reason;
+ for _, session in pairs(sessions) do
+ session:close{ condition = "system-shutdown", text = reason };
+ end
+end, 1000);
+
+
+
+module:provides("net", {
+ name = "c2s";
+ listener = listener;
+ default_port = 5222;
+ encryption = "starttls";
+ multiplex = {
+ pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:client%1.*>";
+ };
+});
+
+module:provides("net", {
+ name = "legacy_ssl";
+ listener = listener;
+ encryption = "ssl";
+ multiplex = {
+ pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:client%1.*>";
+ };
+});
+
+
diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua
new file mode 100644
index 00000000..871a20e4
--- /dev/null
+++ b/plugins/mod_component.lua
@@ -0,0 +1,318 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+module:set_global();
+
+local t_concat = table.concat;
+
+local logger = require "util.logger";
+local sha1 = require "util.hashes".sha1;
+local st = require "util.stanza";
+
+local jid_split = require "util.jid".split;
+local new_xmpp_stream = require "util.xmppstream".new;
+local uuid_gen = require "util.uuid".generate;
+
+local core_process_stanza = prosody.core_process_stanza;
+local hosts = prosody.hosts;
+
+local log = module._log;
+
+local sessions = module:shared("sessions");
+
+function module.add_host(module)
+ if module:get_host_type() ~= "component" then
+ error("Don't load mod_component manually, it should be for a component, please see http://prosody.im/doc/components", 0);
+ end
+
+ local env = module.environment;
+ env.connected = false;
+
+ local send;
+
+ local function on_destroy(session, err)
+ env.connected = false;
+ send = nil;
+ session.on_destroy = nil;
+ end
+
+ -- Handle authentication attempts by component
+ local function handle_component_auth(event)
+ local session, stanza = event.origin, event.stanza;
+
+ if session.type ~= "component_unauthed" then return; end
+
+ if (not session.host) or #stanza.tags > 0 then
+ (session.log or log)("warn", "Invalid component handshake for host: %s", session.host);
+ session:close("not-authorized");
+ return true;
+ end
+
+ local secret = module:get_option("component_secret");
+ if not secret then
+ (session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.host);
+ session:close("not-authorized");
+ return true;
+ end
+
+ local supplied_token = t_concat(stanza);
+ local calculated_token = sha1(session.streamid..secret, true);
+ if supplied_token:lower() ~= calculated_token:lower() then
+ module:log("info", "Component authentication failed for %s", session.host);
+ session:close{ condition = "not-authorized", text = "Given token does not match calculated token" };
+ return true;
+ end
+
+ if env.connected then
+ module:log("error", "Second component attempted to connect, denying connection");
+ session:close{ condition = "conflict", text = "Component already connected" };
+ return true;
+ end
+
+ env.connected = true;
+ send = session.send;
+ session.on_destroy = on_destroy;
+ session.component_validate_from = module:get_option_boolean("validate_from_addresses", true);
+ session.type = "component";
+ module:log("info", "External component successfully authenticated");
+ session.send(st.stanza("handshake"));
+
+ return true;
+ end
+ module:hook("stanza/jabber:component:accept:handshake", handle_component_auth);
+
+ -- Handle stanzas addressed to this component
+ local function handle_stanza(event)
+ local stanza = event.stanza;
+ if send then
+ stanza.attr.xmlns = nil;
+ send(stanza);
+ else
+ if stanza.name == "iq" and stanza.attr.type == "get" and stanza.attr.to == module.host then
+ local query = stanza.tags[1];
+ local node = query.attr.node;
+ if query.name == "query" and query.attr.xmlns == "http://jabber.org/protocol/disco#info" and (not node or node == "") then
+ local name = module:get_option_string("name");
+ if name then
+ event.origin.send(st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info" })
+ :tag("identity", { category = "component", type = "generic", name = module:get_option_string("name", "Prosody") }))
+ return true;
+ end
+ end
+ end
+ module:log("warn", "Component not connected, bouncing error for: %s", stanza:top_tag());
+ if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
+ event.origin.send(st.error_reply(stanza, "wait", "service-unavailable", "Component unavailable"));
+ end
+ end
+ return true;
+ end
+
+ module:hook("iq/bare", handle_stanza, -1);
+ module:hook("message/bare", handle_stanza, -1);
+ module:hook("presence/bare", handle_stanza, -1);
+ module:hook("iq/full", handle_stanza, -1);
+ module:hook("message/full", handle_stanza, -1);
+ module:hook("presence/full", handle_stanza, -1);
+ module:hook("iq/host", handle_stanza, -1);
+ module:hook("message/host", handle_stanza, -1);
+ module:hook("presence/host", handle_stanza, -1);
+end
+
+--- Network and stream part ---
+
+local xmlns_component = 'jabber:component:accept';
+
+local listener = {};
+
+--- Callbacks/data for xmppstream to handle streams for us ---
+
+local stream_callbacks = { default_ns = xmlns_component };
+
+local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
+
+function stream_callbacks.error(session, error, data, data2)
+ if session.destroyed then return; end
+ module:log("warn", "Error processing component stream: %s", tostring(error));
+ if error == "no-stream" then
+ session:close("invalid-namespace");
+ elseif error == "parse-error" then
+ session.log("warn", "External component %s XML parse error: %s", tostring(session.host), tostring(data));
+ session:close("not-well-formed");
+ elseif error == "stream-error" then
+ local condition, text = "undefined-condition";
+ for child in data:children() do
+ if child.attr.xmlns == xmlns_xmpp_streams then
+ if child.name ~= "text" then
+ condition = child.name;
+ else
+ text = child:get_text();
+ end
+ if condition ~= "undefined-condition" and text then
+ break;
+ end
+ end
+ end
+ text = condition .. (text and (" ("..text..")") or "");
+ session.log("info", "Session closed by remote with error: %s", text);
+ session:close(nil, text);
+ end
+end
+
+function stream_callbacks.streamopened(session, attr)
+ if not hosts[attr.to] or not hosts[attr.to].modules.component then
+ session:close{ condition = "host-unknown", text = tostring(attr.to).." does not match any configured external components" };
+ return;
+ end
+ session.host = attr.to;
+ session.streamid = uuid_gen();
+ session.notopen = nil;
+ -- Return stream header
+ session.send("<?xml version='1.0'?>");
+ session.send(st.stanza("stream:stream", { xmlns=xmlns_component,
+ ["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.host }):top_tag());
+end
+
+function stream_callbacks.streamclosed(session)
+ session.log("debug", "Received </stream:stream>");
+ session:close();
+end
+
+function stream_callbacks.handlestanza(session, stanza)
+ -- Namespaces are icky.
+ if not stanza.attr.xmlns and stanza.name == "handshake" then
+ stanza.attr.xmlns = xmlns_component;
+ end
+ if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then
+ local from = stanza.attr.from;
+ if from then
+ if session.component_validate_from then
+ local _, domain = jid_split(stanza.attr.from);
+ if domain ~= session.host then
+ -- Return error
+ session.log("warn", "Component sent stanza with missing or invalid 'from' address");
+ session:close{
+ condition = "invalid-from";
+ text = "Component tried to send from address <"..tostring(from)
+ .."> which is not in domain <"..tostring(session.host)..">";
+ };
+ return;
+ end
+ end
+ else
+ stanza.attr.from = session.host; -- COMPAT: Strictly we shouldn't allow this
+ end
+ if not stanza.attr.to then
+ session.log("warn", "Rejecting stanza with no 'to' address");
+ session.send(st.error_reply(stanza, "modify", "bad-request", "Components MUST specify a 'to' address on stanzas"));
+ return;
+ end
+ end
+ return core_process_stanza(session, stanza);
+end
+
+--- Closing a component connection
+local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
+local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
+local function session_close(session, reason)
+ if session.destroyed then return; end
+ if session.conn then
+ if session.notopen then
+ session.send("<?xml version='1.0'?>");
+ session.send(st.stanza("stream:stream", default_stream_attr):top_tag());
+ end
+ if reason then
+ if type(reason) == "string" then -- assume stream error
+ module:log("info", "Disconnecting component, <stream:error> is: %s", reason);
+ session.send(st.stanza("stream:error"):tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' }));
+ elseif type(reason) == "table" then
+ if reason.condition then
+ local stanza = st.stanza("stream:error"):tag(reason.condition, stream_xmlns_attr):up();
+ if reason.text then
+ stanza:tag("text", stream_xmlns_attr):text(reason.text):up();
+ end
+ if reason.extra then
+ stanza:add_child(reason.extra);
+ end
+ module:log("info", "Disconnecting component, <stream:error> is: %s", tostring(stanza));
+ session.send(stanza);
+ elseif reason.name then -- a stanza
+ module:log("info", "Disconnecting component, <stream:error> is: %s", tostring(reason));
+ session.send(reason);
+ end
+ end
+ end
+ session.send("</stream:stream>");
+ session.conn:close();
+ listener.ondisconnect(session.conn, "stream error");
+ end
+end
+
+--- Component connlistener
+
+function listener.onconnect(conn)
+ local _send = conn.write;
+ local session = { type = "component_unauthed", conn = conn, send = function (data) return _send(conn, tostring(data)); end };
+
+ -- Logging functions --
+ local conn_name = "jcp"..tostring(session):match("[a-f0-9]+$");
+ session.log = logger.init(conn_name);
+ session.close = session_close;
+
+ session.log("info", "Incoming Jabber component connection");
+
+ local stream = new_xmpp_stream(session, stream_callbacks);
+ session.stream = stream;
+
+ session.notopen = true;
+
+ function session.reset_stream()
+ session.notopen = true;
+ session.stream:reset();
+ end
+
+ function session.data(conn, data)
+ local ok, err = stream:feed(data);
+ if ok then return; end
+ module:log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
+ session:close("not-well-formed");
+ end
+
+ session.dispatch_stanza = stream_callbacks.handlestanza;
+
+ sessions[conn] = session;
+end
+function listener.onincoming(conn, data)
+ local session = sessions[conn];
+ session.data(conn, data);
+end
+function listener.ondisconnect(conn, err)
+ local session = sessions[conn];
+ if session then
+ (session.log or log)("info", "component disconnected: %s (%s)", tostring(session.host), tostring(err));
+ if session.on_destroy then session:on_destroy(err); end
+ sessions[conn] = nil;
+ for k in pairs(session) do
+ if k ~= "log" and k ~= "close" then
+ session[k] = nil;
+ end
+ end
+ session.destroyed = true;
+ session = nil;
+ end
+end
+
+module:provides("net", {
+ name = "component";
+ private = true;
+ listener = listener;
+ default_port = 5347;
+ multiplex = {
+ pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:component:accept%1.*>";
+ };
+});
diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua
new file mode 100644
index 00000000..92856099
--- /dev/null
+++ b/plugins/mod_compression.lua
@@ -0,0 +1,195 @@
+-- Prosody IM
+-- Copyright (C) 2009-2012 Tobias Markmann
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza";
+local zlib = require "zlib";
+local pcall = pcall;
+local tostring = tostring;
+
+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_number("compression_level", 7);
+
+if not compression_level or compression_level < 1 or compression_level > 9 then
+ module:log("warn", "Invalid compression level in config: %s", tostring(compression_level));
+ module:log("warn", "Module loading aborted. Compression won't be available.");
+ return;
+end
+
+module:hook("stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if not origin.compressed and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then
+ -- FIXME only advertise compression support when TLS layer has no compression enabled
+ features:add_child(compression_stream_feature);
+ end
+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 and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then
+ features:add_child(compression_stream_feature);
+ end
+end);
+
+-- Hook to activate compression if remote server supports it.
+module:hook_stanza(xmlns_stream, "features",
+ function (session, stanza)
+ if not session.compressed and (session.type == "c2s" or session.type == "s2sin" or session.type == "s2sout") then
+ -- does remote server support compression?
+ local comp_st = stanza:child_with_name("compression");
+ if comp_st then
+ -- do we support the mechanism
+ for a in comp_st:children() do
+ local algorithm = a[1]
+ if algorithm == "zlib" then
+ session.sends2s(st.stanza("compress", {xmlns=xmlns_compression_protocol}):tag("method"):text("zlib"))
+ session.log("debug", "Enabled compression using zlib.")
+ return true;
+ end
+ end
+ session.log("debug", "Remote server supports no compression algorithm we support.")
+ end
+ end
+ end
+, 250);
+
+
+-- returns either nil or a fully functional ready to use inflate stream
+local function get_deflate_stream(session)
+ local status, deflate_stream = pcall(zlib.deflate, compression_level);
+ if status == false then
+ local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
+ (session.sends2s or session.send)(error_st);
+ session.log("error", "Failed to create zlib.deflate filter.");
+ module:log("error", "%s", tostring(deflate_stream));
+ return
+ end
+ return deflate_stream
+end
+
+-- returns either nil or a fully functional ready to use inflate stream
+local function get_inflate_stream(session)
+ local status, inflate_stream = pcall(zlib.inflate);
+ if status == false then
+ local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
+ (session.sends2s or session.send)(error_st);
+ session.log("error", "Failed to create zlib.inflate filter.");
+ module:log("error", "%s", tostring(inflate_stream));
+ return
+ end
+ return inflate_stream
+end
+
+-- setup compression for a stream
+local function setup_compression(session, deflate_stream)
+ 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)
+ 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: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();
+ session:open_stream(session.from_host, session.to_host);
+ 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 true; end
+
+ local inflate_stream = get_inflate_stream(session);
+ if not inflate_stream then return true; end
+
+ (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
+ session:reset_stream();
+
+ -- setup compression for session.w
+ setup_compression(session, deflate_stream);
+
+ -- setup decompression for session.data
+ setup_decompression(session, inflate_stream);
+
+ session.compressed = true;
+ elseif method then
+ session.log("debug", "%s compression selected, but we don't support it.", tostring(method));
+ 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
new file mode 100644
index 00000000..9dcb0ed5
--- /dev/null
+++ b/plugins/mod_dialback.lua
@@ -0,0 +1,181 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local hosts = _G.hosts;
+
+local log = module._log;
+
+local st = require "util.stanza";
+local sha256_hash = require "util.hashes".sha256;
+local nameprep = require "util.encodings".stringprep.nameprep;
+
+local xmlns_stream = "http://etherx.jabber.org/streams";
+
+local dialback_requests = setmetatable({}, { __mode = 'v' });
+
+function generate_dialback(id, to, from)
+ return sha256_hash(id..to..from..hosts[from].dialback_secret, true);
+end
+
+function initiate_dialback(session)
+ -- generate dialback key
+ session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host);
+ session.sends2s(st.stanza("db:result", { from = session.from_host, to = session.to_host }):text(session.dialback_key));
+ session.log("info", "sent dialback key on outgoing s2s stream");
+end
+
+function verify_dialback(id, to, from, key)
+ return key == generate_dialback(id, to, from);
+end
+
+module:hook("stanza/jabber:server:dialback:verify", function(event)
+ local origin, stanza = event.origin, event.stanza;
+
+ if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then
+ -- We are being asked to verify the key, to ensure it was generated by us
+ origin.log("debug", "verifying that dialback key is ours...");
+ local attr = stanza.attr;
+ if attr.type then
+ module:log("warn", "Ignoring incoming session from %s claiming a dialback key for %s is %s",
+ origin.from_host or "(unknown)", attr.from or "(unknown)", attr.type);
+ return true;
+ end
+ -- COMPAT: Grr, ejabberd breaks this one too?? it is black and white in XEP-220 example 34
+ --if attr.from ~= origin.to_host then error("invalid-from"); end
+ local type;
+ if verify_dialback(attr.id, attr.from, attr.to, stanza[1]) then
+ type = "valid"
+ else
+ type = "invalid"
+ origin.log("warn", "Asked to verify a dialback key that was incorrect. An imposter is claiming to be %s?", attr.to);
+ 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]));
+ return true;
+ end
+end);
+
+module:hook("stanza/jabber:server:dialback:result", function(event)
+ local origin, stanza = event.origin, event.stanza;
+
+ if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then
+ -- he wants to be identified through dialback
+ -- We need to check the key with the Authoritative server
+ local attr = stanza.attr;
+ local to, from = nameprep(attr.to), nameprep(attr.from);
+
+ if not hosts[to] then
+ -- Not a host that we serve
+ origin.log("info", "%s tried to connect to %s, which we don't serve", from, to);
+ origin:close("host-unknown");
+ return true;
+ elseif not from then
+ origin:close("improper-addressing");
+ end
+
+ origin.hosts[from] = { dialback_key = stanza[1] };
+
+ dialback_requests[from.."/"..origin.streamid] = origin;
+
+ -- COMPAT: ejabberd, gmail and perhaps others do not always set 'to' and 'from'
+ -- on streams. We fill in the session's to/from here instead.
+ if not origin.from_host then
+ origin.from_host = from;
+ end
+ if not origin.to_host then
+ origin.to_host = to;
+ end
+
+ origin.log("debug", "asking %s if key %s belongs to them", from, stanza[1]);
+ module:fire_event("route/remote", {
+ from_host = to, to_host = from;
+ stanza = st.stanza("db:verify", { from = to, to = from, id = origin.streamid }):text(stanza[1]);
+ });
+ return true;
+ end
+end);
+
+module:hook("stanza/jabber:server:dialback:verify", function(event)
+ local origin, stanza = event.origin, event.stanza;
+
+ if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then
+ local attr = stanza.attr;
+ local dialback_verifying = dialback_requests[attr.from.."/"..(attr.id or "")];
+ if dialback_verifying and attr.from == origin.to_host then
+ local valid;
+ if attr.type == "valid" then
+ module:fire_event("s2s-authenticated", { session = dialback_verifying, host = attr.from });
+ valid = "valid";
+ else
+ -- Warn the original connection that is was not verified successfully
+ log("warn", "authoritative server for %s denied the key", attr.from or "(unknown)");
+ valid = "invalid";
+ end
+ if dialback_verifying.destroyed then
+ log("warn", "Incoming s2s session %s was closed in the meantime, so we can't notify it of the db result", tostring(dialback_verifying):match("%w+$"));
+ else
+ dialback_verifying.sends2s(
+ st.stanza("db:result", { from = attr.to, to = attr.from, id = attr.id, type = valid })
+ :text(dialback_verifying.hosts[attr.from].dialback_key));
+ end
+ dialback_requests[attr.from.."/"..(attr.id or "")] = nil;
+ end
+ return true;
+ end
+end);
+
+module:hook("stanza/jabber:server:dialback:result", function(event)
+ local origin, stanza = event.origin, event.stanza;
+
+ if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then
+ -- Remote server is telling us whether we passed dialback
+
+ local attr = stanza.attr;
+ if not hosts[attr.to] then
+ origin:close("host-unknown");
+ return true;
+ elseif hosts[attr.to].s2sout[attr.from] ~= origin then
+ -- This isn't right
+ origin:close("invalid-id");
+ return true;
+ end
+ if stanza.attr.type == "valid" then
+ module:fire_event("s2s-authenticated", { session = origin, host = attr.from });
+ else
+ origin:close("not-authorized", "dialback authentication failed");
+ end
+ return true;
+ end
+end);
+
+module:hook_stanza("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza)
+ if origin.external_auth == "failed" then
+ module:log("debug", "SASL EXTERNAL failed, falling back to dialback");
+ initiate_dialback(origin);
+ return true;
+ end
+end, 100);
+
+module:hook_stanza(xmlns_stream, "features", function (origin, stanza)
+ if not origin.external_auth or origin.external_auth == "failed" then
+ module:log("debug", "Initiating dialback...");
+ initiate_dialback(origin);
+ return true;
+ end
+end, 100);
+
+module:hook("s2sout-authenticate-legacy", function (event)
+ module:log("debug", "Initiating dialback...");
+ initiate_dialback(event.origin);
+ return true;
+end, 100);
+
+-- Offer dialback to incoming hosts
+module:hook("s2s-stream-features", function (data)
+ data.features:tag("dialback", { xmlns='urn:xmpp:features:dialback' }):up();
+end);
diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua
new file mode 100644
index 00000000..72c9a34c
--- /dev/null
+++ b/plugins/mod_disco.lua
@@ -0,0 +1,159 @@
+-- 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 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
+ for _, item in ipairs(disco_items) do
+ local err;
+ if type(item) ~= "table" then
+ err = "item is not a table";
+ elseif type(item[1]) ~= "string" then
+ err = "item jid is not a string";
+ elseif item[2] and type(item[2]) ~= "string" then
+ err = "item name is not a string";
+ end
+ if err then
+ module:log("error", "option disco_items is malformed: %s", err);
+ disco_items = {}; -- TODO clean up data instead of removing it?
+ break;
+ end
+ end
+end
+
+module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router
+module:add_feature("http://jabber.org/protocol/disco#info");
+module:add_feature("http://jabber.org/protocol/disco#items");
+
+-- 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
+ 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
+ query:tag("feature", {var=feature}):up();
+ done[feature] = true;
+ end
+ end
+ for _,extension in ipairs(module:get_host_items("extension")) do
+ if not done[extension] then
+ query:add_child(extension);
+ done[extension] = true;
+ end
+ end
+ _cached_server_disco_info = query;
+ _cached_server_caps_hash = calculate_hash(query);
+ _cached_server_caps_feature = st.stanza("c", {
+ 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-added/extension", clear_disco_cache);
+module:hook("item-removed/identity", clear_disco_cache);
+module:hook("item-removed/feature", clear_disco_cache);
+module:hook("item-removed/extension", clear_disco_cache);
+
+-- Handle disco requests to the server
+module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type ~= "get" then return; end
+ local node = stanza.tags[1].attr.node;
+ if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then return; end -- TODO fire event?
+ 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);
+module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type ~= "get" then return; end
+ local node = stanza.tags[1].attr.node;
+ if node and node ~= "" then return; end -- TODO fire event?
+
+ local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
+ for jid, name in pairs(get_children(module.host)) do
+ reply:tag("item", {jid = jid, name = name~=true and name or nil}):up();
+ end
+ for _, item in ipairs(disco_items) do
+ reply:tag("item", {jid=item[1], name=item[2]}):up();
+ end
+ 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
+ local node = stanza.tags[1].attr.node;
+ if node and node ~= "" then return; end -- TODO fire event?
+ local username = jid_split(stanza.attr.to) or origin.username;
+ if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+ local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'});
+ if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
+ module:fire_event("account-disco-info", { origin = origin, stanza = reply });
+ origin.send(reply);
+ return true;
+ end
+end);
+module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type ~= "get" then return; end
+ local node = stanza.tags[1].attr.node;
+ if node and node ~= "" then return; end -- TODO fire event?
+ local username = jid_split(stanza.attr.to) or origin.username;
+ if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+ local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items'});
+ if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
+ module:fire_event("account-disco-items", { origin = origin, stanza = reply });
+ origin.send(reply);
+ return true;
+ end
+end);
diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua
new file mode 100644
index 00000000..f7f632c2
--- /dev/null
+++ b/plugins/mod_groups.lua
@@ -0,0 +1,128 @@
+-- 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 groups;
+local members;
+
+local groups_file;
+
+local jid, datamanager = require "util.jid", require "util.datamanager";
+local jid_prep = jid.prep;
+
+local module_host = module:get_host();
+
+function inject_roster_contacts(username, host, roster)
+ --module:log("debug", "Injecting group members to roster");
+ local bare_jid = username.."@"..host;
+ if not members[bare_jid] and not members[false] then return; end -- Not a member of any groups
+
+ local function import_jids_to_roster(group_name)
+ for jid in pairs(groups[group_name]) do
+ -- Add them to roster
+ --module:log("debug", "processing jid %s in group %s", tostring(jid), tostring(group_name));
+ 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
+ roster[jid].groups[group_name] = true;
+ roster[jid].persist = false;
+ end
+ end
+ end
+
+ -- Find groups this JID is a member of
+ if members[bare_jid] then
+ for _, group_name in ipairs(members[bare_jid]) do
+ --module:log("debug", "Importing group %s", group_name);
+ import_jids_to_roster(group_name);
+ end
+ end
+
+ -- Import public groups
+ if members[false] then
+ for _, group_name in ipairs(members[false]) do
+ --module:log("debug", "Importing group %s", group_name);
+ import_jids_to_roster(group_name);
+ end
+ end
+
+ if roster[false] then
+ roster[false].version = true;
+ end
+end
+
+function remove_virtual_contacts(username, host, datastore, data)
+ if host == module_host and datastore == "roster" then
+ local new_roster = {};
+ for jid, contact in pairs(data) do
+ if contact.persist ~= false then
+ new_roster[jid] = contact;
+ end
+ end
+ if new_roster[false] then
+ new_roster[false].version = nil; -- Version is void
+ end
+ return username, host, datastore, new_roster;
+ end
+
+ return username, host, datastore, data;
+end
+
+function module.load()
+ groups_file = module:get_option_string("groups_file");
+ if not groups_file then return; end
+
+ module:hook("roster-load", inject_roster_contacts);
+ datamanager.add_callback(remove_virtual_contacts);
+
+ groups = { default = {} };
+ members = { };
+ local curr_group = "default";
+ for line in io.lines(groups_file) do
+ if line:match("^%s*%[.-%]%s*$") then
+ curr_group = line:match("^%s*%[(.-)%]%s*$");
+ if curr_group:match("^%+") then
+ curr_group = curr_group:gsub("^%+", "");
+ if not members[false] then
+ members[false] = {};
+ end
+ members[false][#members[false]+1] = curr_group; -- Is a public group
+ end
+ module:log("debug", "New group: %s", tostring(curr_group));
+ groups[curr_group] = groups[curr_group] or {};
+ else
+ -- Add JID
+ 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] = name or false;
+ members[jid] = members[jid] or {};
+ members[jid][#members[jid]+1] = curr_group;
+ end
+ end
+ end
+ module:log("info", "Groups loaded successfully");
+end
+
+function module.unload()
+ datamanager.remove_callback(remove_virtual_contacts);
+end
+
+-- Public for other modules to access
+function group_contains(group_name, jid)
+ return groups[group_name][jid];
+end
diff --git a/plugins/mod_http.lua b/plugins/mod_http.lua
new file mode 100644
index 00000000..0689634e
--- /dev/null
+++ b/plugins/mod_http.lua
@@ -0,0 +1,146 @@
+-- Prosody IM
+-- Copyright (C) 2008-2012 Matthew Wild
+-- Copyright (C) 2008-2012 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+module:set_global();
+module:depends("http_errors");
+
+local portmanager = require "core.portmanager";
+local moduleapi = require "core.moduleapi";
+local url_parse = require "socket.url".parse;
+local url_build = require "socket.url".build;
+
+local server = require "net.http.server";
+
+server.set_default_host(module:get_option_string("http_default_host"));
+
+local function normalize_path(path)
+ if path:sub(-1,-1) == "/" then path = path:sub(1, -2); end
+ if path:sub(1,1) ~= "/" then path = "/"..path; end
+ return path;
+end
+
+local function get_http_event(host, app_path, key)
+ local method, path = key:match("^(%S+)%s+(.+)$");
+ if not method then -- No path specified, default to "" (base path)
+ method, path = key, "";
+ end
+ if method:sub(1,1) == "/" then
+ return nil;
+ end
+ if app_path == "/" and path:sub(1,1) == "/" then
+ app_path = "";
+ end
+ return method:upper().." "..host..app_path..path;
+end
+
+local function get_base_path(host_module, app_name, default_app_path)
+ return (normalize_path(host_module:get_option("http_paths", {})[app_name] -- Host
+ or module:get_option("http_paths", {})[app_name] -- Global
+ or default_app_path)) -- Default
+ :gsub("%$(%w+)", { host = module.host });
+end
+
+local ports_by_scheme = { http = 80, https = 443, };
+
+-- Helper to deduce a module's external URL
+function moduleapi.http_url(module, app_name, default_path)
+ app_name = app_name or (module.name:gsub("^http_", ""));
+ local external_url = url_parse(module:get_option_string("http_external_url")) or {};
+ local services = portmanager.get_active_services();
+ local http_services = services:get("https") or services:get("http") or {};
+ for interface, ports in pairs(http_services) do
+ for port, services in pairs(ports) do
+ local url = {
+ scheme = (external_url.scheme or services[1].service.name);
+ host = (external_url.host or module:get_option_string("http_host", module.host));
+ port = tonumber(external_url.port) or port or 80;
+ path = normalize_path(external_url.path or "/")..
+ (get_base_path(module, app_name, default_path or "/"..app_name):sub(2));
+ }
+ if ports_by_scheme[url.scheme] == url.port then url.port = nil end
+ return url_build(url);
+ end
+ end
+end
+
+function module.add_host(module)
+ local host = module:get_option_string("http_host", module.host);
+ local apps = {};
+ module.environment.apps = apps;
+ local function http_app_added(event)
+ local app_name = event.item.name;
+ local default_app_path = event.item.default_path or "/"..app_name;
+ local app_path = get_base_path(module, app_name, default_app_path);
+ if not app_name then
+ -- TODO: Link to docs
+ module:log("error", "HTTP app has no 'name', add one or use module:provides('http', app)");
+ return;
+ end
+ apps[app_name] = apps[app_name] or {};
+ local app_handlers = apps[app_name];
+ for key, handler in pairs(event.item.route or {}) do
+ local event_name = get_http_event(host, app_path, key);
+ if event_name then
+ if type(handler) ~= "function" then
+ local data = handler;
+ handler = function () return data; end
+ elseif event_name:sub(-2, -1) == "/*" then
+ local base_path_len = #event_name:match("/.+$");
+ local _handler = handler;
+ handler = function (event)
+ local path = event.request.path:sub(base_path_len);
+ return _handler(event, path);
+ end;
+ end
+ if not app_handlers[event_name] then
+ app_handlers[event_name] = handler;
+ module:hook_object_event(server, event_name, handler);
+ else
+ module:log("warn", "App %s added handler twice for '%s', ignoring", app_name, event_name);
+ end
+ else
+ module:log("error", "Invalid route in %s, %q. See http://prosody.im/doc/developers/http#routes", app_name, key);
+ end
+ end
+ end
+
+ local function http_app_removed(event)
+ local app_handlers = apps[event.item.name];
+ apps[event.item.name] = nil;
+ for event, handler in pairs(app_handlers) do
+ module:unhook_object_event(server, event, handler);
+ end
+ end
+
+ module:handle_items("http-provider", http_app_added, http_app_removed);
+
+ server.add_host(host);
+ function module.unload()
+ server.remove_host(host);
+ end
+end
+
+module:provides("net", {
+ name = "http";
+ listener = server.listener;
+ default_port = 5280;
+ multiplex = {
+ pattern = "^[A-Z]";
+ };
+});
+
+module:provides("net", {
+ name = "https";
+ listener = server.listener;
+ default_port = 5281;
+ encryption = "ssl";
+ ssl_config = { verify = "none" };
+ multiplex = {
+ pattern = "^[A-Z]";
+ };
+});
diff --git a/plugins/mod_http_errors.lua b/plugins/mod_http_errors.lua
new file mode 100644
index 00000000..2568ea80
--- /dev/null
+++ b/plugins/mod_http_errors.lua
@@ -0,0 +1,75 @@
+module:set_global();
+
+local server = require "net.http.server";
+local codes = require "net.http.codes";
+
+local show_private = module:get_option_boolean("http_errors_detailed", false);
+local always_serve = module:get_option_boolean("http_errors_always_show", true);
+local default_message = { module:get_option_string("http_errors_default_message", "That's all I know.") };
+local default_messages = {
+ [400] = { "What kind of request do you call that??" };
+ [403] = { "You're not allowed to do that." };
+ [404] = { "Whatever you were looking for is not here. %";
+ "Where did you put it?", "It's behind you.", "Keep looking." };
+ [500] = { "% Check your error log for more info.";
+ "Gremlins.", "It broke.", "Don't look at me." };
+};
+
+local messages = setmetatable(module:get_option("http_errors_messages", {}), { __index = default_messages });
+
+local html = [[
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <style>
+ body{
+ margin-top:14%;
+ text-align:center;
+ background-color:#F8F8F8;
+ font-family:sans-serif;
+ }
+ h1{
+ font-size:xx-large;
+ }
+ p{
+ font-size:x-large;
+ }
+ p+p { font-size: large; font-family: courier }
+ </style>
+</head>
+<body>
+ <h1>$title</h1>
+ <p>$message</p>
+ <p>$extra</p>
+</body>
+</html>]];
+html = html:gsub("%s%s+", "");
+
+local entities = {
+ ["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;",
+ ["'"] = "&apos;", ["\""] = "&quot;", ["\n"] = "<br/>",
+};
+
+local function tohtml(plain)
+ return (plain:gsub("[<>&'\"\n]", entities));
+
+end
+
+local function get_page(code, extra)
+ local message = messages[code];
+ if always_serve or message then
+ message = message or default_message;
+ return (html:gsub("$(%a+)", {
+ title = rawget(codes, code) or ("Code "..tostring(code));
+ message = message[1]:gsub("%%", function ()
+ return message[math.random(2, math.max(#message,2))];
+ end);
+ extra = tohtml(extra or "");
+ }));
+ end
+end
+
+module:hook_object_event(server, "http-error", function (event)
+ return get_page(event.code, (show_private and event.private_message) or event.message);
+end);
diff --git a/plugins/mod_http_files.lua b/plugins/mod_http_files.lua
new file mode 100644
index 00000000..915bec58
--- /dev/null
+++ b/plugins/mod_http_files.lua
@@ -0,0 +1,153 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+module:depends("http");
+local server = require"net.http.server";
+local lfs = require "lfs";
+
+local os_date = os.date;
+local open = io.open;
+local stat = lfs.attributes;
+local build_path = require"socket.url".build_path;
+
+local base_path = module:get_option_string("http_files_dir", module:get_option_string("http_path"));
+local dir_indices = module:get_option("http_index_files", { "index.html", "index.htm" });
+local directory_index = module:get_option_boolean("http_dir_listing");
+
+local mime_map = module:shared("mime").types;
+if not mime_map then
+ mime_map = {
+ html = "text/html", htm = "text/html",
+ xml = "application/xml",
+ txt = "text/plain",
+ css = "text/css",
+ js = "application/javascript",
+ png = "image/png",
+ gif = "image/gif",
+ jpeg = "image/jpeg", jpg = "image/jpeg",
+ svg = "image/svg+xml",
+ };
+ module:shared("mime").types = mime_map;
+
+ local mime_types, err = open(module:get_option_string("mime_types_file", "/etc/mime.types"),"r");
+ if mime_types then
+ local mime_data = mime_types:read("*a");
+ mime_types:close();
+ setmetatable(mime_map, {
+ __index = function(t, ext)
+ local typ = mime_data:match("\n(%S+)[^\n]*%s"..(ext:lower()).."%s") or "application/octet-stream";
+ t[ext] = typ;
+ return typ;
+ end
+ });
+ end
+end
+
+local cache = setmetatable({}, { __mode = "kv" }); -- Let the garbage collector have it if it wants to.
+
+function serve(opts)
+ if type(opts) ~= "table" then -- assume path string
+ opts = { path = opts };
+ end
+ local base_path = opts.path;
+ local dir_indices = opts.index_files or dir_indices;
+ local directory_index = opts.directory_index;
+ local function serve_file(event, path)
+ local request, response = event.request, event.response;
+ local orig_path = request.path;
+ local full_path = base_path .. (path and "/"..path or "");
+ local attr = stat(full_path);
+ if not attr then
+ return 404;
+ end
+
+ local request_headers, response_headers = request.headers, response.headers;
+
+ local last_modified = os_date('!%a, %d %b %Y %H:%M:%S GMT', attr.modification);
+ response_headers.last_modified = last_modified;
+
+ local etag = ("%02x-%x-%x-%x"):format(attr.dev or 0, attr.ino or 0, attr.size or 0, attr.modification or 0);
+ response_headers.etag = etag;
+
+ local if_none_match = request_headers.if_none_match
+ local if_modified_since = request_headers.if_modified_since;
+ if etag == if_none_match
+ or (not if_none_match and last_modified == if_modified_since) then
+ return 304;
+ end
+
+ local data = cache[orig_path];
+ if data and data.etag == etag then
+ response_headers.content_type = data.content_type;
+ data = data.data;
+ elseif attr.mode == "directory" and path then
+ if full_path:sub(-1) ~= "/" then
+ local path = { is_absolute = true, is_directory = true };
+ for dir in orig_path:gmatch("[^/]+") do path[#path+1]=dir; end
+ response_headers.location = build_path(path);
+ return 301;
+ end
+ for i=1,#dir_indices do
+ if stat(full_path..dir_indices[i], "mode") == "file" then
+ return serve_file(event, path..dir_indices[i]);
+ end
+ end
+
+ if directory_index then
+ data = server._events.fire_event("directory-index", { path = request.path, full_path = full_path });
+ end
+ if not data then
+ return 403;
+ end
+ cache[orig_path] = { data = data, content_type = mime_map.html; etag = etag; };
+ response_headers.content_type = mime_map.html;
+
+ else
+ local f, err = open(full_path, "rb");
+ if f then
+ data, err = f:read("*a");
+ f:close();
+ end
+ if not data then
+ module:log("debug", "Could not open or read %s. Error was %s", full_path, err);
+ return 403;
+ end
+ local ext = full_path:match("%.([^./]+)$");
+ local content_type = ext and mime_map[ext];
+ cache[orig_path] = { data = data; content_type = content_type; etag = etag };
+ response_headers.content_type = content_type;
+ end
+
+ return response:send(data);
+ end
+
+ return serve_file;
+end
+
+function wrap_route(routes)
+ for route,handler in pairs(routes) do
+ if type(handler) ~= "function" then
+ routes[route] = serve(handler);
+ end
+ end
+ return routes;
+end
+
+if base_path then
+ module:provides("http", {
+ route = {
+ ["GET /*"] = serve {
+ path = base_path;
+ directory_index = directory_index;
+ }
+ };
+ });
+else
+ module:log("debug", "http_files_dir not set, assuming use by some other module");
+end
+
diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua
new file mode 100644
index 00000000..e7901ab4
--- /dev/null
+++ b/plugins/mod_iq.lua
@@ -0,0 +1,76 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
+local st = require "util.stanza";
+
+local full_sessions = prosody.full_sessions;
+
+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 not (session and session.send(stanza)) then
+ if stanza.attr.type == "get" or stanza.attr.type == "set" then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end
+ end
+ return true;
+ end);
+end
+
+module:hook("iq/bare", function(data)
+ -- IQ to bare JID recieved
+ local stanza = data.stanza;
+ local type = stanza.attr.type;
+
+ -- TODO fire post processing events
+ if type == "get" or type == "set" then
+ local child = stanza.tags[1];
+ local xmlns = child.attr.xmlns or "jabber:client";
+ local ret = module:fire_event("iq/bare/"..xmlns..":"..child.name, data);
+ if ret ~= nil then return ret; end
+ return module:fire_event("iq-"..type.."/bare/"..xmlns..":"..child.name, data);
+ else
+ return module:fire_event("iq-"..type.."/bare/"..stanza.attr.id, data);
+ end
+end);
+
+module:hook("iq/self", function(data)
+ -- IQ to self JID recieved
+ local stanza = data.stanza;
+ local type = stanza.attr.type;
+
+ if type == "get" or type == "set" then
+ local child = stanza.tags[1];
+ local xmlns = child.attr.xmlns or "jabber:client";
+ local ret = module:fire_event("iq/self/"..xmlns..":"..child.name, data);
+ if ret ~= nil then return ret; end
+ return module:fire_event("iq-"..type.."/self/"..xmlns..":"..child.name, data);
+ else
+ 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 stanza = data.stanza;
+ local type = stanza.attr.type;
+
+ if type == "get" or type == "set" then
+ local child = stanza.tags[1];
+ local xmlns = child.attr.xmlns or "jabber:client";
+ local ret = module:fire_event("iq/host/"..xmlns..":"..child.name, data);
+ if ret ~= nil then return ret; end
+ return module:fire_event("iq-"..type.."/host/"..xmlns..":"..child.name, data);
+ else
+ return module:fire_event("iq-"..type.."/host/"..stanza.attr.id, data);
+ end
+end);
diff --git a/plugins/mod_lastactivity.lua b/plugins/mod_lastactivity.lua
new file mode 100644
index 00000000..11053709
--- /dev/null
+++ b/plugins/mod_lastactivity.lua
@@ -0,0 +1,52 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza";
+local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
+local jid_bare = require "util.jid".bare;
+local jid_split = require "util.jid".split;
+
+module:add_feature("jabber:iq:last");
+
+local map = {};
+
+module:hook("pre-presence/bare", function(event)
+ local stanza = event.stanza;
+ if not(stanza.attr.to) and stanza.attr.type == "unavailable" then
+ local t = os.time();
+ local s = stanza:child_with_name("status");
+ s = s and #s.tags == 0 and s[1] or "";
+ map[event.origin.username] = {s = s, t = t};
+ end
+end, 10);
+
+module:hook("iq/bare/jabber:iq:last:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type == "get" then
+ local username = jid_split(stanza.attr.to) or origin.username;
+ if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+ local seconds, text = "0", "";
+ if map[username] then
+ seconds = tostring(os.difftime(os.time(), map[username].t));
+ text = map[username].s;
+ end
+ origin.send(st.reply(stanza):tag('query', {xmlns='jabber:iq:last', seconds=seconds}):text(text));
+ else
+ origin.send(st.error_reply(stanza, 'auth', 'forbidden'));
+ end
+ return true;
+ end
+end);
+
+module.save = function()
+ return {map = map};
+end
+module.restore = function(data)
+ map = data.map or {};
+end
+
diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua
index 8ac10bd4..5fb66441 100644
--- a/plugins/mod_legacyauth.lua
+++ b/plugins/mod_legacyauth.lua
@@ -1,57 +1,86 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
local st = require "util.stanza";
-local send = require "core.sessionmanager".send_to_session;
local t_concat = table.concat;
-add_iq_handler("c2s_unauthed", "jabber:iq:auth",
- function (session, stanza)
- 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);
- send(session, reply:query("jabber:iq:auth")
- :tag("username"):up()
- :tag("password"):up()
- :tag("resource"):up());
- return true;
- else
- username, password, resource = t_concat(username), t_concat(password), t_concat(resource);
- local reply = st.reply(stanza);
- require "core.usermanager"
- if usermanager.validate_credentials(session.host, username, password) then
- -- Authentication successful!
- local success, err = sessionmanager.make_authenticated(session, username);
- if success then
- success, err = sessionmanager.bind_resource(session, resource);
- --FIXME: Reply with error
- if not success then
- local reply = st.reply(stanza);
- reply.attr.type = "error";
- if err == "conflict" then
- reply:tag("error", { code = "409", type = "cancel" })
- :tag("conflict", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" });
- elseif err == "constraint" then
- reply:tag("error", { code = "409", type = "cancel" })
- :tag("already-bound", { xmlns = "x-lxmppd:extensions:legacyauth" });
- elseif err == "auth" then
- reply:tag("error", { code = "401", type = "auth" })
- :tag("not-authorized", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" });
- end
- send(session, reply);
- return true;
- end
- end
- send(session, st.reply(stanza));
+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";
+local nodeprep = require "util.encodings".stringprep.nodeprep;
+local resourceprep = require "util.encodings".stringprep.resourceprep;
+
+module:add_feature("jabber:iq:auth");
+module:hook("stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if secure_auth_only and not origin.secure then
+ -- Sorry, not offering to insecure streams!
+ return;
+ elseif not origin.username then
+ features:tag("auth", {xmlns='http://jabber.org/features/iq-auth'}):up();
+ end
+end);
+
+module:hook("stanza/iq/jabber:iq:auth:query", function(event)
+ local session, stanza = event.origin, event.stanza;
+
+ if session.type ~= "c2s_unauthed" then
+ (session.sends2s or 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)
+ if not (username and resource) then
+ session.send(st.error_reply(stanza, "modify", "bad-request"));
+ return true;
+ end
+ if usermanager.test_password(username, session.host, password) then
+ -- Authentication successful!
+ local success, err = sessionmanager.make_authenticated(session, username);
+ 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;
- else
- local reply = st.reply(stanza);
- reply.attr.type = "error";
- reply:tag("error", { code = "401", type = "auth" })
- :tag("not-authorized", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" });
- dispatch_stanza(reply);
+ 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
-
- end); \ No newline at end of file
+ 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
new file mode 100644
index 00000000..e85da613
--- /dev/null
+++ b/plugins/mod_message.lua
@@ -0,0 +1,85 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
+local full_sessions = prosody.full_sessions;
+local bare_sessions = prosody.bare_sessions;
+
+local st = require "util.stanza";
+local jid_bare = require "util.jid".bare;
+local jid_split = require "util.jid".split;
+local user_exists = require "core.usermanager".user_exists;
+
+local function process_to_bare(bare, origin, stanza)
+ local user = bare_sessions[bare];
+
+ local t = stanza.attr.type;
+ if t == "error" then
+ -- discard
+ elseif t == "groupchat" then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ elseif t == "headline" 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);
+ end
+ end
+ end -- current policy is to discard headlines if no recipient is available
+ else -- chat or normal message
+ if user then -- some resources are connected
+ local recipients = user.top_resources;
+ if recipients then
+ local sent;
+ for i=1,#recipients do
+ sent = recipients[i].send(stanza) or sent;
+ end
+ if sent then
+ return true;
+ end
+ end
+ 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
+
+ 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
+ return true;
+end
+
+module:hook("message/full", function(data)
+ -- message to full JID recieved
+ local origin, stanza = data.origin, data.stanza;
+
+ local session = full_sessions[stanza.attr.to];
+ if session and session.send(stanza) then
+ return true;
+ else -- resource not online
+ return process_to_bare(jid_bare(stanza.attr.to), origin, stanza);
+ end
+end);
+
+module:hook("message/bare", function(data)
+ -- message to bare JID recieved
+ local origin, stanza = data.origin, data.stanza;
+
+ return process_to_bare(stanza.attr.to or (origin.username..'@'..origin.host), origin, stanza);
+end);
+
+module:add_feature("msgoffline");
diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua
new file mode 100644
index 00000000..ed78294b
--- /dev/null
+++ b/plugins/mod_motd.lua
@@ -0,0 +1,30 @@
+-- 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_string("motd_text");
+local motd_jid = module:get_option_string("motd_jid", host);
+
+if not motd_text then return; end
+
+local st = require "util.stanza";
+
+motd_text = motd_text:gsub("^%s*(.-)%s*$", "%1"):gsub("\n%s+", "\n"); -- Strip indentation from the config
+
+module:hook("presence/bare", function (event)
+ local session, stanza = event.origin, event.stanza;
+ if session.username and not session.presence
+ and not stanza.attr.type and not stanza.attr.to then
+ local motd_stanza =
+ st.message({ to = session.full_jid, from = motd_jid })
+ :tag("body"):text(motd_text);
+ module:send(motd_stanza);
+ module:log("debug", "MOTD send to user %s", session.full_jid);
+ end
+end, 1);
diff --git a/plugins/mod_net_multiplex.lua b/plugins/mod_net_multiplex.lua
new file mode 100644
index 00000000..d666b907
--- /dev/null
+++ b/plugins/mod_net_multiplex.lua
@@ -0,0 +1,70 @@
+module:set_global();
+
+local max_buffer_len = module:get_option_number("multiplex_buffer_size", 1024);
+
+local portmanager = require "core.portmanager";
+
+local available_services = {};
+
+local function add_service(service)
+ local multiplex_pattern = service.multiplex and service.multiplex.pattern;
+ if multiplex_pattern then
+ module:log("debug", "Adding multiplex service %q with pattern %q", service.name, multiplex_pattern);
+ available_services[service] = multiplex_pattern;
+ else
+ module:log("debug", "Service %q is not multiplex-capable", service.name);
+ end
+end
+module:hook("service-added", function (event) add_service(event.service); end);
+module:hook("service-removed", function (event) available_services[event.service] = nil; end);
+
+for service_name, services in pairs(portmanager.get_registered_services()) do
+ for i, service in ipairs(services) do
+ add_service(service);
+ end
+end
+
+local buffers = {};
+
+local listener = { default_mode = "*a" };
+
+function listener.onconnect()
+end
+
+function listener.onincoming(conn, data)
+ if not data then return; end
+ local buf = buffers[conn];
+ buffers[conn] = nil;
+ buf = buf and buf..data or data;
+ for service, multiplex_pattern in pairs(available_services) do
+ if buf:match(multiplex_pattern) then
+ module:log("debug", "Routing incoming connection to %s", service.name);
+ local listener = service.listener;
+ conn:setlistener(listener);
+ local onconnect = listener.onconnect;
+ if onconnect then onconnect(conn) end
+ return listener.onincoming(conn, buf);
+ end
+ end
+ if #buf > max_buffer_len then -- Give up
+ conn:close();
+ else
+ buffers[conn] = buf;
+ end
+end
+
+function listener.ondisconnect(conn, err)
+ buffers[conn] = nil; -- warn if no buffer?
+end
+
+module:provides("net", {
+ name = "multiplex";
+ config_prefix = "";
+ listener = listener;
+});
+
+module:provides("net", {
+ name = "multiplex_ssl";
+ config_prefix = "ssl";
+ listener = listener;
+});
diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua
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
new file mode 100644
index 00000000..a65ee903
--- /dev/null
+++ b/plugins/mod_pep.lua
@@ -0,0 +1,280 @@
+-- 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 jid_bare = require "util.jid".bare;
+local jid_split = require "util.jid".split;
+local st = require "util.stanza";
+local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
+local pairs = pairs;
+local next = next;
+local type = type;
+local calculate_hash = require "util.caps".calculate_hash;
+local core_post_stanza = prosody.core_post_stanza;
+
+local NULL = {};
+local data = {};
+local recipients = {};
+local hash_map = {};
+
+module.save = function()
+ return { data = data, recipients = recipients, hash_map = hash_map };
+end
+module.restore = function(state)
+ data = state.data or {};
+ recipients = state.recipients or {};
+ hash_map = state.hash_map or {};
+end
+
+module:add_identity("pubsub", "pep", module:get_option_string("name", "Prosody"));
+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 username, host = jid_split(user_bare);
+ return is_contact_subscribed(username, host, recipient_bare);
+end
+
+local function publish(session, node, id, item)
+ item.attr.xmlns = nil;
+ local disable = #item.tags ~= 1 or #item.tags[1] == 0;
+ if #item.tags == 0 then item.name = "retract"; end
+ local bare = session.username..'@'..session.host;
+ local stanza = st.message({from=bare, type='headline'})
+ :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'})
+ :tag('items', {node=node})
+ :add_child(item)
+ :up()
+ :up();
+
+ -- store for the future
+ local user_data = data[bare];
+ if disable then
+ if user_data then
+ user_data[node] = nil;
+ if not next(user_data) then data[bare] = nil; end
+ end
+ else
+ if not user_data then user_data = {}; data[bare] = user_data; end
+ user_data[node] = {id or "1", item};
+ end
+
+ -- broadcast
+ for recipient, notify in pairs(recipients[bare] or NULL) do
+ if notify[node] then
+ stanza.attr.to = recipient;
+ core_post_stanza(session, stanza);
+ end
+ end
+end
+local function publish_all(user, recipient, session)
+ local d = data[user];
+ local notify = recipients[user] and recipients[user][recipient];
+ if d and notify then
+ for node in pairs(notify) do
+ if d[node] then
+ local id, item = unpack(d[node]);
+ session.send(st.message({from=user, to=recipient, type='headline'})
+ :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'})
+ :tag('items', {node=node})
+ :add_child(item)
+ :up()
+ :up());
+ end
+ end
+ end
+end
+
+local function get_caps_hash_from_presence(stanza, current)
+ local t = stanza.attr.type;
+ if not t then
+ for _, child in pairs(stanza.tags) do
+ if child.name == "c" and child.attr.xmlns == "http://jabber.org/protocol/caps" then
+ local attr = child.attr;
+ if attr.hash then -- new caps
+ if attr.hash == 'sha-1' and attr.node and attr.ver then return attr.ver, attr.node.."#"..attr.ver; end
+ else -- legacy caps
+ if attr.node and attr.ver then return attr.node.."#"..attr.ver.."#"..(attr.ext or ""), attr.node.."#"..attr.ver; end
+ end
+ return; -- bad caps format
+ end
+ end
+ elseif t == "unavailable" or t == "error" then
+ return;
+ end
+ return current; -- no caps, could mean caps optimization, so return current
+end
+
+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 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] = 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
+ -- COMPAT from ~= stanza.attr.to because OneTeam can't deal with missing from attribute
+ origin.send(
+ st.stanza("iq", {from=user, 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
+end, 10);
+
+module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event)
+ local session, stanza = event.origin, event.stanza;
+ local payload = stanza.tags[1];
+
+ if stanza.attr.type == 'set' and (not stanza.attr.to or jid_bare(stanza.attr.from) == stanza.attr.to) then
+ payload = payload.tags[1];
+ if payload and (payload.name == 'publish' or payload.name == 'retract') and payload.attr.node then -- <publish node='http://jabber.org/protocol/tune'>
+ local node = payload.attr.node;
+ payload = payload.tags[1];
+ if payload and payload.name == "item" then -- <item>
+ local id = payload.attr.id;
+ session.send(st.reply(stanza));
+ publish(session, node, id, st.clone(payload));
+ return true;
+ end
+ end
+ elseif stanza.attr.type == 'get' then
+ local user = stanza.attr.to and jid_bare(stanza.attr.to) or session.username..'@'..session.host;
+ if subscription_presence(user, stanza.attr.from) then
+ local user_data = data[user];
+ local node, requested_id;
+ payload = payload.tags[1];
+ if payload and payload.name == 'items' then
+ node = payload.attr.node;
+ local item = payload.tags[1];
+ if item and item.name == "item" then
+ requested_id = item.attr.id;
+ end
+ end
+ if node and user_data and user_data[node] then -- Send the last item
+ local id, item = unpack(user_data[node]);
+ if not requested_id or id == requested_id then
+ local stanza = st.reply(stanza)
+ :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'})
+ :tag('items', {node=node})
+ :add_child(item)
+ :up()
+ :up();
+ session.send(stanza);
+ return true;
+ else -- requested item doesn't exist
+ local stanza = st.reply(stanza)
+ :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'})
+ :tag('items', {node=node})
+ :up();
+ session.send(stanza);
+ return true;
+ end
+ elseif node then -- node doesn't exist
+ session.send(st.error_reply(stanza, 'cancel', 'item-not-found'));
+ return true;
+ else --invalid request
+ session.send(st.error_reply(stanza, 'modify', 'bad-request'));
+ return true;
+ end
+ else --no presence subscription
+ session.send(st.error_reply(stanza, 'auth', 'not-authorized')
+ :tag('presence-subscription-required', {xmlns='http://jabber.org/protocol/pubsub#errors'}));
+ return true;
+ end
+ end
+end);
+
+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];
+ if type(current) ~= "string" then return; end -- check if waiting for recipient's response
+ local ver = current;
+ if not string.find(current, "#") then
+ ver = calculate_hash(disco.tags); -- calculate hash
+ end
+ local notify = {};
+ for _, feature in pairs(disco.tags) do
+ if feature.name == "feature" and feature.attr.var then
+ local nfeature = feature.attr.var:match("^(.*)%+notify$");
+ if nfeature then notify[nfeature] = true; end
+ 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);
+ end
+ end
+end);
+
+module:hook("account-disco-info", function(event)
+ local stanza = event.stanza;
+ stanza:tag('identity', {category='pubsub', type='pep'}):up();
+ stanza:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up();
+end);
+
+module:hook("account-disco-items", function(event)
+ local stanza = event.stanza;
+ local bare = stanza.attr.to;
+ local user_data = data[bare];
+
+ if user_data then
+ for node, _ in pairs(user_data) do
+ stanza:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes
+ end
+ end
+end);
diff --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua
new file mode 100644
index 00000000..0bfcac66
--- /dev/null
+++ b/plugins/mod_ping.lua
@@ -0,0 +1,35 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza";
+
+module:add_feature("urn:xmpp:ping");
+
+local function ping_handler(event)
+ if event.stanza.attr.type == "get" then
+ event.origin.send(st.reply(event.stanza));
+ return true;
+ end
+end
+
+module:hook("iq/bare/urn:xmpp:ping:ping", ping_handler);
+module:hook("iq/host/urn:xmpp:ping:ping", ping_handler);
+
+-- 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
new file mode 100644
index 00000000..28fd7f38
--- /dev/null
+++ b/plugins/mod_posix.lua
@@ -0,0 +1,193 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
+local want_pposix_version = "0.3.6";
+
+local pposix = assert(require "util.pposix");
+if pposix._VERSION ~= want_pposix_version then
+ module:log("warn", "Unknown version (%s) of binary pposix module, expected %s. Perhaps you need to recompile?", tostring(pposix._VERSION), want_pposix_version);
+end
+
+local signal = select(2, pcall(require, "util.signal"));
+if type(signal) == "string" then
+ module:log("warn", "Couldn't load signal library, won't respond to SIGTERM");
+end
+
+local lfs = require "lfs";
+local stat = lfs.attributes;
+
+local prosody = _G.prosody;
+
+module:set_global(); -- we're a global module
+
+local umask = module:get_option("umask") or "027";
+pposix.umask(umask);
+
+-- Allow switching away from root, some people like strange ports.
+module:hook("server-started", function ()
+ local uid = module:get_option("setuid");
+ local gid = module:get_option("setgid");
+ if gid then
+ local success, msg = pposix.setgid(gid);
+ if success then
+ module:log("debug", "Changed group to %s successfully.", gid);
+ else
+ module:log("error", "Failed to change group to %s. Error: %s", gid, msg);
+ prosody.shutdown("Failed to change group to %s", gid);
+ end
+ end
+ if uid then
+ local success, msg = pposix.setuid(uid);
+ if success then
+ module:log("debug", "Changed user to %s successfully.", uid);
+ else
+ module:log("error", "Failed to change user to %s. Error: %s", uid, msg);
+ prosody.shutdown("Failed to change user to %s", uid);
+ end
+ end
+ end);
+
+-- Don't even think about it!
+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
+
+local pidfile;
+local pidfile_handle;
+
+local function remove_pidfile()
+ if pidfile_handle then
+ pidfile_handle:close();
+ os.remove(pidfile);
+ pidfile, pidfile_handle = nil, nil;
+ end
+end
+
+local function write_pidfile()
+ if pidfile_handle then
+ remove_pidfile();
+ end
+ pidfile = module:get_option("pidfile");
+ if pidfile then
+ local err;
+ local mode = stat(pidfile) and "r+" or "w+";
+ pidfile_handle, err = io.open(pidfile, mode);
+ 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 not lfs.lock(pidfile_handle, "w") then -- Exclusive lock
+ local other_pid = pidfile_handle:read("*a");
+ module:log("error", "Another Prosody instance seems to be running with PID %s, quitting", other_pid);
+ pidfile_handle = nil;
+ prosody.shutdown("Prosody already running");
+ else
+ 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;
+function syslog_sink_maker(config)
+ if not syslog_opened then
+ pposix.syslog_open("prosody", module:get_option_string("syslog_facility"));
+ syslog_opened = true;
+ end
+ local syslog, format = pposix.syslog_log, string.format;
+ return function (name, level, message, ...)
+ if ... then
+ syslog(level, name, format(message, ...));
+ else
+ syslog(level, name, message);
+ end
+ end;
+end
+require "core.loggingmanager".register_sink_type("syslog", syslog_sink_maker);
+
+local daemonize = module:get_option("daemonize");
+if daemonize == nil then
+ local no_daemonize = module:get_option("no_daemonize"); --COMPAT w/ 0.5
+ daemonize = not no_daemonize;
+ if no_daemonize ~= nil then
+ module:log("warn", "The 'no_daemonize' option is now replaced by 'daemonize'");
+ module:log("warn", "Update your config from 'no_daemonize = %s' to 'daemonize = %s'", tostring(no_daemonize), tostring(daemonize));
+ end
+end
+
+local function remove_log_sinks()
+ local lm = require "core.loggingmanager";
+ lm.register_sink_type("console", nil);
+ lm.register_sink_type("stdout", nil);
+ lm.reload_logging();
+end
+
+if daemonize then
+ local function daemonize_server()
+ module:log("info", "Prosody is about to detach from the console, disabling further console output");
+ remove_log_sinks();
+ local ok, ret = pposix.daemonize();
+ if not ok then
+ module:log("error", "Failed to daemonize: %s", ret);
+ elseif ret and ret > 0 then
+ os.exit(0);
+ else
+ module:log("info", "Successfully daemonized to PID %d", pposix.getpid());
+ write_pidfile();
+ end
+ end
+ 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:hook("server-stopped", remove_pidfile);
+
+-- Set signal handlers
+if signal.signal then
+ signal.signal("SIGTERM", function ()
+ module:log("warn", "Received SIGTERM");
+ prosody.unlock_globals();
+ prosody.shutdown("Received SIGTERM");
+ prosody.lock_globals();
+ end);
+
+ signal.signal("SIGHUP", function ()
+ module:log("info", "Received SIGHUP");
+ prosody.reload_config();
+ prosody.reopen_logfiles();
+ end);
+
+ signal.signal("SIGINT", function ()
+ module:log("info", "Received SIGINT");
+ prosody.unlock_globals();
+ prosody.shutdown("Received SIGINT");
+ prosody.lock_globals();
+ end);
+end
diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua
new file mode 100644
index 00000000..8dac2d35
--- /dev/null
+++ b/plugins/mod_presence.lua
@@ -0,0 +1,382 @@
+-- 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 = module._log;
+
+local require = require;
+local pairs = pairs;
+local t_concat, t_insert = table.concat, table.insert;
+local s_find = string.find;
+local tonumber = tonumber;
+
+local core_post_stanza = prosody.core_post_stanza;
+local st = require "util.stanza";
+local jid_split = require "util.jid".split;
+local jid_bare = require "util.jid".bare;
+local datetime = require "util.datetime";
+local hosts = prosody.hosts;
+local bare_sessions = prosody.bare_sessions;
+local full_sessions = prosody.full_sessions;
+local NULL = {};
+
+local rostermanager = require "core.rostermanager";
+local sessionmanager = require "core.sessionmanager";
+
+local function select_top_resources(user)
+ local priority = 0;
+ local recipients = {};
+ for _, session in pairs(user.sessions) do -- find resource with greatest priority
+ if session.presence then
+ -- TODO check active privacy list for session
+ local p = session.priority;
+ if p > priority then
+ priority = p;
+ recipients = {session};
+ elseif p == priority then
+ t_insert(recipients, session);
+ end
+ end
+ end
+ return recipients;
+end
+local function recalc_resource_map(user)
+ if user then
+ user.top_resources = select_top_resources(user);
+ if #user.top_resources == 0 then user.top_resources = nil; end
+ end
+end
+
+local ignore_presence_priority = module:get_option("ignore_presence_priority");
+
+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
+ for i=#priority.tags,1,-1 do priority.tags[i] = nil; end
+ for i=#priority,1,-1 do priority[i] = nil; end
+ 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
+ local roster = origin.roster;
+ local node, host = origin.username, origin.host;
+ local user = bare_sessions[node.."@"..host];
+ 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_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_post_stanza(origin, stanza, true);
+ end
+ end
+ if stanza.attr.type == nil and not origin.presence then -- initial presence
+ origin.presence = stanza; -- FIXME repeated later
+ local probe = st.presence({from = origin.full_jid, type = "probe"});
+ 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_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_post_stanza(res, res.presence, true);
+ res.presence.attr.to = nil;
+ end
+ end
+ if roster.pending then -- resend incoming subscription requests
+ for jid in pairs(roster.pending) do
+ origin.send(st.presence({type="subscribe", from=jid})); -- TODO add to attribute? Use original?
+ end
+ end
+ local request = st.presence({type="subscribe", from=origin.username.."@"..origin.host});
+ for jid, item in pairs(roster) do -- resend outgoing subscription requests
+ if item.ask then
+ request.attr.to = jid;
+ core_post_stanza(origin, request, true);
+ end
+ end
+
+ if priority >= 0 then
+ local event = { origin = origin }
+ module:fire_event('message/offline/broadcast', event);
+ end
+ end
+ if stanza.attr.type == "unavailable" then
+ origin.presence = nil;
+ if origin.priority then
+ origin.priority = nil;
+ recalc_resource_map(user);
+ end
+ if origin.directed then
+ for jid in pairs(origin.directed) do
+ stanza.attr.to = jid;
+ core_post_stanza(origin, stanza, true);
+ end
+ origin.directed = nil;
+ end
+ else
+ origin.presence = stanza;
+ stanza:tag("delay", { xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime() }):up();
+ if origin.priority ~= priority then
+ origin.priority = priority;
+ recalc_resource_map(user);
+ end
+ end
+ stanza.attr.to = nil; -- reset it
+end
+
+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
+ local u = h.sessions[user];
+ if u then
+ for k, session in pairs(u.sessions) do
+ local pres = session.presence;
+ if pres then
+ if stanza then pres = stanza; pres.attr.from = session.full_jid; end
+ pres.attr.to = jid;
+ core_post_stanza(session, pres, true);
+ pres.attr.to = nil;
+ count = count + 1;
+ end
+ end
+ end
+ end
+ log("debug", "broadcasted presence of %d resources from %s@%s to %s", count, user, host, jid);
+ return count;
+end
+
+function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
+ local node, host = jid_split(from_bare);
+ 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 %s from %s for %s", stanza.attr.type, from_bare, to_bare);
+ if stanza.attr.type == "probe" then
+ stanza.attr.from, stanza.attr.to = st_from, st_to;
+ return;
+ 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_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_post_stanza(origin, stanza);
+ elseif stanza.attr.type == "subscribed" then
+ -- 1. route stanza
+ -- 2. roster_push ()
+ -- 3. send_presence_of_available_resources
+ if rostermanager.subscribed(node, host, to_bare) then
+ rostermanager.roster_push(node, host, to_bare);
+ end
+ core_post_stanza(origin, stanza);
+ send_presence_of_available_resources(node, host, to_bare, origin);
+ elseif stanza.attr.type == "unsubscribed" then
+ -- 1. send unavailable
+ -- 2. route stanza
+ -- 3. roster push (subscription = from or both)
+ local success, pending_in, subscribed = rostermanager.unsubscribed(node, host, to_bare);
+ if success then
+ if subscribed then
+ rostermanager.roster_push(node, host, to_bare);
+ end
+ core_post_stanza(origin, stanza);
+ if subscribed then
+ send_presence_of_available_resources(node, host, to_bare, origin, st.presence({ type = "unavailable" }));
+ end
+ end
+ 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)
+ local node, host = jid_split(to_bare);
+ local st_from, st_to = stanza.attr.from, stanza.attr.to;
+ stanza.attr.from, stanza.attr.to = from_bare, to_bare;
+ log("debug", "inbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare);
+
+ if stanza.attr.type == "probe" then
+ local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
+ if result then
+ 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_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_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) then
+ core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- TODO send last activity
+ end
+ else
+ 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);
+ end -- TODO else return error, unable to save
+ end
+ end
+ elseif stanza.attr.type == "unsubscribe" then
+ if rostermanager.process_inbound_unsubscribe(node, host, from_bare) then
+ sessionmanager.send_to_interested_resources(node, host, stanza);
+ rostermanager.roster_push(node, host, from_bare);
+ end
+ elseif stanza.attr.type == "subscribed" then
+ if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then
+ sessionmanager.send_to_interested_resources(node, host, stanza);
+ rostermanager.roster_push(node, host, from_bare);
+ end
+ elseif stanza.attr.type == "unsubscribed" then
+ if rostermanager.process_inbound_subscription_cancellation(node, host, from_bare) then
+ sessionmanager.send_to_interested_resources(node, host, stanza);
+ rostermanager.roster_push(node, host, from_bare);
+ end
+ 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)
+ -- outbound presence recieved
+ local origin, stanza = data.origin, data.stanza;
+
+ local to = stanza.attr.to;
+ if to then
+ local t = stanza.attr.type;
+ if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes
+ 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);
+ 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?
+ else
+ origin.directed[to] = true; -- FIXME does it make more sense to add to_bare rather than to?
+ end
+ end
+ end -- TODO maybe handle normal presence here, instead of letting it pass to incoming handlers?
+end
+
+module:hook("pre-presence/full", outbound_presence_handler);
+module:hook("pre-presence/bare", outbound_presence_handler);
+module:hook("pre-presence/host", outbound_presence_handler);
+
+module:hook("presence/bare", function(data)
+ -- inbound presence to bare JID recieved
+ local origin, stanza = data.origin, data.stanza;
+
+ local to = stanza.attr.to;
+ 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
+ return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
+ end
+
+ local user = bare_sessions[to];
+ if user then
+ for _, session in pairs(user.sessions) do
+ if session.presence then -- only send to available resources
+ session.send(stanza);
+ end
+ end
+ end -- no resources not online, discard
+ elseif not t or t == "unavailable" then
+ handle_normal_presence(origin, stanza);
+ else
+ origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid presence type"));
+ end
+ return true;
+end);
+module:hook("presence/full", function(data)
+ -- inbound presence to full JID recieved
+ local origin, stanza = data.origin, data.stanza;
+
+ local t = stanza.attr.type;
+ if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID
+ 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];
+ if session then
+ -- TODO fire post processing event
+ session.send(stanza);
+ end -- resource not online, discard
+ return true;
+end);
+module:hook("presence/host", function(data)
+ -- inbound presence to the host
+ local stanza = data.stanza;
+
+ local from_bare = jid_bare(stanza.attr.from);
+ local t = stanza.attr.type;
+ if t == "probe" then
+ core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
+ elseif t == "subscribe" then
+ 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);
+
+module:hook("resource-unbind", function(event)
+ local session, err = event.session, event.error;
+ -- Send unavailable presence
+ if session.presence then
+ local pres = st.presence{ type = "unavailable" };
+ if err then
+ pres:tag("status"):text("Disconnected: "..err):up();
+ end
+ session:dispatch_stanza(pres);
+ elseif session.directed then
+ local pres = st.presence{ type = "unavailable", from = session.full_jid };
+ if err then
+ pres:tag("status"):text("Disconnected: "..err):up();
+ end
+ for jid in pairs(session.directed) do
+ pres.attr.to = jid;
+ core_post_stanza(session, pres, true);
+ end
+ session.directed = nil;
+ end
+end);
diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua
new file mode 100644
index 00000000..31ace9f9
--- /dev/null
+++ b/plugins/mod_privacy.lua
@@ -0,0 +1,448 @@
+-- Prosody IM
+-- Copyright (C) 2009-2010 Matthew Wild
+-- Copyright (C) 2009-2010 Waqas Hussain
+-- Copyright (C) 2009 Thilo Cestonaro
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+module:add_feature("jabber:iq:privacy");
+
+local st = require "util.stanza";
+local bare_sessions, full_sessions = prosody.bare_sessions, prosody.full_sessions;
+local util_Jid = require "util.jid";
+local jid_bare = util_Jid.bare;
+local jid_split, jid_join = util_Jid.split, util_Jid.join;
+local load_roster = require "core.rostermanager".load_roster;
+local to_number = tonumber;
+
+local privacy_storage = module:open_store();
+
+function isListUsed(origin, name, privacy_lists)
+ local user = bare_sessions[origin.username.."@"..origin.host];
+ if user then
+ for resource, session in pairs(user.sessions) do
+ if resource ~= origin.resource then
+ if session.activePrivacyList == name then
+ return true;
+ elseif session.activePrivacyList == nil and privacy_lists.default == name then
+ return true;
+ end
+ end
+ end
+ end
+end
+
+function isAnotherSessionUsingDefaultList(origin)
+ local user = bare_sessions[origin.username.."@"..origin.host];
+ if user then
+ for resource, session in pairs(user.sessions) do
+ if resource ~= origin.resource and session.activePrivacyList == nil then
+ return true;
+ end
+ end
+ end
+end
+
+function declineList(privacy_lists, origin, stanza, which)
+ if which == "default" then
+ if isAnotherSessionUsingDefaultList(origin) then
+ return { "cancel", "conflict", "Another session is online and using the default list."};
+ end
+ privacy_lists.default = nil;
+ origin.send(st.reply(stanza));
+ elseif which == "active" then
+ origin.activePrivacyList = nil;
+ origin.send(st.reply(stanza));
+ else
+ return {"modify", "bad-request", "Neither default nor active list specifed to decline."};
+ end
+ return true;
+end
+
+function activateList(privacy_lists, origin, stanza, which, name)
+ local list = privacy_lists.lists[name];
+
+ if which == "default" and list then
+ if isAnotherSessionUsingDefaultList(origin) then
+ return {"cancel", "conflict", "Another session is online and using the default list."};
+ end
+ privacy_lists.default = name;
+ origin.send(st.reply(stanza));
+ elseif which == "active" and list then
+ origin.activePrivacyList = name;
+ origin.send(st.reply(stanza));
+ elseif not list then
+ return {"cancel", "item-not-found", "No such list: "..name};
+ else
+ return {"modify", "bad-request", "No list chosen to be active or default."};
+ end
+ return true;
+end
+
+function deleteList(privacy_lists, origin, stanza, name)
+ local list = privacy_lists.lists[name];
+
+ if list then
+ if isListUsed(origin, name, privacy_lists) then
+ return {"cancel", "conflict", "Another session is online and using the list which should be deleted."};
+ end
+ if privacy_lists.default == name then
+ privacy_lists.default = nil;
+ end
+ if origin.activePrivacyList == name then
+ origin.activePrivacyList = nil;
+ end
+ privacy_lists.lists[name] = nil;
+ origin.send(st.reply(stanza));
+ return true;
+ end
+ return {"modify", "bad-request", "Not existing list specifed to be deleted."};
+end
+
+function createOrReplaceList (privacy_lists, origin, stanza, name, entries)
+ local bare_jid = origin.username.."@"..origin.host;
+
+ if privacy_lists.lists == nil then
+ privacy_lists.lists = {};
+ end
+
+ local list = {};
+ privacy_lists.lists[name] = list;
+
+ local orderCheck = {};
+ list.name = name;
+ list.items = {};
+
+ for _,item in ipairs(entries) do
+ if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then
+ return {"modify", "bad-request", "Order attribute not valid."};
+ end
+
+ if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then
+ return {"modify", "bad-request", "Type attribute not valid."};
+ end
+
+ local tmp = {};
+ orderCheck[item.attr.order] = true;
+
+ tmp["type"] = item.attr.type;
+ tmp["value"] = item.attr.value;
+ tmp["action"] = item.attr.action;
+ tmp["order"] = to_number(item.attr.order);
+ tmp["presence-in"] = false;
+ tmp["presence-out"] = false;
+ tmp["message"] = false;
+ tmp["iq"] = false;
+
+ if #item.tags > 0 then
+ for _,tag in ipairs(item.tags) do
+ tmp[tag.name] = true;
+ end
+ end
+
+ if tmp.type == "subscription" then
+ if tmp.value ~= "both" and
+ tmp.value ~= "to" and
+ tmp.value ~= "from" and
+ tmp.value ~= "none" then
+ return {"cancel", "bad-request", "Subscription value must be both, to, from or none."};
+ end
+ end
+
+ if tmp.action ~= "deny" and tmp.action ~= "allow" then
+ return {"cancel", "bad-request", "Action must be either deny or allow."};
+ end
+ list.items[#list.items + 1] = tmp;
+ end
+
+ table.sort(list, function(a, b) return a.order < b.order; end);
+
+ origin.send(st.reply(stanza));
+ if bare_sessions[bare_jid] ~= nil then
+ local iq = st.iq ( { type = "set", id="push1" } );
+ iq:tag ("query", { xmlns = "jabber:iq:privacy" } );
+ iq:tag ("list", { name = list.name } ):up();
+ iq:up();
+ for resource, session in pairs(bare_sessions[bare_jid].sessions) do
+ iq.attr.to = bare_jid.."/"..resource
+ session.send(iq);
+ end
+ else
+ return {"cancel", "bad-request", "internal error."};
+ end
+ return true;
+end
+
+function getList(privacy_lists, origin, stanza, name)
+ local reply = st.reply(stanza);
+ reply:tag("query", {xmlns="jabber:iq:privacy"});
+
+ if name == nil then
+ if privacy_lists.lists then
+ if origin.activePrivacyList then
+ reply:tag("active", {name=origin.activePrivacyList}):up();
+ end
+ if privacy_lists.default then
+ reply:tag("default", {name=privacy_lists.default}):up();
+ end
+ for name,list in pairs(privacy_lists.lists) do
+ reply:tag("list", {name=name}):up();
+ end
+ end
+ else
+ local list = privacy_lists.lists[name];
+ if list then
+ reply = reply:tag("list", {name=list.name});
+ for _,item in ipairs(list.items) do
+ reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order});
+ if item["message"] then reply:tag("message"):up(); end
+ if item["iq"] then reply:tag("iq"):up(); end
+ if item["presence-in"] then reply:tag("presence-in"):up(); end
+ if item["presence-out"] then reply:tag("presence-out"):up(); end
+ reply:up();
+ end
+ else
+ return {"cancel", "item-not-found", "Unknown list specified."};
+ end
+ end
+
+ origin.send(reply);
+ return true;
+end
+
+module:hook("iq/bare/jabber:iq:privacy:query", function(data)
+ local origin, stanza = data.origin, data.stanza;
+
+ if stanza.attr.to == nil then -- only service requests to own bare JID
+ local query = stanza.tags[1]; -- the query element
+ local valid = false;
+ local privacy_lists = privacy_storage:get(origin.username) or { lists = {} };
+
+ if privacy_lists.lists[1] then -- Code to migrate from old privacy lists format, remove in 0.8
+ module:log("info", "Upgrading format of stored privacy lists for %s@%s", origin.username, origin.host);
+ local lists = privacy_lists.lists;
+ for idx, list in ipairs(lists) do
+ lists[list.name] = list;
+ lists[idx] = nil;
+ end
+ end
+
+ if stanza.attr.type == "set" then
+ if #query.tags == 1 then -- the <query/> element MUST NOT include more than one child element
+ for _,tag in ipairs(query.tags) do
+ if tag.name == "active" or tag.name == "default" then
+ if tag.attr.name == nil then -- Client declines the use of active / default list
+ valid = declineList(privacy_lists, origin, stanza, tag.name);
+ else -- Client requests change of active / default list
+ valid = activateList(privacy_lists, origin, stanza, tag.name, tag.attr.name);
+ end
+ elseif tag.name == "list" and tag.attr.name then -- Client adds / edits a privacy list
+ if #tag.tags == 0 then -- Client removes a privacy list
+ valid = deleteList(privacy_lists, origin, stanza, tag.attr.name);
+ else -- Client edits a privacy list
+ valid = createOrReplaceList(privacy_lists, origin, stanza, tag.attr.name, tag.tags);
+ end
+ end
+ end
+ end
+ elseif stanza.attr.type == "get" then
+ local name = nil;
+ local listsToRetrieve = 0;
+ if #query.tags >= 1 then
+ for _,tag in ipairs(query.tags) do
+ if tag.name == "list" then -- Client requests a privacy list from server
+ name = tag.attr.name;
+ listsToRetrieve = listsToRetrieve + 1;
+ end
+ end
+ end
+ if listsToRetrieve == 0 or listsToRetrieve == 1 then
+ valid = getList(privacy_lists, origin, stanza, name);
+ end
+ end
+
+ if valid ~= true then
+ valid = valid or { "cancel", "bad-request", "Couldn't understand request" };
+ if valid[1] == nil then
+ valid[1] = "cancel";
+ end
+ if valid[2] == nil then
+ valid[2] = "bad-request";
+ end
+ origin.send(st.error_reply(stanza, valid[1], valid[2], valid[3]));
+ else
+ privacy_storage:set(origin.username, privacy_lists);
+ end
+ return true;
+ end
+end);
+
+function checkIfNeedToBeBlocked(e, session)
+ local origin, stanza = e.origin, e.stanza;
+ local privacy_lists = privacy_storage:get(session.username) or {};
+ local bare_jid = session.username.."@"..session.host;
+ local to = stanza.attr.to or bare_jid;
+ local from = stanza.attr.from;
+
+ local is_to_user = bare_jid == jid_bare(to);
+ local is_from_user = bare_jid == jid_bare(from);
+
+ --module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
+
+ if privacy_lists.lists == nil or
+ not (session.activePrivacyList or privacy_lists.default)
+ then
+ return; -- Nothing to block, default is Allow all
+ end
+ if is_from_user and is_to_user then
+ --module:log("debug", "Not blocking communications between user's resources");
+ return; -- from one of a user's resource to another => HANDS OFF!
+ end
+
+ local listname = session.activePrivacyList;
+ if listname == nil then
+ listname = privacy_lists.default; -- no active list selected, use default list
+ end
+ local list = privacy_lists.lists[listname];
+ if not list then -- should never happen
+ module:log("warn", "given privacy list not found. name: %s for user %s", listname, bare_jid);
+ return;
+ end
+ for _,item in ipairs(list.items) do
+ local apply = false;
+ local block = false;
+ if (
+ (stanza.name == "message" and item.message) or
+ (stanza.name == "iq" and item.iq) or
+ (stanza.name == "presence" and is_to_user and item["presence-in"]) or
+ (stanza.name == "presence" and is_from_user and item["presence-out"]) or
+ (item.message == false and item.iq == false and item["presence-in"] == false and item["presence-out"] == false)
+ ) then
+ apply = true;
+ end
+ if apply then
+ local evilJid = {};
+ apply = false;
+ if is_to_user then
+ --module:log("debug", "evil jid is (from): %s", from);
+ evilJid.node, evilJid.host, evilJid.resource = jid_split(from);
+ else
+ --module:log("debug", "evil jid is (to): %s", to);
+ evilJid.node, evilJid.host, evilJid.resource = jid_split(to);
+ end
+ if item.type == "jid" and
+ (evilJid.node and evilJid.host and evilJid.resource and item.value == evilJid.node.."@"..evilJid.host.."/"..evilJid.resource) or
+ (evilJid.node and evilJid.host and item.value == evilJid.node.."@"..evilJid.host) or
+ (evilJid.host and evilJid.resource and item.value == evilJid.host.."/"..evilJid.resource) or
+ (evilJid.host and item.value == evilJid.host) then
+ apply = true;
+ block = (item.action == "deny");
+ elseif item.type == "group" then
+ local roster = load_roster(session.username, session.host);
+ local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
+ if roster_entry then
+ local groups = roster_entry.groups;
+ for group in pairs(groups) do
+ if group == item.value then
+ apply = true;
+ block = (item.action == "deny");
+ break;
+ end
+ end
+ end
+ elseif item.type == "subscription" then -- we need a valid bare evil jid
+ local roster = load_roster(session.username, session.host);
+ local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
+ if (not(roster_entry) and item.value == "none")
+ or (roster_entry and roster_entry.subscription == item.value) then
+ apply = true;
+ block = (item.action == "deny");
+ end
+ elseif item.type == nil then
+ apply = true;
+ block = (item.action == "deny");
+ end
+ end
+ if apply then
+ if block then
+ -- drop and not bounce groupchat messages, otherwise users will get kicked
+ if stanza.attr.type == "groupchat" then
+ return true;
+ end
+ module:log("debug", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
+ if stanza.name == "message" then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ elseif stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end
+ return true; -- stanza blocked !
+ else
+ --module:log("debug", "stanza explicitly allowed!")
+ return;
+ end
+ end
+ end
+end
+
+function preCheckIncoming(e)
+ local session;
+ if e.stanza.attr.to ~= nil then
+ local node, host, resource = jid_split(e.stanza.attr.to);
+ if node == nil or host == nil then
+ return;
+ end
+ if resource == nil then
+ local prio = 0;
+ 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
+ session = session_;
+ prio = session_.priority;
+ end
+ end
+ end
+ else
+ session = full_sessions[node.."@"..host.."/"..resource];
+ end
+ if session ~= nil then
+ return checkIfNeedToBeBlocked(e, session);
+ else
+ --module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource));
+ end
+ end
+end
+
+function preCheckOutgoing(e)
+ local session = e.origin;
+ if e.stanza.attr.from == nil then
+ e.stanza.attr.from = session.username .. "@" .. session.host;
+ if session.resource ~= nil then
+ e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource;
+ end
+ end
+ if session.username then -- FIXME do properly
+ return checkIfNeedToBeBlocked(e, session);
+ end
+end
+
+module:hook("pre-message/full", preCheckOutgoing, 500);
+module:hook("pre-message/bare", preCheckOutgoing, 500);
+module:hook("pre-message/host", preCheckOutgoing, 500);
+module:hook("pre-iq/full", preCheckOutgoing, 500);
+module:hook("pre-iq/bare", preCheckOutgoing, 500);
+module:hook("pre-iq/host", preCheckOutgoing, 500);
+module:hook("pre-presence/full", preCheckOutgoing, 500);
+module:hook("pre-presence/bare", preCheckOutgoing, 500);
+module:hook("pre-presence/host", preCheckOutgoing, 500);
+
+module:hook("message/full", preCheckIncoming, 500);
+module:hook("message/bare", preCheckIncoming, 500);
+module:hook("message/host", preCheckIncoming, 500);
+module:hook("iq/full", preCheckIncoming, 500);
+module:hook("iq/bare", preCheckIncoming, 500);
+module:hook("iq/host", preCheckIncoming, 500);
+module:hook("presence/full", preCheckIncoming, 500);
+module:hook("presence/bare", preCheckIncoming, 500);
+module:hook("presence/host", preCheckIncoming, 500);
diff --git a/plugins/mod_private.lua b/plugins/mod_private.lua
new file mode 100644
index 00000000..365a997c
--- /dev/null
+++ b/plugins/mod_private.lua
@@ -0,0 +1,52 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
+local st = require "util.stanza"
+
+local private_storage = module:open_store();
+
+module:add_feature("jabber:iq:private");
+
+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 = private_storage:get(origin.username);
+ 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 private_storage:set(origin.username, data) then
+ origin.send(st.reply(stanza));
+ else
+ origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
+ 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
new file mode 100644
index 00000000..1fa42bd8
--- /dev/null
+++ b/plugins/mod_proxy65.lua
@@ -0,0 +1,192 @@
+-- Prosody IM
+-- Copyright (C) 2008-2011 Matthew Wild
+-- Copyright (C) 2008-2011 Waqas Hussain
+-- Copyright (C) 2009 Thilo Cestonaro
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+module:set_global();
+
+local jid_compare, jid_prep = require "util.jid".compare, require "util.jid".prep;
+local st = require "util.stanza";
+local sha1 = require "util.hashes".sha1;
+local b64 = require "util.encodings".base64.encode;
+local server = require "net.server";
+local portmanager = require "core.portmanager";
+
+local sessions, transfers = module:shared("sessions", "transfers");
+local max_buffer_size = 4096;
+
+local listener = {};
+
+function listener.onincoming(conn, data)
+ local session = sessions[conn] or {};
+
+ local transfer = transfers[session.sha];
+ if transfer and transfer.activated then -- copy data between initiator and target
+ local initiator, target = transfer.initiator, transfer.target;
+ (conn == initiator and target or initiator):write(data);
+ return;
+ end -- FIXME server.link should be doing this?
+
+ if not session.greeting_done then
+ local nmethods = data:byte(2) or 0;
+ if data:byte(1) == 0x05 and nmethods > 0 and #data == 2 + nmethods then -- check if we have all the data
+ if data:find("%z") then -- 0x00 = 'No authentication' is supported
+ session.greeting_done = true;
+ sessions[conn] = session;
+ conn:write("\5\0"); -- send (SOCKS version 5, No authentication)
+ module:log("debug", "SOCKS5 greeting complete");
+ return;
+ end
+ end -- else error, unexpected input
+ conn:write("\5\255"); -- send (SOCKS version 5, no acceptable method)
+ conn:close();
+ module:log("debug", "Invalid SOCKS5 greeting recieved: '%s'", b64(data));
+ else -- connection request
+ --local head = string.char( 0x05, 0x01, 0x00, 0x03, 40 ); -- ( VER=5=SOCKS5, CMD=1=CONNECT, RSV=0=RESERVED, ATYP=3=DOMAIMNAME, SHA-1 size )
+ if #data == 47 and data:sub(1,5) == "\5\1\0\3\40" and data:sub(-2) == "\0\0" then
+ local sha = data:sub(6, 45);
+ conn:pause();
+ conn:write("\5\0\0\3\40" .. sha .. "\0\0"); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte)
+ if not transfers[sha] then
+ transfers[sha] = {};
+ transfers[sha].target = conn;
+ session.sha = sha;
+ module:log("debug", "SOCKS5 target connected for session %s", sha);
+ else -- transfers[sha].target ~= nil
+ transfers[sha].initiator = conn;
+ session.sha = sha;
+ module:log("debug", "SOCKS5 initiator connected for session %s", sha);
+ server.link(conn, transfers[sha].target, max_buffer_size);
+ server.link(transfers[sha].target, conn, max_buffer_size);
+ end
+ else -- error, unexpected input
+ conn:write("\5\1\0\3\0\0\0"); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte)
+ conn:close();
+ module:log("debug", "Invalid SOCKS5 negotiation recieved: '%s'", b64(data));
+ end
+ end
+end
+
+function listener.ondisconnect(conn, err)
+ local session = sessions[conn];
+ if session then
+ if transfers[session.sha] then
+ local initiator, target = transfers[session.sha].initiator, transfers[session.sha].target;
+ if initiator == conn and target ~= nil then
+ target:close();
+ elseif target == conn and initiator ~= nil then
+ initiator:close();
+ end
+ transfers[session.sha] = nil;
+ end
+ -- Clean up any session-related stuff here
+ sessions[conn] = nil;
+ end
+end
+
+function module.add_host(module)
+ local host, name = module:get_host(), module:get_option_string("name", "SOCKS5 Bytestreams Service");
+
+ local proxy_address = module:get_option("proxy65_address", host);
+ local proxy_port = next(portmanager.get_active_services():search("proxy65", nil)[1] or {});
+ local proxy_acl = module:get_option("proxy65_acl");
+
+ -- COMPAT w/pre-0.9 where proxy65_port was specified in the components section of the config
+ local legacy_config = module:get_option_number("proxy65_port");
+ if legacy_config then
+ module:log("warn", "proxy65_port is deprecated, please put proxy65_ports = { %d } into the global section instead", legacy_config);
+ end
+
+ module:add_identity("proxy", "bytestreams", name);
+ module:add_feature("http://jabber.org/protocol/bytestreams");
+
+ module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if not stanza.tags[1].attr.node then
+ origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#info")
+ :tag("identity", {category='proxy', type='bytestreams', name=name}):up()
+ :tag("feature", {var="http://jabber.org/protocol/bytestreams"}) );
+ return true;
+ end
+ end, -1);
+
+ module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if not stanza.tags[1].attr.node then
+ origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#items"));
+ return true;
+ end
+ end, -1);
+
+ module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+
+ -- check ACL
+ while proxy_acl and #proxy_acl > 0 do -- using 'while' instead of 'if' so we can break out of it
+ local jid = stanza.attr.from;
+ local allow;
+ for _, acl in ipairs(proxy_acl) do
+ if jid_compare(jid, acl) then allow = true; break; end
+ end
+ if allow then break; end
+ module:log("warn", "Denying use of proxy for %s", tostring(stanza.attr.from));
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
+ return true;
+ end
+
+ local sid = stanza.tags[1].attr.sid;
+ origin.send(st.reply(stanza):tag("query", {xmlns="http://jabber.org/protocol/bytestreams", sid=sid})
+ :tag("streamhost", {jid=host, host=proxy_address, port=proxy_port}));
+ return true;
+ end);
+
+ module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+
+ local query = stanza.tags[1];
+ local sid = query.attr.sid;
+ local from = stanza.attr.from;
+ local to = query:get_child_text("activate");
+ local prepped_to = jid_prep(to);
+
+ local info = "sid: "..tostring(sid)..", initiator: "..tostring(from)..", target: "..tostring(prepped_to or to);
+ if prepped_to and sid then
+ local sha = sha1(sid .. from .. prepped_to, true);
+ if not transfers[sha] then
+ module:log("debug", "Activation request has unknown session id; activation failed (%s)", info);
+ origin.send(st.error_reply(stanza, "modify", "item-not-found"));
+ elseif not transfers[sha].initiator then
+ module:log("debug", "The sender was not connected to the proxy; activation failed (%s)", info);
+ origin.send(st.error_reply(stanza, "cancel", "not-allowed", "The sender (you) is not connected to the proxy"));
+ --elseif not transfers[sha].target then -- can't happen, as target is set when a transfer object is created
+ -- module:log("debug", "The recipient was not connected to the proxy; activation failed (%s)", info);
+ -- origin.send(st.error_reply(stanza, "cancel", "not-allowed", "The recipient is not connected to the proxy"));
+ else -- if transfers[sha].initiator ~= nil and transfers[sha].target ~= nil then
+ module:log("debug", "Transfer activated (%s)", info);
+ transfers[sha].activated = true;
+ transfers[sha].target:resume();
+ transfers[sha].initiator:resume();
+ origin.send(st.reply(stanza));
+ end
+ elseif to and sid then
+ module:log("debug", "Malformed activation jid; activation failed (%s)", info);
+ origin.send(st.error_reply(stanza, "modify", "jid-malformed"));
+ else
+ module:log("debug", "Bad request; activation failed (%s)", info);
+ origin.send(st.error_reply(stanza, "modify", "bad-request"));
+ end
+ return true;
+ end);
+end
+
+module:provides("net", {
+ default_port = 5000;
+ listener = listener;
+ multiplex = {
+ pattern = "^\5";
+ };
+});
diff --git a/plugins/mod_pubsub.lua b/plugins/mod_pubsub.lua
new file mode 100644
index 00000000..22969ab5
--- /dev/null
+++ b/plugins/mod_pubsub.lua
@@ -0,0 +1,466 @@
+local pubsub = require "util.pubsub";
+local st = require "util.stanza";
+local jid_bare = require "util.jid".bare;
+local uuid_generate = require "util.uuid".generate;
+local usermanager = require "core.usermanager";
+
+local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
+local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
+local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
+local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
+
+local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
+local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
+local pubsub_disco_name = module:get_option("name");
+if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end
+
+local service;
+
+local handlers = {};
+
+function handle_pubsub_iq(event)
+ local origin, stanza = event.origin, event.stanza;
+ local pubsub = stanza.tags[1];
+ local action = pubsub.tags[1];
+ if not action then
+ return origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+ end
+ local handler = handlers[stanza.attr.type.."_"..action.name];
+ if handler then
+ handler(origin, stanza, action);
+ return true;
+ end
+end
+
+local pubsub_errors = {
+ ["conflict"] = { "cancel", "conflict" };
+ ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
+ ["jid-required"] = { "modify", "bad-request", nil, "jid-required" };
+ ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" };
+ ["item-not-found"] = { "cancel", "item-not-found" };
+ ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
+ ["forbidden"] = { "cancel", "forbidden" };
+};
+function pubsub_error_reply(stanza, error)
+ local e = pubsub_errors[error];
+ local reply = st.error_reply(stanza, unpack(e, 1, 3));
+ if e[4] then
+ reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up();
+ end
+ return reply;
+end
+
+function handlers.get_items(origin, stanza, items)
+ local node = items.attr.node;
+ local item = items:get_child("item");
+ local id = item and item.attr.id;
+
+ if not node then
+ return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+ end
+ local ok, results = service:get_items(node, stanza.attr.from, id);
+ if not ok then
+ return origin.send(pubsub_error_reply(stanza, results));
+ end
+
+ local data = st.stanza("items", { node = node });
+ for _, entry in pairs(results) do
+ data:add_child(entry);
+ end
+ local reply;
+ if data then
+ reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub })
+ :add_child(data);
+ else
+ reply = pubsub_error_reply(stanza, "item-not-found");
+ end
+ return origin.send(reply);
+end
+
+function handlers.get_subscriptions(origin, stanza, subscriptions)
+ local node = subscriptions.attr.node;
+ if not node then
+ return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+ end
+ local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from);
+ if not ok then
+ return origin.send(pubsub_error_reply(stanza, ret));
+ end
+ local reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub })
+ :tag("subscriptions");
+ for _, sub in ipairs(ret) do
+ reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
+ end
+ return origin.send(reply);
+end
+
+function handlers.set_create(origin, stanza, create)
+ local node = create.attr.node;
+ local ok, ret, reply;
+ if node then
+ ok, ret = service:create(node, stanza.attr.from);
+ if ok then
+ reply = st.reply(stanza);
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ else
+ repeat
+ node = uuid_generate();
+ ok, ret = service:create(node, stanza.attr.from);
+ until ok or ret ~= "conflict";
+ if ok then
+ reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub })
+ :tag("create", { node = node });
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ end
+ return origin.send(reply);
+end
+
+function handlers.set_delete(origin, stanza, delete)
+ local node = delete.attr.node;
+
+ local reply, notifier;
+ if not node then
+ return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+ end
+ local ok, ret = service:delete(node, stanza.attr.from);
+ if ok then
+ reply = st.reply(stanza);
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ return origin.send(reply);
+end
+
+function handlers.set_subscribe(origin, stanza, subscribe)
+ local node, jid = subscribe.attr.node, subscribe.attr.jid;
+ if not (node and jid) then
+ return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
+ end
+ --[[
+ local options_tag, options = stanza.tags[1]:get_child("options"), nil;
+ if options_tag then
+ options = options_form:data(options_tag.tags[1]);
+ end
+ --]]
+ local options_tag, options; -- FIXME
+ local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
+ local reply;
+ if ok then
+ reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub })
+ :tag("subscription", {
+ node = node,
+ jid = jid,
+ subscription = "subscribed"
+ }):up();
+ if options_tag then
+ reply:add_child(options_tag);
+ end
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ origin.send(reply);
+end
+
+function handlers.set_unsubscribe(origin, stanza, unsubscribe)
+ local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
+ if not (node and jid) then
+ return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
+ end
+ local ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
+ local reply;
+ if ok then
+ reply = st.reply(stanza);
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ return origin.send(reply);
+end
+
+function handlers.set_publish(origin, stanza, publish)
+ local node = publish.attr.node;
+ if not node then
+ return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+ end
+ local item = publish:get_child("item");
+ local id = (item and item.attr.id);
+ if not id then
+ id = uuid_generate();
+ if item then
+ item.attr.id = id;
+ end
+ end
+ local ok, ret = service:publish(node, stanza.attr.from, id, item);
+ local reply;
+ if ok then
+ reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub })
+ :tag("publish", { node = node })
+ :tag("item", { id = id });
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ return origin.send(reply);
+end
+
+function handlers.set_retract(origin, stanza, retract)
+ local node, notify = retract.attr.node, retract.attr.notify;
+ notify = (notify == "1") or (notify == "true");
+ local item = retract:get_child("item");
+ local id = item and item.attr.id
+ if not (node and id) then
+ return origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required"));
+ end
+ local reply, notifier;
+ if notify then
+ notifier = st.stanza("retract", { id = id });
+ end
+ local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
+ if ok then
+ reply = st.reply(stanza);
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ return origin.send(reply);
+end
+
+function handlers.set_purge(origin, stanza, purge)
+ local node, notify = purge.attr.node, purge.attr.notify;
+ notify = (notify == "1") or (notify == "true");
+ local reply;
+ if not node then
+ return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+ end
+ local ok, ret = service:purge(node, stanza.attr.from, notify);
+ if ok then
+ reply = st.reply(stanza);
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ return origin.send(reply);
+end
+
+function simple_broadcast(kind, node, jids, item)
+ if item then
+ item = st.clone(item);
+ item.attr.xmlns = nil; -- Clear the pubsub namespace
+ end
+ local message = st.message({ from = module.host, type = "headline" })
+ :tag("event", { xmlns = xmlns_pubsub_event })
+ :tag(kind, { node = node })
+ :add_child(item);
+ for jid in pairs(jids) do
+ module:log("debug", "Sending notification to %s", jid);
+ message.attr.to = jid;
+ module:send(message);
+ end
+end
+
+module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
+module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);
+
+local disco_info;
+
+local feature_map = {
+ create = { "create-nodes", "instant-nodes", "item-ids" };
+ retract = { "delete-items", "retract-items" };
+ purge = { "purge-nodes" };
+ publish = { "publish", autocreate_on_publish and "auto-create" };
+ delete = { "delete-nodes" };
+ get_items = { "retrieve-items" };
+ add_subscription = { "subscribe" };
+ get_subscriptions = { "retrieve-subscriptions" };
+};
+
+local function add_disco_features_from_service(disco, service)
+ for method, features in pairs(feature_map) do
+ if service[method] then
+ for _, feature in ipairs(features) do
+ if feature then
+ disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up();
+ end
+ end
+ end
+ end
+ for affiliation in pairs(service.config.capabilities) do
+ if affiliation ~= "none" and affiliation ~= "owner" then
+ disco:tag("feature", { var = xmlns_pubsub.."#"..affiliation.."-affiliation" }):up();
+ end
+ end
+end
+
+local function build_disco_info(service)
+ local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" })
+ :tag("identity", { category = "pubsub", type = "service", name = pubsub_disco_name }):up()
+ :tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up();
+ add_disco_features_from_service(disco_info, service);
+ return disco_info;
+end
+
+module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event)
+ local origin, stanza = event.origin, event.stanza;
+ local node = stanza.tags[1].attr.node;
+ if not node then
+ return origin.send(st.reply(stanza):add_child(disco_info));
+ else
+ local ok, ret = service:get_nodes(stanza.attr.from);
+ if ok and not ret[node] then
+ ok, ret = false, "item-not-found";
+ end
+ if not ok then
+ return origin.send(pubsub_error_reply(stanza, ret));
+ end
+ local reply = st.reply(stanza)
+ :tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node })
+ :tag("identity", { category = "pubsub", type = "leaf" });
+ return origin.send(reply);
+ end
+end);
+
+local function handle_disco_items_on_node(event)
+ local stanza, origin = event.stanza, event.origin;
+ local query = stanza.tags[1];
+ local node = query.attr.node;
+ local ok, ret = service:get_items(node, stanza.attr.from);
+ if not ok then
+ return origin.send(pubsub_error_reply(stanza, ret));
+ end
+
+ local reply = st.reply(stanza)
+ :tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node });
+
+ for id, item in pairs(ret) do
+ reply:tag("item", { jid = module.host, name = id }):up();
+ end
+
+ return origin.send(reply);
+end
+
+
+module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event)
+ if event.stanza.tags[1].attr.node then
+ return handle_disco_items_on_node(event);
+ end
+ local ok, ret = service:get_nodes(event.stanza.attr.from);
+ if not ok then
+ event.origin.send(pubsub_error_reply(event.stanza, ret));
+ else
+ local reply = st.reply(event.stanza)
+ :tag("query", { xmlns = "http://jabber.org/protocol/disco#items" });
+ for node, node_obj in pairs(ret) do
+ reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up();
+ end
+ event.origin.send(reply);
+ end
+ return true;
+end);
+
+local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
+local function get_affiliation(jid)
+ local bare_jid = jid_bare(jid);
+ if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then
+ return admin_aff;
+ end
+end
+
+function set_service(new_service)
+ service = new_service;
+ module.environment.service = service;
+ disco_info = build_disco_info(service);
+end
+
+function module.save()
+ return { service = service };
+end
+
+function module.restore(data)
+ set_service(data.service);
+end
+
+set_service(pubsub.new({
+ capabilities = {
+ none = {
+ create = false;
+ publish = false;
+ retract = false;
+ get_nodes = true;
+
+ subscribe = true;
+ unsubscribe = true;
+ get_subscription = true;
+ get_subscriptions = true;
+ get_items = true;
+
+ subscribe_other = false;
+ unsubscribe_other = false;
+ get_subscription_other = false;
+ get_subscriptions_other = false;
+
+ be_subscribed = true;
+ be_unsubscribed = true;
+
+ set_affiliation = false;
+ };
+ publisher = {
+ create = false;
+ publish = true;
+ retract = true;
+ get_nodes = true;
+
+ subscribe = true;
+ unsubscribe = true;
+ get_subscription = true;
+ get_subscriptions = true;
+ get_items = true;
+
+ subscribe_other = false;
+ unsubscribe_other = false;
+ get_subscription_other = false;
+ get_subscriptions_other = false;
+
+ be_subscribed = true;
+ be_unsubscribed = true;
+
+ set_affiliation = false;
+ };
+ owner = {
+ create = true;
+ publish = true;
+ retract = true;
+ delete = true;
+ get_nodes = true;
+
+ subscribe = true;
+ unsubscribe = true;
+ get_subscription = true;
+ get_subscriptions = true;
+ get_items = true;
+
+
+ subscribe_other = true;
+ unsubscribe_other = true;
+ get_subscription_other = true;
+ get_subscriptions_other = true;
+
+ be_subscribed = true;
+ be_unsubscribed = true;
+
+ set_affiliation = true;
+ };
+ };
+
+ autocreate_on_publish = autocreate_on_publish;
+ autocreate_on_subscribe = autocreate_on_subscribe;
+
+ broadcaster = simple_broadcast;
+ get_affiliation = get_affiliation;
+
+ normalize_jid = jid_bare;
+}));
diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua
index b1bf4cc0..141a4997 100644
--- a/plugins/mod_register.lua
+++ b/plugins/mod_register.lua
@@ -1,84 +1,257 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
local st = require "util.stanza";
-local send = require "core.sessionmanager".send_to_session;
+local dataform_new = require "util.dataforms".new;
local usermanager_user_exists = require "core.usermanager".user_exists;
local usermanager_create_user = require "core.usermanager".create_user;
+local usermanager_set_password = require "core.usermanager".set_password;
+local usermanager_delete_user = require "core.usermanager".delete_user;
+local os_time = os.time;
+local nodeprep = require "util.encodings".stringprep.nodeprep;
+local jid_bare = require "util.jid".bare;
-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();
- send(session, 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
- send(session, st.error_reply(stanza, "cancel", "not-allowed"));
- 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 = table.concat(username);
- password = table.concat(password);
- if username == session.username then
- if usermanager_create_user(username, password, session.host) then -- password change -- TODO is this the right way?
- send(session, st.reply(stanza));
- else
- -- TODO internal error, unable to write file, file may be locked, etc
- end
+local compat = module:get_option_boolean("registration_compat", true);
+local allow_registration = module:get_option_boolean("allow_registration", false);
+local additional_fields = module:get_option("additional_registration_fields", {});
+
+local account_details = module:open_store("account_details");
+
+local field_map = {
+ username = { name = "username", type = "text-single", label = "Username", required = true };
+ password = { name = "password", type = "text-private", label = "Password", required = true };
+ nick = { name = "nick", type = "text-single", label = "Nickname" };
+ name = { name = "name", type = "text-single", label = "Full Name" };
+ first = { name = "first", type = "text-single", label = "Given Name" };
+ last = { name = "last", type = "text-single", label = "Family Name" };
+ email = { name = "email", type = "text-single", label = "Email" };
+ address = { name = "address", type = "text-single", label = "Street" };
+ city = { name = "city", type = "text-single", label = "City" };
+ state = { name = "state", type = "text-single", label = "State" };
+ zip = { name = "zip", type = "text-single", label = "Postal code" };
+ phone = { name = "phone", type = "text-single", label = "Telephone number" };
+ url = { name = "url", type = "text-single", label = "Webpage" };
+ date = { name = "date", type = "text-single", label = "Birth date" };
+};
+
+local registration_form = dataform_new{
+ title = "Creating a new account";
+ instructions = "Choose a username and password for use with this service.";
+
+ field_map.username;
+ field_map.password;
+};
+
+local registration_query = st.stanza("query", {xmlns = "jabber:iq:register"})
+ :tag("instructions"):text("Choose a username and password for use with this service."):up()
+ :tag("username"):up()
+ :tag("password"):up();
+
+for _, field in ipairs(additional_fields) do
+ if type(field) == "table" then
+ registration_form[#registration_form + 1] = field;
+ else
+ if field:match("%+$") then
+ field = field:sub(1, #field - 1);
+ field_map[field].required = true;
+ end
+
+ registration_form[#registration_form + 1] = field_map[field];
+ registration_query:tag(field):up();
+ end
+end
+registration_query:add_child(registration_form:form());
+
+module:add_feature("jabber:iq:register");
+
+local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up();
+module:hook("stream-features", function(event)
+ local session, features = event.origin, event.features;
+
+ -- Advertise registration to unauthorized clients only.
+ if not(allow_registration) or session.type ~= "c2s_unauthed" then
+ return
+ end
+
+ features:add_child(register_stream_feature);
+end);
+
+local function handle_registration_stanza(event)
+ local session, stanza = event.origin, event.stanza;
+
+ local query = stanza.tags[1];
+ 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
+ local username, host = session.username, session.host;
+
+ local old_session_close = session.close;
+ session.close = function(session, ...)
+ session.send(st.reply(stanza));
+ return old_session_close(session, ...);
+ end
+
+ local ok, err = usermanager_delete_user(username, host);
+
+ if not ok then
+ module:log("debug", "Removing user account %s@%s failed: %s", username, host, err);
+ session.close = old_session_close;
+ session.send(st.error_reply(stanza, "cancel", "service-unavailable", err));
+ return true;
+ end
+
+ 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
- send(session, 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
- send(session, st.error_reply(stanza, "modify", "bad-request"));
+ session.send(st.error_reply(stanza, "modify", "bad-request"));
end
+ else
+ session.send(st.error_reply(stanza, "modify", "bad-request"));
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 function parse_response(query)
+ local form = query:get_child("x", "jabber:x:data");
+ if form then
+ return registration_form:data(form);
else
- send(session, st.error_reply(stanza, "cancel", "service-unavailable"));
- end;
-end);
+ local data = {};
+ local errors = {};
+ for _, field in ipairs(registration_form) do
+ local name, required = field.name, field.required;
+ if field_map[name] then
+ data[name] = query:get_child_text(name);
+ if (not data[name] or #data[name] == 0) and required then
+ errors[name] = "Required value missing";
+ end
+ end
+ end
+ if next(errors) then
+ return data, errors;
+ end
+ return data;
+ end
+end
+
+local recent_ips = {};
+local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations");
+local whitelist_only = module:get_option("whitelist_registration_only");
+local whitelisted_ips = module:get_option("registration_whitelist") or { "127.0.0.1" };
+local blacklisted_ips = module:get_option("registration_blacklist") or {};
+
+for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end
+for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end
-add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, stanza)
- if stanza.tags[1].name == "query" then
+module:hook("stanza/iq/jabber:iq:register:query", function(event)
+ local session, stanza = event.origin, event.stanza;
+
+ if not(allow_registration) or session.type ~= "c2s_unauthed" then
+ session.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ else
local query = stanza.tags[1];
if stanza.attr.type == "get" then
local reply = st.reply(stanza);
- reply:tag("query", {xmlns = "jabber:iq:register"})
- :tag("instructions"):text("Choose a username and password for use with this service."):up()
- :tag("username"):up()
- :tag("password"):up();
- send(session, reply);
+ reply:add_child(registration_query);
+ session.send(reply);
elseif stanza.attr.type == "set" then
if query.tags[1] and query.tags[1].name == "remove" then
- send(session, st.error_reply(stanza, "auth", "registration-required"));
+ session.send(st.error_reply(stanza, "auth", "registration-required"));
else
- local username = query:child_with_name("username");
- local password = query:child_with_name("password");
- if username and password then
- -- FIXME shouldn't use table.concat
- username = table.concat(username);
- password = table.concat(password);
- if usermanager_user_exists(username, session.host) then
- send(session, st.error_reply(stanza, "cancel", "conflict"));
+ local data, errors = parse_response(query);
+ if errors then
+ session.send(st.error_reply(stanza, "modify", "not-acceptable"));
+ else
+ -- Check that the user is not blacklisted or registering too often
+ if not session.ip then
+ module:log("debug", "User's IP not known; can't apply blacklist/whitelist");
+ 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 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 };
+ else
+ local ip = recent_ips[session.ip];
+ ip.count = ip.count + 1;
+
+ if os_time() - ip.time < min_seconds_between_registrations then
+ ip.time = os_time();
+ session.send(st.error_reply(stanza, "wait", "not-acceptable"));
+ return true;
+ end
+ ip.time = os_time();
+ end
+ end
+ local username, password = nodeprep(data.username), data.password;
+ data.username, data.password = nil, nil;
+ local host = module.host;
+ if not username or username == "" then
+ session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is invalid."));
+ return true;
+ end
+ local user = { username = username , host = host, allowed = true }
+ module:fire_event("user-registering", user);
+ if not user.allowed then
+ session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is forbidden."));
+ elseif usermanager_user_exists(username, host) then
+ session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists."));
else
- if usermanager_create_user(username, password, session.host) then
- send(session, st.reply(stanza)); -- user created!
+ -- TODO unable to write file, file may be locked, etc, what's the correct error?
+ local error_reply = st.error_reply(stanza, "wait", "internal-server-error", "Failed to write data to disk.");
+ if usermanager_create_user(username, password, host) then
+ if next(data) and not account_details:set(username, data) then
+ usermanager_delete_user(username, host);
+ session.send(error_reply);
+ return true;
+ end
+ session.send(st.reply(stanza)); -- user created!
+ module:log("info", "User account created: %s@%s", username, host);
+ module:fire_event("user-registered", {
+ username = username, host = host, source = "mod_register",
+ session = session });
else
- -- TODO internal error, unable to write file, file may be locked, etc
+ session.send(error_reply);
end
end
- else
- send(session, st.error_reply(stanza, "modify", "not-acceptable"));
end
end
end
- else
- send(session, 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 8b4a3a0a..d530bb45 100644
--- a/plugins/mod_roster.lua
+++ b/plugins/mod_roster.lua
@@ -1,24 +1,156 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
local st = require "util.stanza"
-local send = require "core.sessionmanager".send_to_session
-
-add_iq_handler("c2s", "jabber:iq:roster",
- function (session, stanza)
- if stanza.attr.type == "get" then
- session.roster = session.roster or rostermanager.getroster(session.username, session.host);
- if session.roster == false then
- send(session, st.reply(stanza)
- :tag("error", { type = "wait" })
- :tag("internal-server-error", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}));
- return true;
- else session.roster = session.roster or {};
+
+local jid_split = require "util.jid".split;
+local jid_prep = require "util.jid".prep;
+local t_concat = table.concat;
+local tonumber = tonumber;
+local pairs, ipairs = pairs, ipairs;
+
+local rm_load_roster = require "core.rostermanager".load_roster;
+local rm_remove_from_roster = require "core.rostermanager".remove_from_roster;
+local rm_add_to_roster = require "core.rostermanager".add_to_roster;
+local rm_roster_push = require "core.rostermanager".roster_push;
+local core_post_stanza = prosody.core_post_stanza;
+
+module:add_feature("jabber:iq:roster");
+
+local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"});
+module:hook("stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if origin.username then
+ features:add_child(rosterver_stream_feature);
+ end
+end);
+
+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
+ roster:up(); -- move out from item
end
- local roster = st.reply(stanza)
- :query("jabber:iq:roster");
- for jid in pairs(session.roster) do
- roster:tag("item", { jid = jid, subscription = "none" }):up();
+ 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 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
+ else
+ -- Trying to add self to roster
+ session.send(st.error_reply(stanza, "cancel", "not-allowed"));
end
- send(session, roster);
- return true;
+ else
+ -- Invalid JID added to roster
+ session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error?
+ 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);
+
+module:hook_global("user-deleted", function(event)
+ local username, host = event.username, event.host;
+ if host ~= module.host then return end
+ local bare = username .. "@" .. host;
+ local roster = rm_load_roster(username, host);
+ for jid, item in pairs(roster) do
+ if jid and jid ~= "pending" then
+ if item.subscription == "both" or item.subscription == "from" or (roster.pending and roster.pending[jid]) then
+ module:send(st.presence({type="unsubscribed", from=bare, to=jid}));
+ end
+ if item.subscription == "both" or item.subscription == "to" or item.ask then
+ module:send(st.presence({type="unsubscribe", from=bare, to=jid}));
end
- end); \ No newline at end of file
+ end
+ end
+end, 300);
diff --git a/plugins/mod_s2s/mod_s2s.lua b/plugins/mod_s2s/mod_s2s.lua
new file mode 100644
index 00000000..30ebb706
--- /dev/null
+++ b/plugins/mod_s2s/mod_s2s.lua
@@ -0,0 +1,677 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+module:set_global();
+
+local prosody = prosody;
+local hosts = prosody.hosts;
+local core_process_stanza = prosody.core_process_stanza;
+
+local tostring, type = tostring, type;
+local t_insert = table.insert;
+local xpcall, traceback = xpcall, debug.traceback;
+local NULL = {};
+
+local add_task = require "util.timer".add_task;
+local st = require "util.stanza";
+local initialize_filters = require "util.filters".initialize;
+local nameprep = require "util.encodings".stringprep.nameprep;
+local new_xmpp_stream = require "util.xmppstream".new;
+local s2s_new_incoming = require "core.s2smanager".new_incoming;
+local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
+local s2s_destroy_session = require "core.s2smanager".destroy_session;
+local uuid_gen = require "util.uuid".generate;
+local cert_verify_identity = require "util.x509".verify_identity;
+local fire_global_event = prosody.events.fire_event;
+
+local s2sout = module:require("s2sout");
+
+local connect_timeout = module:get_option_number("s2s_timeout", 90);
+local stream_close_timeout = module:get_option_number("s2s_close_timeout", 5);
+local opt_keepalives = module:get_option_boolean("s2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true));
+local secure_auth = module:get_option_boolean("s2s_secure_auth", false); -- One day...
+local secure_domains, insecure_domains =
+ module:get_option_set("s2s_secure_domains", {})._items, module:get_option_set("s2s_insecure_domains", {})._items;
+local require_encryption = module:get_option_boolean("s2s_require_encryption", secure_auth);
+
+local sessions = module:shared("sessions");
+
+local log = module._log;
+
+--- Handle stanzas to remote domains
+
+local bouncy_stanzas = { message = true, presence = true, iq = true };
+local function bounce_sendq(session, reason)
+ local sendq = session.sendq;
+ if not sendq then return; end
+ session.log("info", "sending error replies for "..#sendq.." queued stanzas because of failed outgoing connection to "..tostring(session.to_host));
+ local dummy = {
+ type = "s2sin";
+ send = function(s)
+ (session.log or log)("error", "Replying to to an s2s error reply, please report this! Traceback: %s", traceback());
+ end;
+ dummy = true;
+ };
+ for i, data in ipairs(sendq) do
+ local reply = data[2];
+ if reply and not(reply.attr.xmlns) and bouncy_stanzas[reply.name] then
+ reply.attr.type = "error";
+ reply:tag("error", {type = "cancel"})
+ :tag("remote-server-not-found", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up();
+ if reason then
+ reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})
+ :text("Server-to-server connection failed: "..reason):up();
+ end
+ core_process_stanza(dummy, reply);
+ end
+ sendq[i] = nil;
+ end
+ session.sendq = nil;
+end
+
+-- Handles stanzas to existing s2s sessions
+function route_to_existing_session(event)
+ local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza;
+ if not hosts[from_host] then
+ log("warn", "Attempt to send stanza from %s - a host we don't serve", from_host);
+ return false;
+ end
+ if hosts[to_host] then
+ log("warn", "Attempt to route stanza to a remote %s - a host we do serve?!", from_host);
+ return false;
+ end
+ local host = hosts[from_host].s2sout[to_host];
+ if host then
+ -- We have a connection to this host already
+ if host.type == "s2sout_unauthed" and (stanza.name ~= "db:verify" or not host.dialback_key) then
+ (host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host);
+
+ -- Queue stanza until we are able to send it
+ if host.sendq then t_insert(host.sendq, {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)});
+ else host.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} }; end
+ host.log("debug", "stanza [%s] queued ", stanza.name);
+ return true;
+ elseif host.type == "local" or host.type == "component" then
+ log("error", "Trying to send a stanza to ourselves??")
+ log("error", "Traceback: %s", traceback());
+ log("error", "Stanza: %s", tostring(stanza));
+ return false;
+ else
+ (host.log or log)("debug", "going to send stanza to "..to_host.." from "..from_host);
+ -- FIXME
+ if host.from_host ~= from_host then
+ log("error", "WARNING! This might, possibly, be a bug, but it might not...");
+ log("error", "We are going to send from %s instead of %s", tostring(host.from_host), tostring(from_host));
+ end
+ if host.sends2s(stanza) then
+ host.log("debug", "stanza sent over %s", host.type);
+ return true;
+ end
+ end
+ end
+end
+
+-- Create a new outgoing session for a stanza
+function route_to_new_session(event)
+ local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza;
+ log("debug", "opening a new outgoing connection for this stanza");
+ local host_session = s2s_new_outgoing(from_host, to_host);
+
+ -- Store in buffer
+ host_session.bounce_sendq = bounce_sendq;
+ host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
+ log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name));
+ s2sout.initiate_connection(host_session);
+ if (not host_session.connecting) and (not host_session.conn) then
+ log("warn", "Connection to %s failed already, destroying session...", to_host);
+ s2s_destroy_session(host_session, "Connection failed");
+ return false;
+ end
+ return true;
+end
+
+function module.add_host(module)
+ if module:get_option_boolean("disallow_s2s", false) then
+ module:log("warn", "The 'disallow_s2s' config option is deprecated, please see http://prosody.im/doc/s2s#disabling");
+ return nil, "This host has disallow_s2s set";
+ end
+ module:hook("route/remote", route_to_existing_session, -1);
+ module:hook("route/remote", route_to_new_session, -10);
+ module:hook("s2s-authenticated", make_authenticated, -1);
+end
+
+-- Stream is authorised, and ready for normal stanzas
+function mark_connected(session)
+ local sendq, send = session.sendq, session.sends2s;
+
+ local from, to = session.from_host, session.to_host;
+
+ session.log("info", "%s s2s connection %s->%s complete", session.direction, from, to);
+
+ local event_data = { session = session };
+ if session.type == "s2sout" then
+ fire_global_event("s2sout-established", event_data);
+ hosts[from].events.fire_event("s2sout-established", event_data);
+ else
+ local host_session = hosts[to];
+ session.send = function(stanza)
+ return host_session.events.fire_event("route/remote", { from_host = to, to_host = from, stanza = stanza });
+ end;
+
+ fire_global_event("s2sin-established", event_data);
+ hosts[to].events.fire_event("s2sin-established", event_data);
+ end
+
+ if session.direction == "outgoing" then
+ if sendq then
+ session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host);
+ for i, data in ipairs(sendq) do
+ send(data[1]);
+ sendq[i] = nil;
+ end
+ session.sendq = nil;
+ end
+
+ session.ip_hosts = nil;
+ session.srv_hosts = nil;
+ end
+end
+
+function make_authenticated(event)
+ local session, host = event.session, event.host;
+ if not session.secure then
+ if require_encryption or secure_auth or secure_domains[host] then
+ session:close({
+ condition = "policy-violation",
+ text = "Encrypted server-to-server communication is required but was not "
+ ..((session.direction == "outgoing" and "offered") or "used")
+ });
+ end
+ end
+ if hosts[host] then
+ session:close({ condition = "undefined-condition", text = "Attempt to authenticate as a host we serve" });
+ end
+ if session.type == "s2sout_unauthed" then
+ session.type = "s2sout";
+ elseif session.type == "s2sin_unauthed" then
+ session.type = "s2sin";
+ if host then
+ if not session.hosts[host] then session.hosts[host] = {}; end
+ session.hosts[host].authed = true;
+ end
+ elseif session.type == "s2sin" and host then
+ if not session.hosts[host] then session.hosts[host] = {}; end
+ session.hosts[host].authed = true;
+ else
+ return false;
+ end
+ session.log("debug", "connection %s->%s is now authenticated for %s", session.from_host, session.to_host, host);
+
+ mark_connected(session);
+
+ return true;
+end
+
+--- Helper to check that a session peer's certificate is valid
+local function check_cert_status(session)
+ local host = session.direction == "outgoing" and session.to_host or session.from_host
+ local conn = session.conn:socket()
+ local cert
+ if conn.getpeercertificate then
+ cert = conn:getpeercertificate()
+ end
+
+ if cert then
+ local chain_valid, errors;
+ if conn.getpeerverification then
+ chain_valid, errors = conn:getpeerverification();
+ elseif conn.getpeerchainvalid then -- COMPAT mw/luasec-hg
+ chain_valid, errors = conn:getpeerchainvalid();
+ errors = (not chain_valid) and { { errors } } or nil;
+ else
+ chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } };
+ end
+ -- Is there any interest in printing out all/the number of errors here?
+ if not chain_valid then
+ (session.log or log)("debug", "certificate chain validation result: invalid");
+ for depth, t in ipairs(errors or NULL) do
+ (session.log or log)("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", "))
+ end
+ session.cert_chain_status = "invalid";
+ else
+ (session.log or log)("debug", "certificate chain validation result: valid");
+ session.cert_chain_status = "valid";
+
+ -- We'll go ahead and verify the asserted identity if the
+ -- connecting server specified one.
+ if host then
+ if cert_verify_identity(host, "xmpp-server", cert) then
+ session.cert_identity_status = "valid"
+ else
+ session.cert_identity_status = "invalid"
+ end
+ end
+ end
+ end
+ return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert });
+end
+
+--- XMPP stream event handlers
+
+local stream_callbacks = { default_ns = "jabber:server", handlestanza = core_process_stanza };
+
+local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
+
+function stream_callbacks.streamopened(session, attr)
+ local send = session.sends2s;
+
+ session.version = tonumber(attr.version) or 0;
+
+ -- TODO: Rename session.secure to session.encrypted
+ if session.secure == false then
+ session.secure = true;
+
+ -- Check if TLS compression is used
+ local sock = session.conn:socket();
+ if sock.info then
+ session.compressed = sock:info"compression";
+ elseif sock.compression then
+ session.compressed = sock:compression(); --COMPAT mw/luasec-hg
+ end
+ end
+
+ if session.direction == "incoming" then
+ -- Send a reply stream header
+
+ -- Validate to/from
+ local to, from = nameprep(attr.to), nameprep(attr.from);
+ if not to and attr.to then -- COMPAT: Some servers do not reliably set 'to' (especially on stream restarts)
+ session:close({ condition = "improper-addressing", text = "Invalid 'to' address" });
+ return;
+ end
+ if not from and attr.from then -- COMPAT: Some servers do not reliably set 'from' (especially on stream restarts)
+ session:close({ condition = "improper-addressing", text = "Invalid 'from' address" });
+ return;
+ end
+
+ -- Set session.[from/to]_host if they have not been set already and if
+ -- this session isn't already authenticated
+ if session.type == "s2sin_unauthed" and from and not session.from_host then
+ session.from_host = from;
+ elseif from ~= session.from_host then
+ session:close({ condition = "improper-addressing", text = "New stream 'from' attribute does not match original" });
+ return;
+ end
+ if session.type == "s2sin_unauthed" and to and not session.to_host then
+ session.to_host = to;
+ elseif to ~= session.to_host then
+ session:close({ condition = "improper-addressing", text = "New stream 'to' attribute does not match original" });
+ return;
+ end
+
+ -- For convenience we'll put the sanitised values into these variables
+ to, from = session.to_host, session.from_host;
+
+ session.streamid = uuid_gen();
+ (session.log or log)("debug", "Incoming s2s received %s", st.stanza("stream:stream", attr):top_tag());
+ if to then
+ if not hosts[to] then
+ -- Attempting to connect to a host we don't serve
+ session:close({
+ condition = "host-unknown";
+ text = "This host does not serve "..to
+ });
+ return;
+ elseif not hosts[to].modules.s2s then
+ -- Attempting to connect to a host that disallows s2s
+ session:close({
+ condition = "policy-violation";
+ text = "Server-to-server communication is disabled for this host";
+ });
+ return;
+ end
+ end
+
+ if hosts[from] then
+ session:close({ condition = "undefined-condition", text = "Attempt to connect from a host we serve" });
+ return;
+ end
+
+ if session.secure and not session.cert_chain_status then
+ if check_cert_status(session) == false then
+ return;
+ end
+ end
+
+ session:open_stream(session.to_host, session.from_host)
+ if session.version >= 1.0 then
+ local features = st.stanza("stream:features");
+
+ if to then
+ hosts[to].events.fire_event("s2s-stream-features", { origin = session, features = features });
+ else
+ (session.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", from or "unknown host");
+ end
+
+ log("debug", "Sending stream features: %s", tostring(features));
+ send(features);
+ end
+ elseif session.direction == "outgoing" then
+ -- If we are just using the connection for verifying dialback keys, we won't try and auth it
+ if not attr.id then error("stream response did not give us a streamid!!!"); end
+ session.streamid = attr.id;
+
+ if session.secure and not session.cert_chain_status then
+ if check_cert_status(session) == false then
+ return;
+ end
+ end
+
+ -- Send unauthed buffer
+ -- (stanzas which are fine to send before dialback)
+ -- Note that this is *not* the stanza queue (which
+ -- we can only send if auth succeeds) :)
+ local send_buffer = session.send_buffer;
+ if send_buffer and #send_buffer > 0 then
+ log("debug", "Sending s2s send_buffer now...");
+ for i, data in ipairs(send_buffer) do
+ session.sends2s(tostring(data));
+ send_buffer[i] = nil;
+ end
+ end
+ session.send_buffer = nil;
+
+ -- If server is pre-1.0, don't wait for features, just do dialback
+ if session.version < 1.0 then
+ if not session.dialback_verifying then
+ hosts[session.from_host].events.fire_event("s2sout-authenticate-legacy", { origin = session });
+ else
+ mark_connected(session);
+ end
+ end
+ end
+ session.notopen = nil;
+end
+
+function stream_callbacks.streamclosed(session)
+ (session.log or log)("debug", "Received </stream:stream>");
+ session:close(false);
+end
+
+function stream_callbacks.error(session, error, data)
+ if error == "no-stream" then
+ session:close("invalid-namespace");
+ elseif error == "parse-error" then
+ session.log("debug", "Server-to-server XML parse error: %s", tostring(error));
+ session:close("not-well-formed");
+ elseif error == "stream-error" then
+ local condition, text = "undefined-condition";
+ for child in data:children() do
+ if child.attr.xmlns == xmlns_xmpp_streams then
+ if child.name ~= "text" then
+ condition = child.name;
+ else
+ text = child:get_text();
+ end
+ if condition ~= "undefined-condition" and text then
+ break;
+ end
+ end
+ end
+ text = condition .. (text and (" ("..text..")") or "");
+ session.log("info", "Session closed by remote with error: %s", text);
+ session:close(nil, text);
+ end
+end
+
+local function handleerr(err) log("error", "Traceback[s2s]: %s", traceback(tostring(err), 2)); end
+function stream_callbacks.handlestanza(session, stanza)
+ if stanza.attr.xmlns == "jabber:client" then --COMPAT: Prosody pre-0.6.2 may send jabber:client
+ stanza.attr.xmlns = nil;
+ end
+ stanza = session.filter("stanzas/in", stanza);
+ if stanza then
+ return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
+ end
+end
+
+local listener = {};
+
+--- Session methods
+local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
+local function session_close(session, reason, remote_reason)
+ local log = session.log or log;
+ if session.conn then
+ if session.notopen then
+ if session.direction == "incoming" then
+ session:open_stream(session.to_host, session.from_host);
+ else
+ session:open_stream(session.from_host, session.to_host);
+ end
+ end
+ if reason then -- nil == no err, initiated by us, false == initiated by remote
+ if type(reason) == "string" then -- assume stream error
+ log("debug", "Disconnecting %s[%s], <stream:error> is: %s", session.host or "(unknown host)", session.type, reason);
+ session.sends2s(st.stanza("stream:error"):tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' }));
+ elseif type(reason) == "table" then
+ if reason.condition then
+ local stanza = st.stanza("stream:error"):tag(reason.condition, stream_xmlns_attr):up();
+ if reason.text then
+ stanza:tag("text", stream_xmlns_attr):text(reason.text):up();
+ end
+ if reason.extra then
+ stanza:add_child(reason.extra);
+ end
+ log("debug", "Disconnecting %s[%s], <stream:error> is: %s", session.host or "(unknown host)", session.type, tostring(stanza));
+ session.sends2s(stanza);
+ elseif reason.name then -- a stanza
+ log("debug", "Disconnecting %s->%s[%s], <stream:error> is: %s", session.from_host or "(unknown host)", session.to_host or "(unknown host)", session.type, tostring(reason));
+ session.sends2s(reason);
+ end
+ end
+ end
+
+ session.sends2s("</stream:stream>");
+ function session.sends2s() return false; end
+
+ local reason = remote_reason or (reason and (reason.text or reason.condition)) or reason;
+ session.log("info", "%s s2s stream %s->%s closed: %s", session.direction, session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed");
+
+ -- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
+ local conn = session.conn;
+ if reason == nil and not session.notopen and session.type == "s2sin" then
+ add_task(stream_close_timeout, function ()
+ if not session.destroyed then
+ session.log("warn", "Failed to receive a stream close response, closing connection anyway...");
+ s2s_destroy_session(session, reason);
+ conn:close();
+ end
+ end);
+ else
+ s2s_destroy_session(session, reason);
+ conn:close(); -- Close immediately, as this is an outgoing connection or is not authed
+ end
+ end
+end
+
+function session_open_stream(session, from, to)
+ local attr = {
+ ["xmlns:stream"] = 'http://etherx.jabber.org/streams',
+ xmlns = 'jabber:server',
+ version = session.version and (session.version > 0 and "1.0" or nil),
+ ["xml:lang"] = 'en',
+ id = session.streamid,
+ from = from, to = to,
+ }
+ if not from or (hosts[from] and hosts[from].modules.dialback) then
+ attr["xmlns:db"] = 'jabber:server:dialback';
+ end
+
+ session.sends2s("<?xml version='1.0'?>");
+ session.sends2s(st.stanza("stream:stream", attr):top_tag());
+ return true;
+end
+
+-- Session initialization logic shared by incoming and outgoing
+local function initialize_session(session)
+ local stream = new_xmpp_stream(session, stream_callbacks);
+ session.stream = stream;
+
+ session.notopen = true;
+
+ function session.reset_stream()
+ session.notopen = true;
+ session.stream:reset();
+ end
+
+ session.open_stream = session_open_stream;
+
+ local filter = session.filter;
+ function session.data(data)
+ data = filter("bytes/in", data);
+ if data then
+ local ok, err = stream:feed(data);
+ if ok then return; end
+ (session.log or log)("warn", "Received invalid XML: %s", data);
+ (session.log or log)("warn", "Problem was: %s", err);
+ session:close("not-well-formed");
+ end
+ end
+
+ session.close = session_close;
+
+ local handlestanza = stream_callbacks.handlestanza;
+ function session.dispatch_stanza(session, stanza)
+ return handlestanza(session, stanza);
+ end
+
+ add_task(connect_timeout, function ()
+ if session.type == "s2sin" or session.type == "s2sout" then
+ return; -- Ok, we're connected
+ elseif session.type == "s2s_destroyed" then
+ return; -- Session already destroyed
+ end
+ -- Not connected, need to close session and clean up
+ (session.log or log)("debug", "Destroying incomplete session %s->%s due to inactivity",
+ session.from_host or "(unknown)", session.to_host or "(unknown)");
+ session:close("connection-timeout");
+ end);
+end
+
+function listener.onconnect(conn)
+ conn:setoption("keepalive", opt_keepalives);
+ local session = sessions[conn];
+ if not session then -- New incoming connection
+ session = s2s_new_incoming(conn);
+ sessions[conn] = session;
+ session.log("debug", "Incoming s2s connection");
+
+ local filter = initialize_filters(session);
+ local w = conn.write;
+ session.sends2s = function (t)
+ log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)"));
+ if t.name then
+ t = filter("stanzas/out", t);
+ end
+ if t then
+ t = filter("bytes/out", tostring(t));
+ if t then
+ return w(conn, t);
+ end
+ end
+ end
+
+ initialize_session(session);
+ else -- Outgoing session connected
+ session:open_stream(session.from_host, session.to_host);
+ end
+end
+
+function listener.onincoming(conn, data)
+ local session = sessions[conn];
+ if session then
+ session.data(data);
+ end
+end
+
+function listener.onstatus(conn, status)
+ if status == "ssl-handshake-complete" then
+ local session = sessions[conn];
+ if session and session.direction == "outgoing" then
+ session.log("debug", "Sending stream header...");
+ session:open_stream(session.from_host, session.to_host);
+ end
+ end
+end
+
+function listener.ondisconnect(conn, err)
+ local session = sessions[conn];
+ if session then
+ sessions[conn] = nil;
+ if err and session.direction == "outgoing" and session.notopen then
+ (session.log or log)("debug", "s2s connection attempt failed: %s", err);
+ if s2sout.attempt_connection(session, err) then
+ (session.log or log)("debug", "...so we're going to try another target");
+ return; -- Session lives for now
+ end
+ end
+ (session.log or log)("debug", "s2s disconnected: %s->%s (%s)", tostring(session.from_host), tostring(session.to_host), tostring(err or "connection closed"));
+ s2s_destroy_session(session, err);
+ end
+end
+
+function listener.register_outgoing(conn, session)
+ session.direction = "outgoing";
+ sessions[conn] = session;
+ initialize_session(session);
+end
+
+function check_auth_policy(event)
+ local host, session = event.host, event.session;
+ local must_secure = secure_auth;
+
+ if not must_secure and secure_domains[host] then
+ must_secure = true;
+ elseif must_secure and insecure_domains[host] then
+ must_secure = false;
+ end
+
+ if must_secure and not session.cert_identity_status then
+ module:log("warn", "Forbidding insecure connection to/from %s", host);
+ if session.direction == "incoming" then
+ session:close({ condition = "not-authorized", text = "Your server's certificate is invalid, expired, or not trusted by "..session.to_host });
+ else -- Close outgoing connections without warning
+ session:close(false);
+ end
+ return false;
+ end
+end
+
+module:hook("s2s-check-certificate", check_auth_policy, -1);
+
+s2sout.set_listener(listener);
+
+module:hook("server-stopping", function(event)
+ local reason = event.reason;
+ for _, session in pairs(sessions) do
+ session:close{ condition = "system-shutdown", text = reason };
+ end
+end,500);
+
+
+
+module:provides("net", {
+ name = "s2s";
+ listener = listener;
+ default_port = 5269;
+ encryption = "starttls";
+ multiplex = {
+ pattern = "^<.*:stream.*%sxmlns%s*=%s*(['\"])jabber:server%1.*>";
+ };
+});
+
diff --git a/plugins/mod_s2s/s2sout.lib.lua b/plugins/mod_s2s/s2sout.lib.lua
new file mode 100644
index 00000000..cb2f8be4
--- /dev/null
+++ b/plugins/mod_s2s/s2sout.lib.lua
@@ -0,0 +1,361 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+--- Module containing all the logic for connecting to a remote server
+
+local portmanager = require "core.portmanager";
+local wrapclient = require "net.server".wrapclient;
+local initialize_filters = require "util.filters".initialize;
+local idna_to_ascii = require "util.encodings".idna.to_ascii;
+local new_ip = require "util.ip".new_ip;
+local rfc6724_dest = require "util.rfc6724".destination;
+local socket = require "socket";
+local adns = require "net.adns";
+local dns = require "net.dns";
+local t_insert, t_sort, ipairs = table.insert, table.sort, ipairs;
+local st = require "util.stanza";
+
+local s2s_destroy_session = require "core.s2smanager".destroy_session;
+
+local log = module._log;
+
+local sources = {};
+local has_ipv4, has_ipv6;
+
+local dns_timeout = module:get_option_number("dns_timeout", 15);
+dns.settimeout(dns_timeout);
+local max_dns_depth = module:get_option_number("dns_max_depth", 3);
+
+local s2sout = {};
+
+local s2s_listener;
+
+
+function s2sout.set_listener(listener)
+ s2s_listener = listener;
+end
+
+local function compare_srv_priorities(a,b)
+ return a.priority < b.priority or (a.priority == b.priority and a.weight > b.weight);
+end
+
+function s2sout.initiate_connection(host_session)
+ initialize_filters(host_session);
+ host_session.version = 1;
+ host_session.open_stream = session_open_stream;
+
+ -- Kick the connection attempting machine into life
+ if not s2sout.attempt_connection(host_session) then
+ -- Intentionally not returning here, the
+ -- session is needed, connected or not
+ s2s_destroy_session(host_session);
+ end
+
+ if not host_session.sends2s then
+ -- A sends2s which buffers data (until the stream is opened)
+ -- note that data in this buffer will be sent before the stream is authed
+ -- and will not be ack'd in any way, successful or otherwise
+ local buffer;
+ function host_session.sends2s(data)
+ if not buffer then
+ buffer = {};
+ host_session.send_buffer = buffer;
+ end
+ log("debug", "Buffering data on unconnected s2sout to %s", tostring(host_session.to_host));
+ buffer[#buffer+1] = data;
+ log("debug", "Buffered item %d: %s", #buffer, tostring(data));
+ end
+ end
+end
+
+function s2sout.attempt_connection(host_session, err)
+ local from_host, to_host = host_session.from_host, host_session.to_host;
+ local connect_host, connect_port = to_host and idna_to_ascii(to_host), 5269;
+
+ if not connect_host then
+ return false;
+ end
+
+ if not err then -- This is our first attempt
+ log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host);
+ host_session.connecting = true;
+ local handle;
+ handle = adns.lookup(function (answer)
+ handle = nil;
+ host_session.connecting = nil;
+ if answer and #answer > 0 then
+ log("debug", "%s has SRV records, handling...", to_host);
+ local srv_hosts = { answer = answer };
+ host_session.srv_hosts = srv_hosts;
+ for _, record in ipairs(answer) do
+ t_insert(srv_hosts, record.srv);
+ end
+ if #srv_hosts == 1 and srv_hosts[1].target == "." then
+ log("debug", "%s does not provide a XMPP service", to_host);
+ s2s_destroy_session(host_session, err); -- Nothing to see here
+ return;
+ end
+ t_sort(srv_hosts, compare_srv_priorities);
+
+ local srv_choice = srv_hosts[1];
+ host_session.srv_choice = 1;
+ if srv_choice then
+ connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
+ log("debug", "Best record found, will connect to %s:%d", connect_host, connect_port);
+ end
+ else
+ log("debug", "%s has no SRV records, falling back to A/AAAA", to_host);
+ end
+ -- Try with SRV, or just the plain hostname if no SRV
+ local ok, err = s2sout.try_connect(host_session, connect_host, connect_port);
+ if not ok then
+ if not s2sout.attempt_connection(host_session, err) then
+ -- No more attempts will be made
+ s2s_destroy_session(host_session, err);
+ end
+ end
+ end, "_xmpp-server._tcp."..connect_host..".", "SRV");
+
+ return true; -- Attempt in progress
+ elseif host_session.ip_hosts then
+ return s2sout.try_connect(host_session, connect_host, connect_port, err);
+ elseif host_session.srv_hosts and #host_session.srv_hosts > host_session.srv_choice then -- Not our first attempt, and we also have SRV
+ host_session.srv_choice = host_session.srv_choice + 1;
+ local srv_choice = host_session.srv_hosts[host_session.srv_choice];
+ connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
+ host_session.log("info", "Connection failed (%s). Attempt #%d: This time to %s:%d", tostring(err), host_session.srv_choice, connect_host, connect_port);
+ else
+ host_session.log("info", "Out of connection options, can't connect to %s", tostring(host_session.to_host));
+ -- We're out of options
+ return false;
+ end
+
+ if not (connect_host and connect_port) then
+ -- Likely we couldn't resolve DNS
+ log("warn", "Hmm, we're without a host (%s) and port (%s) to connect to for %s, giving up :(", tostring(connect_host), tostring(connect_port), tostring(to_host));
+ return false;
+ end
+
+ return s2sout.try_connect(host_session, connect_host, connect_port);
+end
+
+function s2sout.try_next_ip(host_session)
+ host_session.connecting = nil;
+ host_session.ip_choice = host_session.ip_choice + 1;
+ local ip = host_session.ip_hosts[host_session.ip_choice];
+ local ok, err= s2sout.make_connect(host_session, ip.ip, ip.port);
+ if not ok then
+ if not s2sout.attempt_connection(host_session, err or "closed") then
+ err = err and (": "..err) or "";
+ s2s_destroy_session(host_session, "Connection failed"..err);
+ end
+ end
+end
+
+function s2sout.try_connect(host_session, connect_host, connect_port, err)
+ host_session.connecting = true;
+
+ if not err then
+ local IPs = {};
+ host_session.ip_hosts = IPs;
+ local handle4, handle6;
+ local have_other_result = not(has_ipv4) or not(has_ipv6) or false;
+
+ if has_ipv4 then
+ handle4 = adns.lookup(function (reply, err)
+ handle4 = nil;
+
+ -- COMPAT: This is a compromise for all you CNAME-(ab)users :)
+ if not (reply and reply[#reply] and reply[#reply].a) then
+ local count = max_dns_depth;
+ reply = dns.peek(connect_host, "CNAME", "IN");
+ while count > 0 and reply and reply[#reply] and not reply[#reply].a and reply[#reply].cname do
+ log("debug", "Looking up %s (DNS depth is %d)", tostring(reply[#reply].cname), count);
+ reply = dns.peek(reply[#reply].cname, "A", "IN") or dns.peek(reply[#reply].cname, "CNAME", "IN");
+ count = count - 1;
+ end
+ end
+ -- end of CNAME resolving
+
+ if reply and reply[#reply] and reply[#reply].a then
+ for _, ip in ipairs(reply) do
+ log("debug", "DNS reply for %s gives us %s", connect_host, ip.a);
+ IPs[#IPs+1] = new_ip(ip.a, "IPv4");
+ end
+ end
+
+ if have_other_result then
+ if #IPs > 0 then
+ rfc6724_dest(host_session.ip_hosts, sources);
+ for i = 1, #IPs do
+ IPs[i] = {ip = IPs[i], port = connect_port};
+ end
+ host_session.ip_choice = 0;
+ s2sout.try_next_ip(host_session);
+ else
+ log("debug", "DNS lookup failed to get a response for %s", connect_host);
+ host_session.ip_hosts = nil;
+ if not s2sout.attempt_connection(host_session, "name resolution failed") then -- Retry if we can
+ log("debug", "No other records to try for %s - destroying", host_session.to_host);
+ err = err and (": "..err) or "";
+ s2s_destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't
+ end
+ end
+ else
+ have_other_result = true;
+ end
+ end, connect_host, "A", "IN");
+ else
+ have_other_result = true;
+ end
+
+ if has_ipv6 then
+ handle6 = adns.lookup(function (reply, err)
+ handle6 = nil;
+
+ if reply and reply[#reply] and reply[#reply].aaaa then
+ for _, ip in ipairs(reply) do
+ log("debug", "DNS reply for %s gives us %s", connect_host, ip.aaaa);
+ IPs[#IPs+1] = new_ip(ip.aaaa, "IPv6");
+ end
+ end
+
+ if have_other_result then
+ if #IPs > 0 then
+ rfc6724_dest(host_session.ip_hosts, sources);
+ for i = 1, #IPs do
+ IPs[i] = {ip = IPs[i], port = connect_port};
+ end
+ host_session.ip_choice = 0;
+ s2sout.try_next_ip(host_session);
+ else
+ log("debug", "DNS lookup failed to get a response for %s", connect_host);
+ host_session.ip_hosts = nil;
+ if not s2sout.attempt_connection(host_session, "name resolution failed") then -- Retry if we can
+ log("debug", "No other records to try for %s - destroying", host_session.to_host);
+ err = err and (": "..err) or "";
+ s2s_destroy_session(host_session, "DNS resolution failed"..err); -- End of the line, we can't
+ end
+ end
+ else
+ have_other_result = true;
+ end
+ end, connect_host, "AAAA", "IN");
+ else
+ have_other_result = true;
+ end
+ return true;
+ elseif host_session.ip_hosts and #host_session.ip_hosts > host_session.ip_choice then -- Not our first attempt, and we also have IPs left to try
+ s2sout.try_next_ip(host_session);
+ else
+ host_session.ip_hosts = nil;
+ if not s2sout.attempt_connection(host_session, "out of IP addresses") then -- Retry if we can
+ log("debug", "No other records to try for %s - destroying", host_session.to_host);
+ err = err and (": "..err) or "";
+ s2s_destroy_session(host_session, "Connecting failed"..err); -- End of the line, we can't
+ return false;
+ end
+ end
+
+ return true;
+end
+
+function s2sout.make_connect(host_session, connect_host, connect_port)
+ (host_session.log or log)("info", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port);
+ -- Ok, we're going to try to connect
+
+ local from_host, to_host = host_session.from_host, host_session.to_host;
+
+ -- Reset secure flag in case this is another
+ -- connection attempt after a failed STARTTLS
+ host_session.secure = nil;
+
+ local conn, handler;
+ if connect_host.proto == "IPv4" then
+ conn, handler = socket.tcp();
+ else
+ conn, handler = socket.tcp6();
+ end
+
+ if not conn then
+ log("warn", "Failed to create outgoing connection, system error: %s", handler);
+ return false, handler;
+ end
+
+ conn:settimeout(0);
+ local success, err = conn:connect(connect_host.addr, connect_port);
+ if not success and err ~= "timeout" then
+ log("warn", "s2s connect() to %s (%s:%d) failed: %s", host_session.to_host, connect_host.addr, connect_port, err);
+ return false, err;
+ end
+
+ conn = wrapclient(conn, connect_host.addr, connect_port, s2s_listener, "*a");
+ host_session.conn = conn;
+
+ local filter = initialize_filters(host_session);
+ local w, log = conn.write, host_session.log;
+ host_session.sends2s = function (t)
+ log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
+ if t.name then
+ t = filter("stanzas/out", t);
+ end
+ if t then
+ t = filter("bytes/out", tostring(t));
+ if t then
+ return w(conn, tostring(t));
+ end
+ end
+ end
+
+ -- Register this outgoing connection so that xmppserver_listener knows about it
+ -- otherwise it will assume it is a new incoming connection
+ s2s_listener.register_outgoing(conn, host_session);
+
+ log("debug", "Connection attempt in progress...");
+ return true;
+end
+
+module:hook_global("service-added", function (event)
+ if event.name ~= "s2s" then return end
+
+ local s2s_sources = portmanager.get_active_services():get("s2s");
+ if not s2s_sources then
+ module:log("warn", "s2s not listening on any ports, outgoing connections may fail");
+ return;
+ end
+ for source, _ in pairs(s2s_sources) do
+ if source == "*" or source == "0.0.0.0" then
+ if not socket.local_addresses then
+ sources[#sources + 1] = new_ip("0.0.0.0", "IPv4");
+ else
+ for _, addr in ipairs(socket.local_addresses("ipv4", true)) do
+ sources[#sources + 1] = new_ip(addr, "IPv4");
+ end
+ end
+ elseif source == "::" then
+ if not socket.local_addresses then
+ sources[#sources + 1] = new_ip("::", "IPv6");
+ else
+ for _, addr in ipairs(socket.local_addresses("ipv6", true)) do
+ sources[#sources + 1] = new_ip(addr, "IPv6");
+ end
+ end
+ else
+ sources[#sources + 1] = new_ip(source, (source:find(":") and "IPv6") or "IPv4");
+ end
+ end
+ for i = 1,#sources do
+ if sources[i].proto == "IPv6" then
+ has_ipv6 = true;
+ elseif sources[i].proto == "IPv4" then
+ has_ipv4 = true;
+ end
+ end
+end);
+
+return s2sout;
diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua
index 8ef1e09d..201cc477 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -1,113 +1,299 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
local st = require "util.stanza";
-local send = require "core.sessionmanager".send_to_session;
local sm_bind_resource = require "core.sessionmanager".bind_resource;
+local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
+local base64 = require "util.encodings".base64;
+
+local cert_verify_identity = require "util.x509".verify_identity;
-local usermanager_validate_credentials = require "core.usermanager".validate_credentials;
-local t_concat, t_insert = table.concat, table.insert;
+local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler;
local tostring = tostring;
-local log = require "util.logger".init("mod_saslauth");
+local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
+local allow_unencrypted_plain_auth = module:get_option("allow_unencrypted_plain_auth")
+
+local log = module._log;
local xmlns_sasl ='urn:ietf:params:xml:ns:xmpp-sasl';
local xmlns_bind ='urn:ietf:params:xml:ns:xmpp-bind';
-local xmlns_stanzas ='urn:ietf:params:xml:ns:xmpp-stanzas';
-
-local new_connhandler = require "net.connhandlers".new;
-local new_sasl = require "util.sasl".new;
-
-add_handler("c2s_unauthed", "auth", xmlns_sasl,
- function (session, stanza)
- if not session.sasl_handler then
- session.sasl_handler = new_sasl(stanza.attr.mechanism,
- function (username, password)
- -- onAuth
- require "core.usermanager"
- if usermanager_validate_credentials(session.host, username, password) then
- return true;
- end
- return false;
- end,
- function (username)
- -- onSuccess
- local success, err = sessionmanager.make_authenticated(session, username);
- if not success then
- sessionmanager.destroy_session(session);
- return;
- end
- session.sasl_handler = nil;
- session.connhandler = new_connhandler("xmpp-client", session);
- session.notopen = true;
- end,
- function (reason)
- -- onFail
- log("debug", "SASL failure, reason: %s", reason);
- end,
- function (stanza)
- -- onWrite
- log("debug", "SASL writes: %s", tostring(stanza));
- send(session, stanza);
- end
- );
- session.sasl_handler:feed(stanza);
- else
- error("Client tried to negotiate SASL again", 0);
- end
-
- end);
-
-add_event_hook("stream-features",
- function (session, features)
- if not session.username then
- t_insert(features, "<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>");
- t_insert(features, "<mechanism>PLAIN</mechanism>");
- t_insert(features, "</mechanisms>");
- else
- t_insert(features, "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><required/></bind>");
- t_insert(features, "<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>");
- end
- --send [[<register xmlns="http://jabber.org/features/iq-register"/> ]]
- end);
-
-add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind",
- function (session, stanza)
- log("debug", "Client tried to bind to a resource");
- 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
+
+local function build_reply(status, ret, err_msg)
+ local reply = st.stanza(status, {xmlns = xmlns_sasl});
+ if status == "challenge" then
+ --log("debug", "CHALLENGE: %s", ret or "");
+ reply:text(base64.encode(ret or ""));
+ elseif status == "failure" then
+ reply:tag(ret):up();
+ if err_msg then reply:tag("text"):text(err_msg); end
+ elseif status == "success" then
+ --log("debug", "SUCCESS: %s", ret or "");
+ reply:text(base64.encode(ret or ""));
+ else
+ module:log("error", "Unknown sasl status: %s", status);
+ end
+ return reply;
+end
+
+local function handle_status(session, status, ret, err_msg)
+ if status == "failure" then
+ module:fire_event("authentication-failure", { session = session, condition = ret, text = err_msg });
+ session.sasl_handler = session.sasl_handler:clean_clone();
+ elseif status == "success" then
+ local ok, err = sm_make_authenticated(session, session.sasl_handler.username);
+ if ok then
+ module:fire_event("authentication-success", { session = session });
+ session.sasl_handler = nil;
+ session:reset_stream();
+ else
+ module:log("warn", "SASL succeeded but username was invalid");
+ module:fire_event("authentication-failure", { session = session, condition = "not-authorized", text = err });
+ session.sasl_handler = session.sasl_handler:clean_clone();
+ return "failure", "not-authorized", "User authenticated successfully, but username was invalid";
+ end
+ end
+ return status, ret, err_msg;
+end
+
+local function sasl_process_cdata(session, stanza)
+ local text = stanza[1];
+ if text then
+ text = base64.decode(text);
+ --log("debug", "AUTH: %s", text:gsub("[%z\001-\008\011\012\014-\031]", " "));
+ if not text then
+ session.sasl_handler = nil;
+ session.send(build_reply("failure", "incorrect-encoding"));
+ return true;
+ end
+ end
+ local status, ret, err_msg = session.sasl_handler:process(text);
+ status, ret, err_msg = handle_status(session, status, ret, err_msg);
+ local s = build_reply(status, ret, err_msg);
+ log("debug", "sasl reply: %s", tostring(s));
+ session.send(s);
+ return true;
+end
+
+module:hook_stanza(xmlns_sasl, "success", function (session, stanza)
+ if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
+ module:log("debug", "SASL EXTERNAL with %s succeeded", session.to_host);
+ session.external_auth = "succeeded"
+ session:reset_stream();
+ session:open_stream(session.from_host, session.to_host);
+
+ module:fire_event("s2s-authenticated", { session = session, host = session.to_host });
+ return true;
+end)
+
+module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
+ if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
+
+ module:log("info", "SASL EXTERNAL with %s failed", session.to_host)
+ -- TODO: Log the failure reason
+ session.external_auth = "failed"
+end, 500)
+
+module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
+ -- TODO: Dialback wasn't loaded. Do something useful.
+end, 90)
+
+module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
+ if session.type ~= "s2sout_unauthed" or not session.secure then return; end
+
+ local mechanisms = stanza:get_child("mechanisms", xmlns_sasl)
+ if mechanisms then
+ for mech in mechanisms:childtags() do
+ if mech[1] == "EXTERNAL" then
+ module:log("debug", "Initiating SASL EXTERNAL with %s", session.to_host);
+ local reply = st.stanza("auth", {xmlns = xmlns_sasl, mechanism = "EXTERNAL"});
+ reply:text(base64.encode(session.from_host))
+ session.sends2s(reply)
+ session.external_auth = "attempting"
+ return true
end
- local success, err = sm_bind_resource(session, resource);
- if not success then
- local reply = st.reply(stanza);
- reply.attr.type = "error";
- if err == "conflict" then
- reply:tag("error", { type = "modify" })
- :tag("conflict", { xmlns = xmlns_stanzas });
- elseif err == "constraint" then
- reply:tag("error", { type = "cancel" })
- :tag("resource-constraint", { xmlns = xmlns_stanzas });
- elseif err == "auth" then
- reply:tag("error", { type = "cancel" })
- :tag("not-allowed", { xmlns = xmlns_stanzas });
- end
- send(session, reply);
- else
- local reply = st.reply(stanza);
- reply:tag("bind", { xmlns = xmlns_bind})
- :tag("jid"):text(session.full_jid);
- send(session, reply);
+ end
+ end
+end, 150);
+
+local function s2s_external_auth(session, stanza)
+ local mechanism = stanza.attr.mechanism;
+
+ if not session.secure then
+ if mechanism == "EXTERNAL" then
+ session.sends2s(build_reply("failure", "encryption-required"))
+ else
+ session.sends2s(build_reply("failure", "invalid-mechanism"))
+ end
+ return true;
+ end
+
+ if mechanism ~= "EXTERNAL" or session.cert_chain_status ~= "valid" then
+ session.sends2s(build_reply("failure", "invalid-mechanism"))
+ return true;
+ end
+
+ local text = stanza[1]
+ if not text then
+ session.sends2s(build_reply("failure", "malformed-request"))
+ return true
+ end
+
+ -- Either the value is "=" and we've already verified the external
+ -- cert identity, or the value is a string and either matches the
+ -- from_host (
+
+ text = base64.decode(text)
+ if not text then
+ session.sends2s(build_reply("failure", "incorrect-encoding"))
+ return true;
+ end
+
+ if session.cert_identity_status == "valid" then
+ if text ~= "" and text ~= session.from_host then
+ session.sends2s(build_reply("failure", "invalid-authzid"))
+ return true
+ end
+ else
+ if text == "" then
+ session.sends2s(build_reply("failure", "invalid-authzid"))
+ return true
+ end
+
+ local cert = session.conn:socket():getpeercertificate()
+ if (cert_verify_identity(text, "xmpp-server", cert)) then
+ session.cert_identity_status = "valid"
+ else
+ session.cert_identity_status = "invalid"
+ session.sends2s(build_reply("failure", "invalid-authzid"))
+ return true
+ end
+ end
+
+ session.external_auth = "succeeded"
+
+ if not session.from_host then
+ session.from_host = text;
+ end
+ session.sends2s(build_reply("success"))
+
+ local domain = text ~= "" and text or session.from_host;
+ module:log("info", "Accepting SASL EXTERNAL identity from %s", domain);
+ module:fire_event("s2s-authenticated", { session = session, host = domain });
+ session:reset_stream();
+ return true
+end
+
+module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
+ local session, stanza = event.origin, event.stanza;
+ if session.type == "s2sin_unauthed" then
+ return s2s_external_auth(session, stanza)
+ end
+
+ 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, session);
+ end
+ local mechanism = stanza.attr.mechanism;
+ if not session.secure and (secure_auth_only or (mechanism == "PLAIN" and not allow_unencrypted_plain_auth)) then
+ 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' };
+local xmpp_session_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-session' };
+module:hook("stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if not origin.username then
+ if secure_auth_only and not origin.secure then
+ return;
+ end
+ origin.sasl_handler = usermanager_get_sasl_handler(module.host, origin);
+ local mechanisms = st.stanza("mechanisms", mechanisms_attr);
+ for mechanism in pairs(origin.sasl_handler:mechanisms()) do
+ if mechanism ~= "PLAIN" or origin.secure or allow_unencrypted_plain_auth then
+ mechanisms:tag("mechanism"):text(mechanism):up();
end
- end);
-
-add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-session",
- function (session, stanza)
- log("debug", "Client tried to bind to a resource");
- send(session, st.reply(stanza));
- end);
+ end
+ if mechanisms[1] then features:add_child(mechanisms); end
+ else
+ features:tag("bind", bind_attr):tag("required"):up():up();
+ features:tag("session", xmpp_session_attr):tag("optional"):up():up();
+ end
+end);
+
+module:hook("s2s-stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if origin.secure and origin.type == "s2sin_unauthed" then
+ -- Offer EXTERNAL if chain is valid and either we didn't validate
+ -- the identity or it passed.
+ if origin.cert_chain_status == "valid" and origin.cert_identity_status ~= "invalid" then --TODO: Configurable
+ module:log("debug", "Offering SASL EXTERNAL")
+ features:tag("mechanisms", { xmlns = xmlns_sasl })
+ :tag("mechanism"):text("EXTERNAL")
+ :up():up();
+ end
+ end
+end);
+
+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];
+ 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(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
+ 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);
+
+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..972ecbee
--- /dev/null
+++ b/plugins/mod_storage_internal.lua
@@ -0,0 +1,31 @@
+local datamanager = require "core.storagemanager".olddm;
+
+local host = module.host;
+
+local driver = {};
+local driver_mt = { __index = driver };
+
+function driver:open(store, typ)
+ return setmetatable({ store = store, type = typ }, driver_mt);
+end
+function driver:get(user)
+ return datamanager.load(user, host, self.store);
+end
+
+function driver:set(user, data)
+ return datamanager.store(user, host, self.store, data);
+end
+
+function driver:stores(username)
+ return datamanager.stores(username, host);
+end
+
+function driver:users()
+ return datamanager.users(host, self.store, self.type);
+end
+
+function driver:purge(user)
+ return datamanager.purge(user, host);
+end
+
+module:provides("storage", driver);
diff --git a/plugins/mod_storage_none.lua b/plugins/mod_storage_none.lua
new file mode 100644
index 00000000..8f2d2f56
--- /dev/null
+++ b/plugins/mod_storage_none.lua
@@ -0,0 +1,23 @@
+local driver = {};
+local driver_mt = { __index = driver };
+
+function driver:open(store)
+ return setmetatable({ store = store }, driver_mt);
+end
+function driver:get(user)
+ return {};
+end
+
+function driver:set(user, data)
+ return nil, "Storage disabled";
+end
+
+function driver:stores(username)
+ return { "roster" };
+end
+
+function driver:purge(user)
+ return true;
+end
+
+module:provides("storage", driver);
diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua
new file mode 100644
index 00000000..eed3fec9
--- /dev/null
+++ b/plugins/mod_storage_sql.lua
@@ -0,0 +1,414 @@
+
+--[[
+
+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 build_url = require"socket.url".build;
+
+local DBI;
+local connection;
+local host,user,store = module.host;
+local params = module:get_option("sql");
+
+local dburi;
+local connections = module:shared "/*/sql/connection-cache";
+
+local function db2uri(params)
+ return build_url{
+ scheme = params.driver,
+ user = params.username,
+ password = params.password,
+ host = params.host,
+ port = params.port,
+ path = params.database,
+ };
+end
+
+
+local resolve_relative_path = require "core.configmanager".resolve_relative_path;
+
+local function test_connection()
+ if not connection then return nil; end
+ if connection:ping() then
+ return true;
+ else
+ module:log("debug", "Database connection closed");
+ connection = nil;
+ connections[dburi] = nil;
+ end
+end
+local function connect()
+ if not test_connection() then
+ prosody.unlock_globals();
+ local dbh, err = DBI.Connect(
+ params.driver, params.database,
+ params.username, params.password,
+ params.host, params.port
+ );
+ prosody.lock_globals();
+ if not dbh then
+ module:log("debug", "Database connection failed: %s", tostring(err));
+ return nil, err;
+ end
+ module:log("debug", "Successfully connected to database");
+ dbh:autocommit(false); -- don't commit automatically
+ connection = dbh;
+
+ connections[dburi] = dbh;
+ end
+ return connection;
+end
+
+local function create_table()
+ if not module:get_option("sql_manage_tables", true) then
+ return;
+ end
+ local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);";
+ if params.driver == "PostgreSQL" then
+ create_sql = create_sql:gsub("`", "\"");
+ elseif params.driver == "MySQL" then
+ create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT");
+ end
+
+ local stmt, err = connection:prepare(create_sql);
+ if stmt then
+ local ok = stmt:execute();
+ local commit_ok = connection:commit();
+ if ok and commit_ok then
+ module:log("info", "Initialized new %s database with prosody table", params.driver);
+ local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)";
+ if params.driver == "PostgreSQL" then
+ index_sql = index_sql:gsub("`", "\"");
+ elseif params.driver == "MySQL" then
+ index_sql = index_sql:gsub("`([,)])", "`(20)%1");
+ end
+ local stmt, err = connection:prepare(index_sql);
+ local ok, commit_ok, commit_err;
+ if stmt then
+ ok, err = stmt:execute();
+ commit_ok, commit_err = connection:commit();
+ end
+ if not(ok and commit_ok) then
+ module:log("warn", "Failed to create index (%s), lookups may not be optimised", err or commit_err);
+ end
+ elseif params.driver == "MySQL" then -- COMPAT: Upgrade tables from 0.8.0
+ -- Failed to create, but check existing MySQL table here
+ local stmt = connection:prepare("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'");
+ local ok = stmt:execute();
+ local commit_ok = connection:commit();
+ if ok and commit_ok then
+ if stmt:rowcount() > 0 then
+ module:log("info", "Upgrading database schema...");
+ local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT");
+ local ok, err = stmt:execute();
+ local commit_ok = connection:commit();
+ if ok and commit_ok then
+ module:log("info", "Database table automatically upgraded");
+ else
+ module:log("error", "Failed to upgrade database schema (%s), please see "
+ .."http://prosody.im/doc/mysql for help",
+ err or "unknown error");
+ end
+ end
+ repeat until not stmt:fetch();
+ end
+ end
+ elseif params.driver ~= "SQLite3" then -- SQLite normally fails to prepare for existing table
+ module:log("warn", "Prosody was not able to automatically check/create the database table (%s), "
+ .."see http://prosody.im/doc/modules/mod_storage_sql#table_management for help.",
+ err or "unknown error");
+ end
+end
+
+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");
+
+ dburi = db2uri(params);
+ connection = connections[dburi];
+
+ 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 dosql(sql, ...)
+ if params.driver == "PostgreSQL" then
+ sql = sql:gsub("`", "\"");
+ end
+ -- do prepared statement stuff
+ local stmt, err = connection:prepare(sql);
+ if not stmt and not test_connection() then error("connection failed"); end
+ if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
+ -- run query
+ local ok, err = stmt:execute(...);
+ if not ok and not test_connection() then error("connection failed"); end
+ if not ok then return nil, err; end
+
+ return stmt;
+end
+local function getsql(sql, ...)
+ return dosql(sql, host or "", user or "", store or "", ...);
+end
+local function setsql(sql, ...)
+ local stmt, err = getsql(sql, ...);
+ if not stmt then return stmt, err; end
+ 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(...)
+ local success,err = connection:commit();
+ if not success then return nil, "SQL commit failed: "..tostring(err); 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
+function keyval_store:users()
+ local stmt, err = dosql("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store);
+ if not stmt then
+ return rollback(nil, err);
+ end
+ local next = stmt:rows();
+ return commit(function()
+ local row = next();
+ return row and row[1];
+ end);
+end
+
+local function map_store_get(key)
+ local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
+ if not stmt then return rollback(nil, err); end
+
+ local haveany;
+ local result = {};
+ for row in stmt:rows(true) do
+ 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 = {};
+
+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
+
+function driver:stores(username)
+ local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" ..
+ (username == true and "!=?" or "=?");
+ if username == true or not username then
+ username = "";
+ end
+ local stmt, err = dosql(sql, host, username);
+ if not stmt then
+ return rollback(nil, err);
+ end
+ local next = stmt:rows();
+ return commit(function()
+ local row = next();
+ return row and row[1];
+ end);
+end
+
+function driver:purge(username)
+ local stmt, err = dosql("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username);
+ if not stmt then return rollback(stmt, err); end
+ local changed, err = stmt:affected();
+ if not changed then return rollback(changed, err); end
+ return commit(true, changed);
+end
+
+module:provides("storage", driver);
diff --git a/plugins/mod_storage_sql2.lua b/plugins/mod_storage_sql2.lua
new file mode 100644
index 00000000..7d705b0b
--- /dev/null
+++ b/plugins/mod_storage_sql2.lua
@@ -0,0 +1,237 @@
+
+local json = require "util.json";
+local resolve_relative_path = require "core.configmanager".resolve_relative_path;
+
+local mod_sql = module:require("sql");
+local params = module:get_option("sql");
+
+local engine; -- TODO create engine
+
+local function create_table()
+ --[[local Table,Column,Index = mod_sql.Table,mod_sql.Column,mod_sql.Index;
+ local ProsodyTable = Table {
+ name="prosody";
+ Column { name="host", type="TEXT", nullable=false };
+ Column { name="user", type="TEXT", nullable=false };
+ Column { name="store", type="TEXT", nullable=false };
+ Column { name="key", type="TEXT", nullable=false };
+ Column { name="type", type="TEXT", nullable=false };
+ Column { name="value", type="TEXT", nullable=false };
+ Index { name="prosody_index", "host", "user", "store", "key" };
+ };
+ engine:transaction(function()
+ ProsodyTable:create(engine);
+ end);]]
+ if not module:get_option("sql_manage_tables", true) then
+ return;
+ end
+
+ local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);";
+ if params.driver == "PostgreSQL" then
+ create_sql = create_sql:gsub("`", "\"");
+ elseif params.driver == "MySQL" then
+ create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT")
+ :gsub(";$", " CHARACTER SET 'utf8' COLLATE 'utf8_bin';");
+ end
+
+ local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)";
+ if params.driver == "PostgreSQL" then
+ index_sql = index_sql:gsub("`", "\"");
+ elseif params.driver == "MySQL" then
+ index_sql = index_sql:gsub("`([,)])", "`(20)%1");
+ end
+
+ local success,err = engine:transaction(function()
+ engine:execute(create_sql);
+ engine:execute(index_sql);
+ end);
+ if not success then -- so we failed to create
+ if params.driver == "MySQL" then
+ success,err = engine:transaction(function()
+ local result = engine:execute("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'");
+ if result:rowcount() > 0 then
+ module:log("info", "Upgrading database schema...");
+ engine:execute("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT");
+ module:log("info", "Database table automatically upgraded");
+ end
+ return true;
+ end);
+ if not success then
+ module:log("error", "Failed to check/upgrade database schema (%s), please see "
+ .."http://prosody.im/doc/mysql for help",
+ err or "unknown error");
+ end
+ end
+ end
+end
+local function set_encoding()
+ if params.driver ~= "SQLite3" then
+ local set_names_query = "SET NAMES 'utf8';";
+ if params.driver == "MySQL" then
+ set_names_query = set_names_query:gsub(";$", " COLLATE 'utf8_bin';");
+ end
+ local success,err = engine:transaction(function() return engine:execute(set_names_query); end);
+ if not success then
+ module:log("error", "Failed to set database connection encoding to UTF8: %s", err);
+ return;
+ end
+ if params.driver == "MySQL" then
+ -- COMPAT w/pre-0.9: Upgrade tables to UTF-8 if not already
+ local check_encoding_query = "SELECT `COLUMN_NAME`,`COLUMN_TYPE` FROM `information_schema`.`columns` WHERE `TABLE_NAME`='prosody' AND ( `CHARACTER_SET_NAME`!='utf8' OR `COLLATION_NAME`!='utf8_bin' );";
+ local success,err = engine:transaction(function()
+ local result = engine:execute(check_encoding_query);
+ local n_bad_columns = result:rowcount();
+ if n_bad_columns > 0 then
+ module:log("warn", "Found %d columns in prosody table requiring encoding change, updating now...", n_bad_columns);
+ local fix_column_query1 = "ALTER TABLE `prosody` CHANGE `%s` `%s` BLOB;";
+ local fix_column_query2 = "ALTER TABLE `prosody` CHANGE `%s` `%s` %s CHARACTER SET 'utf8' COLLATE 'utf8_bin';";
+ for row in success:rows() do
+ local column_name, column_type = unpack(row);
+ engine:execute(fix_column_query1:format(column_name, column_name));
+ engine:execute(fix_column_query2:format(column_name, column_name, column_type));
+ end
+ module:log("info", "Database encoding upgrade complete!");
+ end
+ end);
+ local success,err = engine:transaction(function() return engine:execute(check_encoding_query); end);
+ if not success then
+ module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error");
+ end
+ end
+ end
+end
+
+do -- process options to get a db connection
+ params = params or { driver = "SQLite3" };
+
+ if params.driver == "SQLite3" then
+ params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
+ end
+
+ assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");
+
+ --local dburi = db2uri(params);
+ engine = mod_sql:create_engine(params);
+
+ -- Encoding mess
+ set_encoding();
+
+ -- Automatically create table, ignore failure (table probably already exists)
+ create_table();
+end
+
+local function serialize(value)
+ local t = type(value);
+ if t == "string" or t == "boolean" or t == "number" then
+ return t, tostring(value);
+ elseif 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 host = module.host;
+local user, store;
+
+local function keyval_store_get()
+ local haveany;
+ local result = {};
+ for row in engine:select("SELECT `key`,`type`,`value` FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user, store) do
+ haveany = true;
+ local k = row[1];
+ local v = deserialize(row[2], row[3]);
+ if k and v then
+ if k ~= "" then result[k] = v; elseif type(v) == "table" then
+ for a,b in pairs(v) do
+ result[a] = b;
+ end
+ end
+ end
+ end
+ if haveany then
+ return result;
+ end
+end
+local function keyval_store_set(data)
+ engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user, store);
+
+ if data and next(data) ~= nil then
+ local extradata = {};
+ for key, value in pairs(data) do
+ if type(key) == "string" and key ~= "" then
+ local t, value = serialize(value);
+ assert(t, value);
+ engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user, store, key, t, value);
+ else
+ extradata[key] = value;
+ end
+ end
+ if next(extradata) ~= nil then
+ local t, extradata = serialize(extradata);
+ assert(t, extradata);
+ engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user, store, "", t, extradata);
+ end
+ end
+ return true;
+end
+
+local keyval_store = {};
+keyval_store.__index = keyval_store;
+function keyval_store:get(username)
+ user,store = username,self.store;
+ return select(2, engine:transaction(keyval_store_get));
+end
+function keyval_store:set(username, data)
+ user,store = username,self.store;
+ return engine:transaction(function()
+ return keyval_store_set(data);
+ end);
+end
+function keyval_store:users()
+ return engine:transaction(function()
+ return engine:select("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store);
+ end);
+end
+
+local driver = {};
+
+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
+
+function driver:stores(username)
+ local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" ..
+ (username == true and "!=?" or "=?");
+ if username == true or not username then
+ username = "";
+ end
+ return engine:transaction(function()
+ return engine:select(sql, host, username);
+ end);
+end
+
+function driver:purge(username)
+ return engine:transaction(function()
+ local stmt,err = engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username);
+ return true,err;
+ end);
+end
+
+module:provides("storage", driver);
+
+
diff --git a/plugins/mod_time.lua b/plugins/mod_time.lua
new file mode 100644
index 00000000..cb69ebe7
--- /dev/null
+++ b/plugins/mod_time.lua
@@ -0,0 +1,44 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza";
+local datetime = require "util.datetime".datetime;
+local legacy = require "util.datetime".legacy;
+
+-- XEP-0202: Entity Time
+
+module:add_feature("urn:xmpp:time");
+
+local function time_handler(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type == "get" then
+ origin.send(st.reply(stanza):tag("time", {xmlns="urn:xmpp:time"})
+ :tag("tzo"):text("+00:00"):up() -- TODO get the timezone in a platform independent fashion
+ :tag("utc"):text(datetime()));
+ return true;
+ end
+end
+
+module:hook("iq/bare/urn:xmpp:time:time", time_handler);
+module:hook("iq/host/urn:xmpp:time:time", time_handler);
+
+-- XEP-0090: Entity Time (deprecated)
+
+module:add_feature("jabber:iq:time");
+
+local function legacy_time_handler(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type == "get" then
+ origin.send(st.reply(stanza):tag("query", {xmlns="jabber:iq:time"})
+ :tag("utc"):text(legacy()));
+ return true;
+ end
+end
+
+module:hook("iq/bare/jabber:iq:time:query", legacy_time_handler);
+module:hook("iq/host/jabber:iq:time:query", legacy_time_handler);
diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua
new file mode 100644
index 00000000..80b56abb
--- /dev/null
+++ b/plugins/mod_tls.lua
@@ -0,0 +1,107 @@
+-- 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 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");
+local secure_s2s_only = module:get_option("s2s_require_encryption");
+local allow_s2s_tls = module:get_option("s2s_allow_encryption") ~= false;
+
+local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls';
+local starttls_attr = { xmlns = xmlns_starttls };
+local starttls_proceed = st.stanza("proceed", starttls_attr);
+local starttls_failure = st.stanza("failure", starttls_attr);
+local c2s_feature = st.stanza("starttls", starttls_attr);
+local s2s_feature = st.stanza("starttls", starttls_attr);
+if secure_auth_only then c2s_feature:tag("required"):up(); end
+if secure_s2s_only then s2s_feature:tag("required"):up(); end
+
+local global_ssl_ctx = prosody.global_ssl_ctx;
+
+local hosts = prosody.hosts;
+local host = hosts[module.host];
+
+local function can_do_tls(session)
+ if session.type == "c2s_unauthed" then
+ return session.conn.starttls and host.ssl_ctx_in;
+ elseif session.type == "s2sin_unauthed" and allow_s2s_tls then
+ return session.conn.starttls and host.ssl_ctx_in;
+ elseif session.direction == "outgoing" and allow_s2s_tls then
+ return session.conn.starttls and host.ssl_ctx;
+ end
+ return false;
+end
+
+-- Hook <starttls/>
+module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event)
+ local origin = event.origin;
+ if can_do_tls(origin) then
+ (origin.sends2s or origin.send)(starttls_proceed);
+ origin:reset_stream();
+ local host = origin.to_host or origin.host;
+ local ssl_ctx = host and hosts[host].ssl_ctx_in or global_ssl_ctx;
+ origin.conn:starttls(ssl_ctx);
+ origin.log("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);
+ (origin.sends2s or origin.send)(starttls_failure);
+ origin:close();
+ end
+ return true;
+end);
+
+-- Advertize stream feature
+module:hook("stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if can_do_tls(origin) then
+ features:add_child(c2s_feature);
+ end
+end);
+module:hook("s2s-stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if can_do_tls(origin) then
+ features:add_child(s2s_feature);
+ end
+end);
+
+-- For s2sout connections, start TLS if we can
+module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
+ module:log("debug", "Received features element");
+ if can_do_tls(session) and stanza:child_with_ns(xmlns_starttls) then
+ module:log("debug", "%s is offering TLS, taking up the offer...", session.to_host);
+ session.sends2s("<starttls xmlns='"..xmlns_starttls.."'/>");
+ return true;
+ end
+end, 500);
+
+module:hook_stanza(xmlns_starttls, "proceed", function (session, stanza)
+ module:log("debug", "Proceeding with TLS on s2sout...");
+ session:reset_stream();
+ local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx;
+ session.conn:starttls(ssl_ctx);
+ session.secure = false;
+ return true;
+end);
+
+function module.load()
+ local ssl_config = config.rawget(module.host, "ssl");
+ if not ssl_config then
+ local base_host = module.host:match("%.(.*)");
+ ssl_config = config.get(base_host, "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
new file mode 100644
index 00000000..3f275b2f
--- /dev/null
+++ b/plugins/mod_uptime.lua
@@ -0,0 +1,48 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza";
+
+local start_time = prosody.start_time;
+module:hook_global("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)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type == "get" then
+ origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:last", seconds = tostring(os.difftime(os.time(), start_time))}));
+ return true;
+ end
+end);
+
+-- 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_vcard.lua b/plugins/mod_vcard.lua
new file mode 100644
index 00000000..26b30e3a
--- /dev/null
+++ b/plugins/mod_vcard.lua
@@ -0,0 +1,54 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza"
+local jid_split = require "util.jid".split;
+
+local vcards = module:open_store();
+
+module:add_feature("vcard-temp");
+
+local function handle_vcard(event)
+ local session, stanza = event.origin, event.stanza;
+ local to = stanza.attr.to;
+ if stanza.attr.type == "get" then
+ local vCard;
+ if to then
+ local node, host = jid_split(to);
+ vCard = st.deserialize(vcards:get(node)); -- load vCard for user or server
+ else
+ vCard = st.deserialize(vcards:get(session.username));-- load user's own vCard
+ end
+ if vCard then
+ session.send(st.reply(stanza):add_child(vCard)); -- send vCard!
+ else
+ session.send(st.error_reply(stanza, "cancel", "item-not-found"));
+ end
+ else
+ if not to then
+ if vcards:set(session.username, st.preserialize(stanza.tags[1])) then
+ session.send(st.reply(stanza));
+ else
+ -- TODO unable to write file, file may be locked, etc, what's the correct error?
+ session.send(st.error_reply(stanza, "wait", "internal-server-error"));
+ end
+ else
+ session.send(st.error_reply(stanza, "auth", "forbidden"));
+ end
+ end
+ return true;
+end
+
+module:hook("iq/bare/vcard-temp:vCard", handle_vcard);
+module:hook("iq/host/vcard-temp:vCard", handle_vcard);
+
+-- COMPAT w/0.8
+if module:get_option("vcard_compatibility") ~= nil then
+ module:log("error", "The vcard_compatibility option has been removed, see"..
+ "mod_compat_vcard in prosody-modules if you still need this.");
+end
diff --git a/plugins/mod_version.lua b/plugins/mod_version.lua
new file mode 100644
index 00000000..d35103b6
--- /dev/null
+++ b/plugins/mod_version.lua
@@ -0,0 +1,48 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza";
+
+module:add_feature("jabber:iq:version");
+
+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 os_version_command = module:get_option("os_version_command");
+ local ok, pposix = pcall(require, "util.pposix");
+ if not os_version_command and (ok and pposix and pposix.uname) then
+ version = pposix.uname().sysname;
+ end
+ 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
+
+module:hook("iq/host/jabber:iq:version:query", function(event)
+ local stanza = event.stanza;
+ if stanza.attr.type == "get" and stanza.attr.to == module.host then
+ event.origin.send(st.reply(stanza):add_child(query));
+ return true;
+ end
+end);
diff --git a/plugins/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua
new file mode 100644
index 00000000..abca90bd
--- /dev/null
+++ b/plugins/mod_watchregistrations.lua
@@ -0,0 +1,30 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
+local host = module:get_host();
+local jid_prep = require "util.jid".prep;
+
+local registration_watchers = module:get_option_set("registration_watchers", module:get_option("admins", {})) / jid_prep;
+local registration_notification = module:get_option("registration_notification", "User $username just registered on $host from $ip");
+
+local st = require "util.stanza";
+
+module:hook("user-registered", function (user)
+ module:log("debug", "Notifying of new registration");
+ local message = st.message{ type = "chat", from = host }
+ :tag("body")
+ :text(registration_notification:gsub("%$(%w+)", function (v)
+ return user[v] or user.session and user.session[v] or nil;
+ end));
+ for jid in registration_watchers do
+ module:log("debug", "Notifying %s", jid);
+ message.attr.to = jid;
+ module:send(message);
+ end
+end);
diff --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua
new file mode 100644
index 00000000..e498f0b3
--- /dev/null
+++ b/plugins/mod_welcome.lua
@@ -0,0 +1,21 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local host = module:get_host();
+local welcome_text = module:get_option("welcome_message") or "Hello $username, welcome to the $host IM server!";
+
+local st = require "util.stanza";
+
+module:hook("user-registered",
+ function (user)
+ local welcome_stanza =
+ st.message({ to = user.username.."@"..user.host, from = host })
+ :tag("body"):text(welcome_text:gsub("$(%w+)", user));
+ module:send(welcome_stanza);
+ module:log("debug", "Welcomed user %s@%s", user.username, user.host);
+ end);
diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua
new file mode 100644
index 00000000..7861092c
--- /dev/null
+++ b/plugins/muc/mod_muc.lua
@@ -0,0 +1,229 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
+if module:get_host_type() ~= "component" then
+ error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0);
+end
+
+local muc_host = module:get_host();
+local muc_name = module:get_option("name");
+if type(muc_name) ~= "string" then muc_name = "Prosody Chatrooms"; end
+local restrict_room_creation = module:get_option("restrict_room_creation");
+if restrict_room_creation 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 muclib = module:require "muc";
+local muc_new_room = muclib.new_room;
+local jid_split = require "util.jid".split;
+local jid_bare = require "util.jid".bare;
+local st = require "util.stanza";
+local uuid_gen = require "util.uuid".generate;
+local um_is_admin = require "core.usermanager".is_admin;
+local hosts = prosody.hosts;
+
+rooms = {};
+local rooms = rooms;
+local persistent_rooms_storage = module:open_store("persistent");
+local persistent_rooms = persistent_rooms_storage:get() or {};
+local room_configs = module:open_store("config");
+
+-- Configurable options
+muclib.set_max_history_length(module:get_option_number("max_history_messages"));
+
+local function is_admin(jid)
+ return um_is_admin(jid, module.host);
+end
+
+local _set_affiliation = muc_new_room.room_mt.set_affiliation;
+local _get_affiliation = muc_new_room.room_mt.get_affiliation;
+function muclib.room_mt:get_affiliation(jid)
+ if is_admin(jid) then return "owner"; end
+ return _get_affiliation(self, jid);
+end
+function muclib.room_mt:set_affiliation(actor, jid, affiliation, callback, reason)
+ if is_admin(jid) then return nil, "modify", "not-acceptable"; end
+ return _set_affiliation(self, actor, jid, affiliation, callback, reason);
+end
+
+local function room_route_stanza(room, stanza) module:send(stanza); end
+local function room_save(room, forced)
+ local node = jid_split(room.jid);
+ persistent_rooms[room.jid] = room._data.persistent;
+ if room._data.persistent then
+ local history = room._data.history;
+ room._data.history = nil;
+ local data = {
+ jid = room.jid;
+ _data = room._data;
+ _affiliations = room._affiliations;
+ };
+ room_configs:set(node, data);
+ room._data.history = history;
+ elseif forced then
+ room_configs:set(node, nil);
+ if not next(room._occupants) then -- Room empty
+ rooms[room.jid] = nil;
+ end
+ end
+ if forced then persistent_rooms_storage:set(nil, persistent_rooms); end
+end
+
+function create_room(jid)
+ local room = muc_new_room(jid);
+ room.route_stanza = room_route_stanza;
+ room.save = room_save;
+ rooms[jid] = room;
+ return room;
+end
+
+local persistent_errors = false;
+for jid in pairs(persistent_rooms) do
+ local node = jid_split(jid);
+ local data = room_configs:get(node);
+ if data then
+ local room = create_room(jid);
+ room._data = data._data;
+ room._affiliations = data._affiliations;
+ else -- missing room data
+ persistent_rooms[jid] = nil;
+ module:log("error", "Missing data for room '%s', removing from persistent room list", jid);
+ persistent_errors = true;
+ end
+end
+if persistent_errors then persistent_rooms_storage:set(nil, persistent_rooms); end
+
+local host_room = muc_new_room(muc_host);
+host_room.route_stanza = room_route_stanza;
+host_room.save = room_save;
+
+local function get_disco_info(stanza)
+ return st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info")
+ :tag("identity", {category='conference', type='text', name=muc_name}):up()
+ :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply
+end
+local function get_disco_items(stanza)
+ local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
+ for jid, room in pairs(rooms) do
+ if not room:is_hidden() then
+ reply:tag("item", {jid=jid, name=room:get_name()}):up();
+ end
+ end
+ return reply; -- TODO cache disco reply
+end
+
+local function handle_to_domain(event)
+ local origin, stanza = event.origin, event.stanza;
+ local type = stanza.attr.type;
+ if type == "error" or type == "result" then return; end
+ if stanza.name == "iq" and type == "get" then
+ local xmlns = stanza.tags[1].attr.xmlns;
+ local node = stanza.tags[1].attr.node;
+ if xmlns == "http://jabber.org/protocol/disco#info" and not node then
+ origin.send(get_disco_info(stanza));
+ elseif xmlns == "http://jabber.org/protocol/disco#items" and not node then
+ origin.send(get_disco_items(stanza));
+ elseif xmlns == "http://jabber.org/protocol/muc#unique" then
+ origin.send(st.reply(stanza):tag("unique", {xmlns = xmlns}):text(uuid_gen())); -- FIXME Random UUIDs can theoretically have collisions
+ else
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc
+ end
+ else
+ host_room:handle_stanza(origin, stanza);
+ --origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it"));
+ end
+ return true;
+end
+
+function stanza_handler(event)
+ local origin, stanza = event.origin, event.stanza;
+ local bare = jid_bare(stanza.attr.to);
+ local room = rooms[bare];
+ if not room then
+ if stanza.name ~= "presence" then
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+ return true;
+ end
+ if not(restrict_room_creation) or
+ (restrict_room_creation == "admin" and is_admin(stanza.attr.from)) or
+ (restrict_room_creation == "local" and select(2, jid_split(stanza.attr.from)) == module.host:gsub("^[^%.]+%.", "")) then
+ room = create_room(bare);
+ end
+ end
+ if room then
+ room:handle_stanza(origin, stanza);
+ if not next(room._occupants) and not persistent_rooms[room.jid] then -- empty, non-persistent room
+ rooms[bare] = nil; -- discard room
+ end
+ else
+ origin.send(st.error_reply(stanza, "cancel", "not-allowed"));
+ end
+ 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", handle_to_domain, -1);
+module:hook("message/host", handle_to_domain, -1);
+module:hook("presence/host", handle_to_domain, -1);
+
+hosts[module.host].send = function(stanza) -- FIXME do a generic fix
+ if stanza.attr.type == "result" or stanza.attr.type == "error" then
+ module:send(stanza);
+ else error("component.send only supports result and error stanzas at the moment"); end
+end
+
+hosts[module:get_host()].muc = { rooms = rooms };
+
+local saved = false;
+module.save = function()
+ saved = true;
+ return {rooms = rooms};
+end
+module.restore = function(data)
+ for jid, oldroom in pairs(data.rooms or {}) do
+ local room = create_room(jid);
+ room._jid_nick = oldroom._jid_nick;
+ room._occupants = oldroom._occupants;
+ room._data = oldroom._data;
+ room._affiliations = oldroom._affiliations;
+ end
+ hosts[module:get_host()].muc = { rooms = rooms };
+end
+
+function shutdown_room(room, stanza)
+ for nick, occupant in pairs(room._occupants) do
+ stanza.attr.from = nick;
+ for jid in pairs(occupant.sessions) do
+ stanza.attr.to = jid;
+ room:_route_stanza(stanza);
+ room._jid_nick[jid] = nil;
+ end
+ room._occupants[nick] = nil;
+ end
+end
+function shutdown_component()
+ if not saved then
+ local stanza = st.presence({type = "unavailable"})
+ :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
+ :tag("item", { affiliation='none', role='none' }):up();
+ for roomjid, room in pairs(rooms) do
+ shutdown_room(room, stanza);
+ end
+ shutdown_room(host_room, stanza);
+ end
+end
+module.unload = shutdown_component;
+module:hook_global("server-stopping", shutdown_component);
diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua
new file mode 100644
index 00000000..a5aba3c8
--- /dev/null
+++ b/plugins/muc/muc.lib.lua
@@ -0,0 +1,1171 @@
+-- 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 select = select;
+local pairs, ipairs = pairs, ipairs;
+
+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;
+local st = require "util.stanza";
+local log = require "util.logger".init("mod_muc");
+local t_insert, t_remove = table.insert, table.remove;
+local setmetatable = setmetatable;
+local base64 = require "util.encodings".base64;
+local md5 = require "util.hashes".md5;
+
+local muc_domain = nil; --module:get_host();
+local default_history_length, max_history_length = 20, math.huge;
+
+------------
+local function filter_xmlns_from_array(array, filters)
+ local count = 0;
+ for i=#array,1,-1 do
+ local attr = array[i].attr;
+ if filters[attr and attr.xmlns] then
+ t_remove(array, i);
+ count = count + 1;
+ end
+ end
+ return count;
+end
+local function filter_xmlns_from_stanza(stanza, filters)
+ if filters then
+ if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then
+ return stanza, filter_xmlns_from_array(stanza, filters);
+ end
+ end
+ return stanza, 0;
+end
+local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true};
+local function get_filtered_presence(stanza)
+ return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters);
+end
+local kickable_error_conditions = {
+ ["gone"] = true;
+ ["internal-server-error"] = true;
+ ["item-not-found"] = true;
+ ["jid-malformed"] = true;
+ ["recipient-unavailable"] = true;
+ ["redirect"] = true;
+ ["remote-server-not-found"] = true;
+ ["remote-server-timeout"] = true;
+ ["service-unavailable"] = true;
+ ["malformed error"] = true;
+};
+
+local function get_error_condition(stanza)
+ local _, condition = stanza:get_error();
+ return condition or "malformed error";
+end
+
+local function is_kickable_error(stanza)
+ local cond = get_error_condition(stanza);
+ return kickable_error_conditions[cond] and cond;
+end
+local function getUsingPath(stanza, path, getText)
+ local tag = stanza;
+ for _, name in ipairs(path) do
+ if type(tag) ~= 'table' then return; end
+ tag = tag:child_with_name(name);
+ end
+ if tag and getText then tag = table.concat(tag); end
+ return tag;
+end
+local function getTag(stanza, path) return getUsingPath(stanza, path); end
+local function getText(stanza, path) return getUsingPath(stanza, path, true); end
+-----------
+
+local room_mt = {};
+room_mt.__index = room_mt;
+
+function room_mt:__tostring()
+ return "MUC room ("..self.jid..")";
+end
+
+function room_mt:get_default_role(affiliation)
+ if affiliation == "owner" or affiliation == "admin" then
+ return "moderator";
+ elseif affiliation == "member" then
+ return "participant";
+ elseif not affiliation then
+ if not self:is_members_only() then
+ return self:is_moderated() and "visitor" or "participant";
+ end
+ end
+end
+
+function room_mt:broadcast_presence(stanza, sid, code, nick)
+ stanza = get_filtered_presence(stanza);
+ local occupant = self._occupants[stanza.attr.from];
+ stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none", nick=nick}):up();
+ if code then
+ stanza:tag("status", {code=code}):up();
+ end
+ self:broadcast_except_nick(stanza, stanza.attr.from);
+ local me = self._occupants[stanza.attr.from];
+ if me then
+ stanza:tag("status", {code='110'}):up();
+ stanza.attr.to = sid;
+ self:_route_stanza(stanza);
+ end
+end
+function room_mt:broadcast_message(stanza, historic)
+ local to = stanza.attr.to;
+ for occupant, o_data in pairs(self._occupants) do
+ for jid in pairs(o_data.sessions) do
+ stanza.attr.to = jid;
+ self:_route_stanza(stanza);
+ end
+ end
+ stanza.attr.to = to;
+ if historic then -- add to history
+ local history = self._data['history'];
+ if not history then history = {}; self._data['history'] = history; end
+ stanza = st.clone(stanza);
+ stanza.attr.to = "";
+ local stamp = datetime.datetime();
+ stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203
+ stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
+ local entry = { stanza = stanza, stamp = stamp };
+ 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)
+ for rnick, occupant in pairs(self._occupants) do
+ if rnick ~= nick then
+ for jid in pairs(occupant.sessions) do
+ stanza.attr.to = jid;
+ self:_route_stanza(stanza);
+ end
+ end
+ end
+end
+
+function room_mt:send_occupant_list(to)
+ local current_nick = self._jid_nick[to];
+ for occupant, o_data in pairs(self._occupants) do
+ if occupant ~= current_nick then
+ local pres = get_filtered_presence(o_data.sessions[o_data.jid]);
+ pres.attr.to, pres.attr.from = to, occupant;
+ pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("item", {affiliation=o_data.affiliation or "none", role=o_data.role or "none"}):up();
+ self:_route_stanza(pres);
+ end
+ end
+end
+function room_mt:send_history(to, stanza)
+ local history = self._data['history']; -- send discussion history
+ if history then
+ local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc");
+ local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc");
+
+ local maxchars = history_tag and tonumber(history_tag.attr.maxchars);
+ if maxchars then maxchars = math.floor(maxchars); end
+
+ local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history);
+ if not history_tag then maxstanzas = 20; end
+
+ 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;
+
+ 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._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject']));
+ end
+end
+
+function room_mt:get_disco_info(stanza)
+ local count = 0; for _ in pairs(self._occupants) do count = count + 1; end
+ return st.reply(stanza):query("http://jabber.org/protocol/disco#info")
+ :tag("identity", {category="conference", type="text", name=self:get_name()}):up()
+ :tag("feature", {var="http://jabber.org/protocol/muc"}):up()
+ :tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up()
+ :tag("feature", {var=self:is_moderated() and "muc_moderated" or "muc_unmoderated"}):up()
+ :tag("feature", {var=self:is_members_only() and "muc_membersonly" or "muc_open"}):up()
+ :tag("feature", {var=self:is_persistent() and "muc_persistent" or "muc_temporary"}):up()
+ :tag("feature", {var=self:is_hidden() and "muc_hidden" or "muc_public"}):up()
+ :tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up()
+ :add_child(dataform.new({
+ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" },
+ { name = "muc#roominfo_description", label = "Description"},
+ { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) }
+ }):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");
+ for room_jid in pairs(self._occupants) do
+ reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up();
+ end
+ return reply;
+end
+function room_mt:set_subject(current_nick, subject)
+ -- TODO check nick's authority
+ if subject == "" then subject = nil; end
+ self._data['subject'] = subject;
+ 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();
+ self:broadcast_message(msg, false);
+ return true;
+end
+
+local function build_unavailable_presence_from_error(stanza)
+ local type, condition, text = stanza:get_error();
+ local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error");
+ if text then
+ error_message = error_message..": "..text;
+ end
+ return st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to})
+ :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:get_historylength()
+ return self._data.history_length or default_history_length;
+end
+function room_mt:set_historylength(length)
+ length = math.min(tonumber(length) or default_history_length, max_history_length or math.huge);
+ if length == default_history_length then
+ length = nil;
+ end
+ self._data.history_length = length;
+end
+
+
+local function construct_stanza_id(room, stanza)
+ local from_jid, to_nick = stanza.attr.from, stanza.attr.to;
+ local from_nick = room._jid_nick[from_jid];
+ local occupant = room._occupants[to_nick];
+ local to_jid = occupant.jid;
+
+ return from_nick, to_jid, base64.encode(to_jid.."\0"..stanza.attr.id.."\0"..md5(from_jid));
+end
+local function deconstruct_stanza_id(room, stanza)
+ local from_jid_possiblybare, to_nick = stanza.attr.from, stanza.attr.to;
+ local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(.+)%z(.*)%z(.+)$");
+ local from_nick = room._jid_nick[from_jid];
+
+ if not(from_nick) then return; end
+ if not(from_jid_possiblybare == from_jid or from_jid_possiblybare == jid_bare(from_jid)) then return; end
+
+ local occupant = room._occupants[to_nick];
+ for to_jid in pairs(occupant and occupant.sessions or {}) do
+ if md5(to_jid) == to_jid_hash then
+ return from_nick, to_jid, id;
+ end
+ end
+end
+
+
+function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
+ local from, to = stanza.attr.from, stanza.attr.to;
+ local room = jid_bare(to);
+ local current_nick = self._jid_nick[from];
+ local type = stanza.attr.type;
+ log("debug", "room: %s, current_nick: %s, stanza: %s", room or "nil", current_nick or "nil", stanza:top_tag());
+ if (select(2, jid_split(from)) == muc_domain) then error("Presence from the MUC itself!!!"); end
+ if stanza.name == "presence" then
+ local pr = get_filtered_presence(stanza);
+ pr.attr.from = current_nick;
+ if type == "error" then -- error, kick em out!
+ if current_nick then
+ log("debug", "kicking %s from %s", current_nick, room);
+ self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza));
+ end
+ elseif type == "unavailable" then -- unavailable
+ if current_nick then
+ log("debug", "%s leaving %s", current_nick, room);
+ self._jid_nick[from] = nil;
+ local occupant = self._occupants[current_nick];
+ local new_jid = next(occupant.sessions);
+ if new_jid == from then new_jid = next(occupant.sessions, new_jid); end
+ if new_jid then
+ local jid = occupant.jid;
+ occupant.jid = new_jid;
+ occupant.sessions[from] = nil;
+ pr.attr.to = from;
+ pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("item", {affiliation=occupant.affiliation or "none", role='none'}):up()
+ :tag("status", {code='110'}):up();
+ self:_route_stanza(pr);
+ if jid ~= new_jid then
+ pr = st.clone(occupant.sessions[new_jid])
+ :tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none"});
+ pr.attr.from = current_nick;
+ self:broadcast_except_nick(pr, current_nick);
+ end
+ else
+ occupant.role = 'none';
+ self:broadcast_presence(pr, from);
+ self._occupants[current_nick] = nil;
+ end
+ end
+ elseif not type then -- available
+ if current_nick then
+ --if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence
+ if current_nick == to then -- simple presence
+ log("debug", "%s broadcasted presence", current_nick);
+ self._occupants[current_nick].sessions[from] = pr;
+ self:broadcast_presence(pr, from);
+ else -- change nick
+ local occupant = self._occupants[current_nick];
+ local is_multisession = next(occupant.sessions, next(occupant.sessions));
+ if self._occupants[to] or is_multisession then
+ log("debug", "%s couldn't change nick", current_nick);
+ local reply = st.error_reply(stanza, "cancel", "conflict"):up();
+ reply.tags[1].attr.code = "409";
+ origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ else
+ local data = self._occupants[current_nick];
+ local to_nick = select(3, jid_split(to));
+ if to_nick then
+ log("debug", "%s (%s) changing nick to %s", current_nick, data.jid, to);
+ local p = st.presence({type='unavailable', from=current_nick});
+ self:broadcast_presence(p, from, '303', to_nick);
+ self._occupants[current_nick] = nil;
+ self._occupants[to] = data;
+ self._jid_nick[from] = to;
+ pr.attr.from = to;
+ self._occupants[to].sessions[from] = pr;
+ self:broadcast_presence(pr, from);
+ else
+ --TODO malformed-jid
+ end
+ end
+ end
+ --else -- possible rejoin
+ -- log("debug", "%s had connection replaced", current_nick);
+ -- self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to})
+ -- :tag('status'):text('Replaced by new connection'):up()); -- send unavailable
+ -- self:handle_to_occupant(origin, stanza); -- resend available
+ --end
+ else -- enter room
+ local new_nick = to;
+ local is_merge;
+ if self._occupants[to] then
+ if jid_bare(from) ~= jid_bare(self._occupants[to].jid) then
+ new_nick = nil;
+ end
+ is_merge = true;
+ end
+ 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";
+ origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ else
+ log("debug", "%s joining as %s", from, to);
+ if not next(self._affiliations) then -- new room, no owners
+ self._affiliations[jid_bare(from)] = "owner";
+ end
+ local affiliation = self:get_affiliation(from);
+ local role = self:get_default_role(affiliation)
+ if role then -- new occupant
+ if not is_merge then
+ self._occupants[to] = {affiliation=affiliation, role=role, jid=from, sessions={[from]=get_filtered_presence(stanza)}};
+ else
+ self._occupants[to].sessions[from] = get_filtered_presence(stanza);
+ end
+ self._jid_nick[from] = to;
+ self:send_occupant_list(from);
+ pr.attr.from = to;
+ 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_except_nick(pr, to);
+ end
+ pr:tag("status", {code='110'}):up();
+ if self._data.whois == 'anyone' then
+ pr:tag("status", {code='100'}):up();
+ end
+ 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";
+ origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ end
+ end
+ end
+ elseif type ~= 'result' then -- bad type
+ if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences
+ origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
+ end
+ end
+ elseif not current_nick then -- not in room
+ if (type == "error" or type == "result") and stanza.name == "iq" then
+ local id = stanza.attr.id;
+ stanza.attr.from, stanza.attr.to, stanza.attr.id = deconstruct_stanza_id(self, stanza);
+ if stanza.attr.id then
+ self:_route_stanza(stanza);
+ end
+ stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
+ elseif type ~= "error" then
+ origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
+ end
+ elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM
+ origin.send(st.error_reply(stanza, "modify", "bad-request"));
+ elseif current_nick and stanza.name == "message" and type == "error" and is_kickable_error(stanza) then
+ log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
+ self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable
+ else -- private stanza
+ local o_data = self._occupants[to];
+ if o_data then
+ log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid);
+ if stanza.name == "iq" then
+ local id = stanza.attr.id;
+ if stanza.attr.type == "get" or stanza.attr.type == "set" then
+ stanza.attr.from, stanza.attr.to, stanza.attr.id = construct_stanza_id(self, stanza);
+ else
+ stanza.attr.from, stanza.attr.to, stanza.attr.id = deconstruct_stanza_id(self, stanza);
+ end
+ if type == 'get' and stanza.tags[1].attr.xmlns == 'vcard-temp' then
+ stanza.attr.to = jid_bare(stanza.attr.to);
+ end
+ if stanza.attr.id then
+ self:_route_stanza(stanza);
+ end
+ stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
+ else -- message
+ stanza.attr.from = current_nick;
+ for jid in pairs(o_data.sessions) do
+ stanza.attr.to = jid;
+ self:_route_stanza(stanza);
+ end
+ stanza.attr.from, stanza.attr.to = from, to;
+ end
+ elseif type ~= "error" and type ~= "result" then -- recipient not in room
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
+ end
+ end
+end
+
+function room_mt:send_form(origin, stanza)
+ origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
+ :add_child(self:get_form_layout():form())
+ );
+end
+
+function room_mt:get_form_layout()
+ local form = dataform.new({
+ title = "Configuration for "..self.jid,
+ instructions = "Complete and submit this form to configure the room.",
+ {
+ 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()
+ },
+ {
+ name = 'muc#roomconfig_historylength',
+ type = 'text-single',
+ label = 'Maximum Number of History Messages Returned by Room',
+ value = tostring(self:get_historylength())
+ }
+ });
+ return module:fire_event("muc-config-form", { room = self, form = form }) or form;
+end
+
+local valid_whois = {
+ moderators = true,
+ anyone = true,
+}
+
+function room_mt:process_form(origin, stanza)
+ local query = stanza.tags[1];
+ local form;
+ 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", "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 event = { room = self, fields = fields, changed = dirty };
+ module:fire_event("muc-config-submitted", event);
+ dirty = event.changed or dirty;
+
+ 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'];
+ dirty = dirty or (self:is_persistent() ~= persistent)
+ module:log("debug", "persistent=%s", tostring(persistent));
+
+ local moderated = fields['muc#roomconfig_moderatedroom'];
+ dirty = dirty or (self:is_moderated() ~= moderated)
+ module:log("debug", "moderated=%s", tostring(moderated));
+
+ local membersonly = fields['muc#roomconfig_membersonly'];
+ dirty = dirty or (self:is_members_only() ~= membersonly)
+ module:log("debug", "membersonly=%s", tostring(membersonly));
+
+ local public = fields['muc#roomconfig_publicroom'];
+ dirty = dirty or (self:is_hidden() ~= (not public and true or nil))
+
+ local 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 historylength = tonumber(fields['muc#roomconfig_historylength']);
+ dirty = dirty or (historylength and (self:get_historylength() ~= historylength));
+ module:log('debug', 'historylength=%s', historylength)
+
+
+ local whois = fields['muc#roomconfig_whois'];
+ if not valid_whois[whois] then
+ origin.send(st.error_reply(stanza, 'cancel', 'bad-request', "Invalid value for 'whois'"));
+ return;
+ end
+ local whois_changed = self._data.whois ~= whois
+ self._data.whois = whois
+ module:log('debug', 'whois=%s', whois)
+
+ local 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);
+ self:set_historylength(historylength);
+
+ 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()
+
+ 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)
+ end
+end
+
+function room_mt:destroy(newjid, reason, password)
+ local pr = st.presence({type = "unavailable"})
+ :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
+ :tag("item", { affiliation='none', role='none' }):up()
+ :tag("destroy", {jid=newjid})
+ if reason then pr:tag("reason"):text(reason):up(); end
+ if password then pr:tag("password"):text(password):up(); end
+ for nick, occupant in pairs(self._occupants) do
+ pr.attr.from = nick;
+ for jid in pairs(occupant.sessions) do
+ pr.attr.to = jid;
+ self:_route_stanza(pr);
+ self._jid_nick[jid] = nil;
+ end
+ self._occupants[nick] = nil;
+ end
+ self:set_persistent(false);
+end
+
+function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
+ local type = stanza.attr.type;
+ local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns;
+ if stanza.name == "iq" then
+ if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" and not stanza.tags[1].attr.node then
+ origin.send(self:get_disco_info(stanza));
+ elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" and not stanza.tags[1].attr.node then
+ origin.send(self:get_disco_items(stanza));
+ elseif xmlns == "http://jabber.org/protocol/muc#admin" then
+ local actor = stanza.attr.from;
+ local affiliation = self:get_affiliation(actor);
+ local current_nick = self._jid_nick[actor];
+ local role = current_nick and self._occupants[current_nick].role or self:get_default_role(affiliation);
+ local item = stanza.tags[1].tags[1];
+ if item and item.name == "item" then
+ if type == "set" then
+ local callback = function() origin.send(st.reply(stanza)); end
+ if item.attr.jid then -- Validate provided JID
+ item.attr.jid = jid_prep(item.attr.jid);
+ if not item.attr.jid then
+ origin.send(st.error_reply(stanza, "modify", "jid-malformed"));
+ return;
+ end
+ end
+ if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation
+ local occupant = self._occupants[self.jid.."/"..item.attr.nick];
+ if occupant then item.attr.jid = occupant.jid; end
+ elseif not item.attr.nick and item.attr.jid then
+ local nick = self._jid_nick[item.attr.jid];
+ if nick then item.attr.nick = select(3, jid_split(nick)); end
+ end
+ local reason = item.tags[1] and item.tags[1].name == "reason" and #item.tags[1] == 1 and item.tags[1][1];
+ if item.attr.affiliation and item.attr.jid and not item.attr.role then
+ local success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, callback, reason);
+ if not success then origin.send(st.error_reply(stanza, errtype, err)); end
+ elseif item.attr.role and item.attr.nick and not item.attr.affiliation then
+ local success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, callback, reason);
+ if not success then origin.send(st.error_reply(stanza, errtype, err)); end
+ else
+ origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+ end
+ elseif type == "get" then
+ local _aff = item.attr.affiliation;
+ local _rol = item.attr.role;
+ if _aff and not _rol then
+ if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then
+ local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
+ for jid, affiliation in pairs(self._affiliations) do
+ if affiliation == _aff then
+ reply:tag("item", {affiliation = _aff, jid = jid}):up();
+ end
+ end
+ origin.send(reply);
+ else
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
+ end
+ elseif _rol and not _aff then
+ if role == "moderator" then
+ -- TODO allow admins and owners not in room? Provide read-only access to everyone who can see the participants anyway?
+ if _rol == "none" then _rol = nil; end
+ local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
+ for occupant_jid, occupant in pairs(self._occupants) do
+ if occupant.role == _rol then
+ reply:tag("item", {
+ nick = select(3, jid_split(occupant_jid)),
+ role = _rol or "none",
+ affiliation = occupant.affiliation or "none",
+ jid = occupant.jid
+ }):up();
+ end
+ end
+ origin.send(reply);
+ else
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
+ end
+ else
+ origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+ end
+ end
+ elseif type == "set" or type == "get" then
+ origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+ end
+ elseif xmlns == "http://jabber.org/protocol/muc#owner" and (type == "get" or type == "set") and stanza.tags[1].name == "query" then
+ if self:get_affiliation(stanza.attr.from) ~= "owner" then
+ origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms"));
+ elseif stanza.attr.type == "get" then
+ self:send_form(origin, stanza);
+ elseif stanza.attr.type == "set" then
+ local child = stanza.tags[1].tags[1];
+ if not child then
+ origin.send(st.error_reply(stanza, "modify", "bad-request"));
+ elseif child.name == "destroy" then
+ local newjid = child.attr.jid;
+ local reason, password;
+ for _,tag in ipairs(child.tags) do
+ if tag.name == "reason" then
+ reason = #tag.tags == 0 and tag[1];
+ elseif tag.name == "password" then
+ password = #tag.tags == 0 and tag[1];
+ end
+ end
+ self:destroy(newjid, reason, password);
+ origin.send(st.reply(stanza));
+ else
+ self:process_form(origin, stanza);
+ end
+ end
+ elseif type == "set" or type == "get" then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end
+ elseif stanza.name == "message" and type == "groupchat" then
+ local from, to = stanza.attr.from, stanza.attr.to;
+ local current_nick = self._jid_nick[from];
+ local occupant = self._occupants[current_nick];
+ if not occupant then -- not in room
+ origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
+ elseif occupant.role == "visitor" then
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
+ else
+ local from = stanza.attr.from;
+ stanza.attr.from = current_nick;
+ local subject = getText(stanza, {"subject"});
+ if subject 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;
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
+ end
+ else
+ self:broadcast_message(stanza, self:get_historylength() > 0 and stanza:get_child("body"));
+ end
+ stanza.attr.from = from;
+ end
+ elseif stanza.name == "message" and type == "error" and is_kickable_error(stanza) then
+ local current_nick = self._jid_nick[stanza.attr.from];
+ log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
+ self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable
+ elseif stanza.name == "presence" then -- hack - some buggy clients send presence updates to the room rather than their nick
+ local to = stanza.attr.to;
+ local current_nick = self._jid_nick[stanza.attr.from];
+ if current_nick then
+ stanza.attr.to = current_nick;
+ self:handle_to_occupant(origin, stanza);
+ stanza.attr.to = to;
+ elseif type ~= "error" and type ~= "result" then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end
+ elseif stanza.name == "message" and not(type == "chat" or type == "error" or type == "groupchat" or type == "headline") and #stanza.tags == 1
+ and self._jid_nick[stanza.attr.from] and stanza.tags[1].name == "x" and stanza.tags[1].attr.xmlns == "http://jabber.org/protocol/muc#user" then
+ local x = stanza.tags[1];
+ local payload = (#x.tags == 1 and x.tags[1]);
+ if payload and payload.name == "invite" and payload.attr.to then
+ local _from, _to = stanza.attr.from, stanza.attr.to;
+ local _invitee = jid_prep(payload.attr.to);
+ if _invitee then
+ local _reason = payload.tags[1] and payload.tags[1].name == 'reason' and #payload.tags[1].tags == 0 and payload.tags[1][1];
+ local invite = st.message({from = _to, to = _invitee, id = stanza.attr.id})
+ :tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag('invite', {from=_from})
+ :tag('reason'):text(_reason or ""):up()
+ :up();
+ 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"));
+ end
+ else
+ origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+ end
+ else
+ if type == "error" or type == "result" then return; end
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end
+end
+
+function room_mt:handle_stanza(origin, stanza)
+ local to_node, to_host, to_resource = jid_split(stanza.attr.to);
+ if to_resource then
+ self:handle_to_occupant(origin, stanza);
+ else
+ self:handle_to_room(origin, stanza);
+ end
+end
+
+function room_mt:route_stanza(stanza) end -- Replace with a routing function, e.g., function(room, stanza) core_route_stanza(origin, stanza); end
+
+function room_mt:get_affiliation(jid)
+ local node, host, resource = jid_split(jid);
+ local bare = node and node.."@"..host or host;
+ local result = self._affiliations[bare]; -- Affiliations are granted, revoked, and maintained based on the user's bare JID.
+ if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned
+ return result;
+end
+function room_mt:set_affiliation(actor, jid, affiliation, callback, reason)
+ jid = jid_bare(jid);
+ if affiliation == "none" then affiliation = nil; end
+ if affiliation and affiliation ~= "outcast" and affiliation ~= "owner" and affiliation ~= "admin" and affiliation ~= "member" then
+ return nil, "modify", "not-acceptable";
+ end
+ if actor ~= true then
+ local actor_affiliation = self:get_affiliation(actor);
+ local target_affiliation = self:get_affiliation(jid);
+ if target_affiliation == affiliation then -- no change, shortcut
+ if callback then callback(); end
+ return true;
+ end
+ if actor_affiliation ~= "owner" then
+ if affiliation == "owner" or affiliation == "admin" or actor_affiliation ~= "admin" or target_affiliation == "owner" or target_affiliation == "admin" then
+ return nil, "cancel", "not-allowed";
+ end
+ elseif target_affiliation == "owner" and jid_bare(actor) == jid then -- self change
+ local is_last = true;
+ for j, aff in pairs(self._affiliations) do if j ~= jid and aff == "owner" then is_last = false; break; end end
+ if is_last then
+ return nil, "cancel", "conflict";
+ end
+ end
+ end
+ self._affiliations[jid] = affiliation;
+ local role = self:get_default_role(affiliation);
+ local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"})
+ :tag("item", {affiliation=affiliation or "none", role=role or "none"})
+ :tag("reason"):text(reason or ""):up()
+ :up();
+ local presence_type = nil;
+ if not role then -- getting kicked
+ presence_type = "unavailable";
+ if affiliation == "outcast" then
+ x:tag("status", {code="301"}):up(); -- banned
+ else
+ x:tag("status", {code="321"}):up(); -- affiliation change
+ end
+ end
+ local modified_nicks = {};
+ for nick, occupant in pairs(self._occupants) do
+ if jid_bare(occupant.jid) == jid then
+ if not role then -- getting kicked
+ self._occupants[nick] = nil;
+ else
+ occupant.affiliation, occupant.role = affiliation, role;
+ end
+ 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,p in pairs(modified_nicks) do
+ p.attr.from = nick;
+ self:broadcast_except_nick(p, nick);
+ end
+ return true;
+end
+
+function room_mt:get_role(nick)
+ local session = self._occupants[nick];
+ return session and session.role or nil;
+end
+function room_mt:can_set_role(actor_jid, occupant_jid, role)
+ local occupant = self._occupants[occupant_jid];
+ if not occupant or not actor then return nil, "modify", "not-acceptable"; end
+
+ if actor_jid == true then return true; end
+
+ local actor = self._occupants[self._jid_nick[actor_jid]];
+ if actor.role == "moderator" then
+ if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then
+ if actor.affiliation == "owner" or actor.affiliation == "admin" then
+ 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
+ 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];
+ 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
+ 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
+ x:tag("status", {code = "307"}):up();
+ else
+ occupant.role = role;
+ end
+ 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
+ if bp then
+ self:broadcast_except_nick(bp, occupant_jid);
+ end
+ return true;
+end
+
+function room_mt:_route_stanza(stanza)
+ local muc_child;
+ local to_occupant = self._occupants[self._jid_nick[stanza.attr.to]];
+ local from_occupant = self._occupants[stanza.attr.from];
+ if stanza.name == "presence" then
+ if to_occupant and from_occupant then
+ if self._data.whois == 'anyone' then
+ muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user");
+ else
+ if to_occupant.role == "moderator" or jid_bare(to_occupant.jid) == jid_bare(from_occupant.jid) then
+ muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user");
+ end
+ end
+ end
+ end
+ if muc_child then
+ for _, item in pairs(muc_child.tags) do
+ if item.name == "item" then
+ if from_occupant == to_occupant then
+ item.attr.jid = stanza.attr.to;
+ else
+ item.attr.jid = from_occupant.jid;
+ end
+ end
+ end
+ end
+ self:route_stanza(stanza);
+ if muc_child then
+ for _, item in pairs(muc_child.tags) do
+ if item.name == "item" then
+ item.attr.jid = nil;
+ end
+ end
+ end
+end
+
+local _M = {}; -- module "muc"
+
+function _M.new_room(jid, config)
+ return setmetatable({
+ jid = jid;
+ _jid_nick = {};
+ _occupants = {};
+ _data = {
+ whois = 'moderators';
+ history_length = math.min((config and config.history_length)
+ or default_history_length, max_history_length);
+ };
+ _affiliations = {};
+ }, room_mt);
+end
+
+function _M.set_max_history_length(_max_history_length)
+ max_history_length = _max_history_length or math.huge;
+end
+
+_M.room_mt = room_mt;
+
+return _M;
diff --git a/plugins/sql.lib.lua b/plugins/sql.lib.lua
new file mode 100644
index 00000000..005ee45d
--- /dev/null
+++ b/plugins/sql.lib.lua
@@ -0,0 +1,9 @@
+local cache = module:shared("/*/sql.lib/util.sql");
+
+if not cache._M then
+ prosody.unlock_globals();
+ cache._M = require "util.sql";
+ prosody.lock_globals();
+end
+
+return cache._M;
diff --git a/plugins/storage/mod_xep0227.lua b/plugins/storage/mod_xep0227.lua
new file mode 100644
index 00000000..5d07a2ea
--- /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 = require "util.xml".parse;
+
+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:provides("storage", driver);
diff --git a/plugins/storage/sqlbasic.lib.lua b/plugins/storage/sqlbasic.lib.lua
new file mode 100644
index 00000000..ab3648f9
--- /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 envload = require "util.envload".envload;
+local deser = function(data)
+ module:log("debug", "deser: %s", tostring(data));
+ if not data then return nil; end
+ local f = envload("return "..data, nil, {});
+ if not f then return nil; end
+ 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;