aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/adhoc/adhoc.lib.lua85
-rw-r--r--plugins/adhoc/mod_adhoc.lua74
-rw-r--r--plugins/mod_announce.lua85
-rw-r--r--plugins/mod_auth_anonymous.lua69
-rw-r--r--plugins/mod_auth_cyrus.lua61
-rw-r--r--plugins/mod_auth_internal_hashed.lua178
-rw-r--r--plugins/mod_auth_internal_plain.lua97
-rw-r--r--plugins/mod_bosh.lua5
-rw-r--r--plugins/mod_component.lua2
-rw-r--r--plugins/mod_compression.lua72
-rw-r--r--plugins/mod_disco.lua60
-rw-r--r--plugins/mod_groups.lua10
-rw-r--r--plugins/mod_httpserver.lua11
-rw-r--r--plugins/mod_iq.lua11
-rw-r--r--plugins/mod_legacyauth.lua2
-rw-r--r--plugins/mod_motd.lua25
-rw-r--r--plugins/mod_offline.lua56
-rw-r--r--plugins/mod_pep.lua95
-rw-r--r--plugins/mod_posix.lua35
-rw-r--r--plugins/mod_presence.lua91
-rw-r--r--plugins/mod_privacy.lua4
-rw-r--r--plugins/mod_proxy65.lua23
-rw-r--r--plugins/mod_register.lua5
-rw-r--r--plugins/mod_roster.lua10
-rw-r--r--plugins/mod_saslauth.lua41
-rw-r--r--plugins/mod_tls.lua2
-rw-r--r--plugins/mod_uptime.lua26
-rw-r--r--plugins/muc/mod_muc.lua20
-rw-r--r--plugins/muc/muc.lib.lua225
-rw-r--r--plugins/storage/ejabberdstore.lib.lua190
-rw-r--r--plugins/storage/mod_storage.lua83
-rw-r--r--plugins/storage/sqlbasic.lib.lua97
-rw-r--r--plugins/storage/xep227store.lib.lua168
-rw-r--r--plugins/storage/xmlparse.lib.lua56
34 files changed, 1761 insertions, 313 deletions
diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua
new file mode 100644
index 00000000..361f4731
--- /dev/null
+++ b/plugins/adhoc/adhoc.lib.lua
@@ -0,0 +1,85 @@
+-- Copyright (C) 2009-2010 Florian Zeitz
+--
+-- This file is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st, uuid = require "util.stanza", require "util.uuid";
+
+local xmlns_cmd = "http://jabber.org/protocol/commands";
+
+local states = {}
+
+local _M = {};
+
+function _cmdtag(desc, status, sessionid, action)
+ local cmd = st.stanza("command", { xmlns = xmlns_cmd, node = desc.node, status = status });
+ if sessionid then cmd.attr.sessionid = sessionid; end
+ if action then cmd.attr.action = action; end
+
+ return cmd;
+end
+
+function _M.new(name, node, handler, permission)
+ return { name = name, node = node, handler = handler, cmdtag = _cmdtag, permission = (permission or "user") };
+end
+
+function _M.handle_cmd(command, origin, stanza)
+ local sessionid = stanza.tags[1].attr.sessionid or uuid.generate();
+ local dataIn = {};
+ dataIn.to = stanza.attr.to;
+ dataIn.from = stanza.attr.from;
+ dataIn.action = stanza.tags[1].attr.action or "execute";
+ dataIn.form = stanza.tags[1]:child_with_ns("jabber:x:data");
+
+ local data, state = command:handler(dataIn, states[sessionid]);
+ states[sessionid] = state;
+ local stanza = st.reply(stanza);
+ if data.status == "completed" then
+ states[sessionid] = nil;
+ cmdtag = command:cmdtag("completed", sessionid);
+ elseif data.status == "canceled" then
+ states[sessionid] = nil;
+ cmdtag = command:cmdtag("canceled", sessionid);
+ elseif data.status == "error" then
+ states[sessionid] = nil;
+ stanza = st.error_reply(stanza, data.error.type, data.error.condition, data.error.message);
+ origin.send(stanza);
+ return true;
+ else
+ cmdtag = command:cmdtag("executing", sessionid);
+ end
+
+ for name, content in pairs(data) do
+ if name == "info" then
+ cmdtag:tag("note", {type="info"}):text(content):up();
+ elseif name == "warn" then
+ cmdtag:tag("note", {type="warn"}):text(content):up();
+ elseif name == "error" then
+ cmdtag:tag("note", {type="error"}):text(content.message):up();
+ elseif name =="actions" then
+ local actions = st.stanza("actions");
+ for _, action in ipairs(content) do
+ if (action == "prev") or (action == "next") or (action == "complete") then
+ actions:tag(action):up();
+ else
+ module:log("error", 'Command "'..command.name..
+ '" at node "'..command.node..'" provided an invalid action "'..action..'"');
+ end
+ end
+ cmdtag:add_child(actions);
+ elseif name == "form" then
+ cmdtag:add_child((content.layout or content):form(content.data));
+ elseif name == "result" then
+ cmdtag:add_child((content.layout or content):form(content.data, "result"));
+ elseif name == "other" then
+ cmdtag:add_child(content);
+ end
+ end
+ stanza:add_child(cmdtag);
+ origin.send(stanza);
+
+ return true;
+end
+
+return _M;
diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua
new file mode 100644
index 00000000..41362d81
--- /dev/null
+++ b/plugins/adhoc/mod_adhoc.lua
@@ -0,0 +1,74 @@
+-- Copyright (C) 2009 Thilo Cestonaro
+--
+-- This file is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza";
+local is_admin = require "core.usermanager".is_admin;
+local adhoc_handle_cmd = module:require "adhoc".handle_cmd;
+local xmlns_cmd = "http://jabber.org/protocol/commands";
+local xmlns_disco = "http://jabber.org/protocol/disco";
+local commands = {};
+
+module:add_feature(xmlns_cmd);
+
+module:hook("iq/host/"..xmlns_disco.."#items:query", function (event)
+ local origin, stanza = event.origin, event.stanza;
+ local privileged = is_admin(stanza.attr.from, stanza.attr.to);
+ if stanza.attr.type == "get" and stanza.tags[1].attr.node
+ and stanza.tags[1].attr.node == xmlns_cmd then
+ reply = st.reply(stanza);
+ reply:tag("query", { xmlns = xmlns_disco.."#items",
+ node = xmlns_cmd });
+ for node, command in pairs(commands) do
+ if (command.permission == "admin" and privileged)
+ or (command.permission == "user") then
+ reply:tag("item", { name = command.name,
+ node = node, jid = module:get_host() });
+ reply:up();
+ end
+ end
+ origin.send(reply);
+ return true;
+ end
+end, 500);
+
+module:hook("iq/host", function (event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type == "set" and stanza.tags[1]
+ and stanza.tags[1].name == "command" then
+ local node = stanza.tags[1].attr.node
+ -- TODO: Is this correct, or should is_admin be changed?
+ local privileged = is_admin(event.stanza.attr.from)
+ or is_admin(stanza.attr.from, stanza.attr.to);
+ if commands[node] then
+ if commands[node].permission == "admin"
+ and not privileged then
+ origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up()
+ :add_child(commands[node]:cmdtag("canceled")
+ :tag("note", {type="error"}):text("You don't have permission to execute this command")));
+ return true
+ end
+ -- User has permission now execute the command
+ return adhoc_handle_cmd(commands[node], origin, stanza);
+ end
+ end
+end, 500);
+
+local function handle_item_added(item)
+ commands[item.node] = item;
+end
+
+module:hook("item-added/adhoc", function (event)
+ return handle_item_added(event.item);
+end, 500);
+
+module:hook("item-removed/adhoc", function (event)
+ commands[event.item.node] = nil;
+end, 500);
+
+-- Pick up any items that are already added
+for _, item in ipairs(module:get_host_items("adhoc")) do
+ handle_item_added(item);
+end
diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua
index d3017f6c..77555bec 100644
--- a/plugins/mod_announce.lua
+++ b/plugins/mod_announce.lua
@@ -6,14 +6,38 @@
-- COPYING file in the source package for more information.
--
-local st, jid, set = require "util.stanza", require "util.jid", require "util.set";
+local st, jid = require "util.stanza", require "util.jid";
local is_admin = require "core.usermanager".is_admin;
-local admins = set.new(config.get(module:get_host(), "core", "admins"));
-function handle_announcement(data)
- local origin, stanza = data.origin, data.stanza;
- local host, resource = select(2, jid.split(stanza.attr.to));
+function send_to_online(message, host)
+ local sessions;
+ if host then
+ sessions = { [host] = hosts[host] };
+ else
+ sessions = hosts;
+ end
+
+ local c = 0;
+ for hostname, host_session in pairs(sessions) do
+ if host_session.sessions then
+ message.attr.from = hostname;
+ for username in pairs(host_session.sessions) do
+ c = c + 1;
+ message.attr.to = username.."@"..hostname;
+ core_post_stanza(host_session, message);
+ end
+ end
+ end
+
+ return c;
+end
+
+
+-- Old <message>-based jabberd-style announcement sending
+function handle_announcement(event)
+ local origin, stanza = event.origin, event.stanza;
+ local node, host, resource = jid.split(stanza.attr.to);
if resource ~= "announce/online" then
return; -- Not an announcement
@@ -21,25 +45,56 @@ function handle_announcement(data)
if not is_admin(stanza.attr.from) then
-- Not an admin? Not allowed!
- module:log("warn", "Non-admin %s tried to send server announcement", tostring(jid.bare(stanza.attr.from)));
+ module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from);
return;
end
module:log("info", "Sending server announcement to all online users");
- local host_session = hosts[host];
local message = st.clone(stanza);
message.attr.type = "headline";
message.attr.from = host;
- local c = 0;
- for user in pairs(host_session.sessions) do
- c = c + 1;
- message.attr.to = user.."@"..host;
- core_post_stanza(host_session, message);
- end
-
+ local c = send_to_online(message, host);
module:log("info", "Announcement sent to %d online users", c);
return true;
end
-
module:hook("message/host", handle_announcement);
+
+-- Ad-hoc command (XEP-0133)
+local dataforms_new = require "util.dataforms".new;
+local announce_layout = dataforms_new{
+ title = "Making an Announcement";
+ instructions = "Fill out this form to make an announcement to all\nactive users of this service.";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
+ { name = "subject", type = "text-single", label = "Subject" };
+ { name = "announcement", type = "text-multi", required = true, label = "Announcement" };
+};
+
+function announce_handler(self, data, state)
+ if state then
+ if data.action == "cancel" then
+ return { status = "canceled" };
+ end
+
+ local fields = announce_layout:data(data.form);
+
+ module:log("info", "Sending server announcement to all online users");
+ local message = st.message({type = "headline"}, fields.announcement):up()
+ :tag("subject"):text(fields.subject or "Announcement");
+
+ local count = send_to_online(message, data.to);
+
+ module:log("info", "Announcement sent to %d online users", count);
+ return { status = "completed", info = ("Announcement sent to %d online users"):format(count) };
+ else
+ return { status = "executing", form = announce_layout }, "executing";
+ end
+
+ return true;
+end
+
+local adhoc_new = module:require "adhoc".new;
+local announce_desc = adhoc_new("Send Announcement to Online Users", "http://jabber.org/protocol/admin#announce", announce_handler, "admin");
+module:add_item("adhoc", announce_desc);
+
diff --git a/plugins/mod_auth_anonymous.lua b/plugins/mod_auth_anonymous.lua
new file mode 100644
index 00000000..35e87a2e
--- /dev/null
+++ b/plugins/mod_auth_anonymous.lua
@@ -0,0 +1,69 @@
+-- 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_anonymous");
+local new_sasl = require "util.sasl".new;
+local datamanager = require "util.datamanager";
+
+function new_default_provider(host)
+ local provider = { name = "anonymous" };
+
+ function provider.test_password(username, password)
+ return nil, "Password based auth not supported.";
+ end
+
+ function provider.get_password(username)
+ return nil, "Password not available.";
+ end
+
+ function provider.set_password(username, password)
+ return nil, "Password based auth not supported.";
+ end
+
+ function provider.user_exists(username)
+ return nil, "Only anonymous users are supported."; -- FIXME check if anonymous user is connected?
+ end
+
+ function provider.create_user(username, password)
+ return nil, "Account creation/modification not supported.";
+ end
+
+ function provider.get_sasl_handler()
+ local realm = module:get_option("sasl_realm") or module.host;
+ local anonymous_authentication_profile = {
+ anonymous = function(username, realm)
+ return true; -- for normal usage you should always return true here
+ end
+ };
+ return new_sasl(realm, anonymous_authentication_profile);
+ end
+
+ return provider;
+end
+
+local function dm_callback(username, host, datastore, data)
+ if host == module.host then
+ return false;
+ end
+ return username, host, datastore, data;
+end
+local host = hosts[module.host];
+local _saved_disallow_s2s = host.disallow_s2s;
+function module.load()
+ _saved_disallow_s2s = host.disallow_s2s;
+ host.disallow_s2s = module:get_option("disallow_s2s") ~= false;
+ datamanager.add_callback(dm_callback);
+end
+function module.unload()
+ host.disallow_s2s = _saved_disallow_s2s;
+ datamanager.remove_callback(dm_callback);
+end
+
+module:add_item("auth-provider", new_default_provider(module.host));
+
diff --git a/plugins/mod_auth_cyrus.lua b/plugins/mod_auth_cyrus.lua
new file mode 100644
index 00000000..7c23ea21
--- /dev/null
+++ b/plugins/mod_auth_cyrus.lua
@@ -0,0 +1,61 @@
+-- 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_cyrus");
+
+local cyrus_service_realm = module:get_option("cyrus_service_realm");
+local cyrus_service_name = module:get_option("cyrus_service_name");
+local cyrus_application_name = module:get_option("cyrus_application_name");
+
+prosody.unlock_globals(); --FIXME: Figure out why this is needed and
+ -- why cyrussasl isn't caught by the sandbox
+local cyrus_new = require "util.sasl_cyrus".new;
+prosody.lock_globals();
+local new_sasl = function(realm)
+ return cyrus_new(
+ cyrus_service_realm or realm,
+ cyrus_service_name or "xmpp",
+ cyrus_application_name or "prosody"
+ );
+end
+
+function new_default_provider(host)
+ local provider = { name = "cyrus" };
+ log("debug", "initializing default authentication provider for host '%s'", host);
+
+ function provider.test_password(username, password)
+ return nil, "Legacy auth not supported with Cyrus SASL.";
+ end
+
+ function provider.get_password(username)
+ return nil, "Passwords unavailable for Cyrus SASL.";
+ end
+
+ function provider.set_password(username, password)
+ return nil, "Passwords unavailable for Cyrus SASL.";
+ end
+
+ function provider.user_exists(username)
+ return true;
+ end
+
+ function provider.create_user(username, password)
+ return nil, "Account creation/modification not available with Cyrus SASL.";
+ end
+
+ function provider.get_sasl_handler()
+ local realm = module:get_option("sasl_realm") or module.host;
+ return new_sasl(realm);
+ end
+
+ return provider;
+end
+
+module:add_item("auth-provider", new_default_provider(module.host));
+
diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua
new file mode 100644
index 00000000..2bee141c
--- /dev/null
+++ b/plugins/mod_auth_internal_hashed.lua
@@ -0,0 +1,178 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- Copyright (C) 2010 Jeff Mitchell
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local datamanager = require "util.datamanager";
+local log = require "util.logger".init("auth_internal_hashed");
+local type = type;
+local error = error;
+local ipairs = ipairs;
+local hashes = require "util.hashes";
+local jid_bare = require "util.jid".bare;
+local getAuthenticationDatabaseSHA1 = require "util.sasl.scram".getAuthenticationDatabaseSHA1;
+local config = require "core.configmanager";
+local usermanager = require "core.usermanager";
+local generate_uuid = require "util.uuid".generate;
+local new_sasl = require "util.sasl".new;
+local nodeprep = require "util.encodings".stringprep.nodeprep;
+local hosts = hosts;
+
+-- COMPAT w/old trunk: remove these two lines before 0.8 release
+local hmac_sha1 = require "util.hmac".sha1;
+local sha1 = require "util.hashes".sha1;
+
+local to_hex;
+do
+ local function replace_byte_with_hex(byte)
+ return ("%02x"):format(byte:byte());
+ end
+ function to_hex(binary_string)
+ return binary_string:gsub(".", replace_byte_with_hex);
+ end
+end
+
+local from_hex;
+do
+ local function replace_hex_with_byte(hex)
+ return string.char(tonumber(hex, 16));
+ end
+ function from_hex(hex_string)
+ return hex_string:gsub("..", replace_hex_with_byte);
+ end
+end
+
+
+local prosody = _G.prosody;
+
+-- Default; can be set per-user
+local iteration_count = 4096;
+
+function new_hashpass_provider(host)
+ local provider = { name = "internal_hashed" };
+ log("debug", "initializing hashpass authentication provider for host '%s'", host);
+
+ function provider.test_password(username, password)
+ local credentials = datamanager.load(username, host, "accounts") or {};
+
+ if credentials.password ~= nil and string.len(credentials.password) ~= 0 then
+ if credentials.password ~= password then
+ return nil, "Auth failed. Provided password is incorrect.";
+ end
+
+ if provider.set_password(username, credentials.password) == nil then
+ return nil, "Auth failed. Could not set hashed password from plaintext.";
+ else
+ return true;
+ end
+ end
+
+ if credentials.iteration_count == nil or credentials.salt == nil or string.len(credentials.salt) == 0 then
+ return nil, "Auth failed. Stored salt and iteration count information is not complete.";
+ end
+
+ -- convert hexpass to stored_key and server_key
+ -- COMPAT w/old trunk: remove before 0.8 release
+ if credentials.hashpass then
+ local salted_password = from_hex(credentials.hashpass);
+ credentials.stored_key = sha1(hmac_sha1(salted_password, "Client Key"), true);
+ credentials.server_key = to_hex(hmac_sha1(salted_password, "Server Key"));
+ credentials.hashpass = nil
+ datamanager.store(username, host, "accounts", credentials);
+ end
+
+ local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, credentials.salt, credentials.iteration_count);
+
+ local stored_key_hex = to_hex(stored_key);
+ local server_key_hex = to_hex(server_key);
+
+ if valid and stored_key_hex == credentials.stored_key and server_key_hex == credentials.server_key then
+ return true;
+ else
+ return nil, "Auth failed. Invalid username, password, or password hash information.";
+ end
+ end
+
+ function provider.set_password(username, password)
+ local account = datamanager.load(username, host, "accounts");
+ if account then
+ account.salt = account.salt or generate_uuid();
+ account.iteration_count = account.iteration_count or iteration_count;
+ local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, account.salt, account.iteration_count);
+ local stored_key_hex = to_hex(stored_key);
+ local server_key_hex = to_hex(server_key);
+
+ account.stored_key = stored_key_hex
+ account.server_key = server_key_hex
+
+ account.password = nil;
+ return datamanager.store(username, host, "accounts", account);
+ end
+ return nil, "Account not available.";
+ end
+
+ function provider.user_exists(username)
+ local account = datamanager.load(username, host, "accounts");
+ if not account then
+ log("debug", "account not found for username '%s' at host '%s'", username, module.host);
+ return nil, "Auth failed. Invalid username";
+ end
+ return true;
+ end
+
+ function provider.create_user(username, password)
+ local salt = generate_uuid();
+ local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, iteration_count);
+ local stored_key_hex = to_hex(stored_key);
+ local server_key_hex = to_hex(server_key);
+ return datamanager.store(username, host, "accounts", {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = iteration_count});
+ end
+
+ function provider.get_sasl_handler()
+ local realm = module:get_option("sasl_realm") or module.host;
+ local testpass_authentication_profile = {
+ plain_test = function(username, password, realm)
+ local prepped_username = nodeprep(username);
+ if not prepped_username then
+ log("debug", "NODEprep failed on username: %s", username);
+ return "", nil;
+ end
+ return usermanager.test_password(prepped_username, realm, password), true;
+ end,
+ scram_sha_1 = function(username, realm)
+ local credentials = datamanager.load(username, host, "accounts");
+ if not credentials then return; end
+ if credentials.password then
+ usermanager.set_password(username, credentials.password, host);
+ credentials = datamanager.load(username, host, "accounts");
+ if not credentials then return; end
+ end
+
+ -- convert hexpass to stored_key and server_key
+ -- COMPAT w/old trunk: remove before 0.8 release
+ if credentials.hashpass then
+ local salted_password = from_hex(credentials.hashpass);
+ credentials.stored_key = sha1(hmac_sha1(salted_password, "Client Key"), true);
+ credentials.server_key = to_hex(hmac_sha1(salted_password, "Server Key"));
+ credentials.hashpass = nil
+ datamanager.store(username, host, "accounts", credentials);
+ end
+
+ local stored_key, server_key, iteration_count, salt = credentials.stored_key, credentials.server_key, credentials.iteration_count, credentials.salt;
+ stored_key = stored_key and from_hex(stored_key);
+ server_key = server_key and from_hex(server_key);
+ return stored_key, server_key, iteration_count, salt, true;
+ end
+ };
+ return new_sasl(realm, testpass_authentication_profile);
+ end
+
+ return provider;
+end
+
+module:add_item("auth-provider", new_hashpass_provider(module.host));
+
diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua
new file mode 100644
index 00000000..fdc1216c
--- /dev/null
+++ b/plugins/mod_auth_internal_plain.lua
@@ -0,0 +1,97 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- Copyright (C) 2010 Jeff Mitchell
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local datamanager = require "util.datamanager";
+local log = require "util.logger".init("auth_internal_plain");
+local type = type;
+local error = error;
+local ipairs = ipairs;
+local hashes = require "util.hashes";
+local jid_bare = require "util.jid".bare;
+local config = require "core.configmanager";
+local usermanager = require "core.usermanager";
+local new_sasl = require "util.sasl".new;
+local nodeprep = require "util.encodings".stringprep.nodeprep;
+local hosts = hosts;
+
+local prosody = _G.prosody;
+
+local is_cyrus = usermanager.is_cyrus;
+
+function new_default_provider(host)
+ local provider = { name = "internal_plain" };
+ log("debug", "initializing default authentication provider for host '%s'", host);
+
+ function provider.test_password(username, password)
+ log("debug", "test password '%s' for user %s at host %s", password, username, module.host);
+ if is_cyrus(host) then return nil, "Legacy auth not supported with Cyrus SASL."; end
+ local credentials = datamanager.load(username, host, "accounts") or {};
+
+ if password == credentials.password then
+ return true;
+ else
+ return nil, "Auth failed. Invalid username or password.";
+ end
+ end
+
+ function provider.get_password(username)
+ log("debug", "get_password for username '%s' at host '%s'", username, module.host);
+ if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end
+ return (datamanager.load(username, host, "accounts") or {}).password;
+ end
+
+ function provider.set_password(username, password)
+ if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end
+ local account = datamanager.load(username, host, "accounts");
+ if account then
+ account.password = password;
+ return datamanager.store(username, host, "accounts", account);
+ end
+ return nil, "Account not available.";
+ end
+
+ function provider.user_exists(username)
+ if is_cyrus(host) then return true; end
+ local account = datamanager.load(username, host, "accounts");
+ if not account then
+ log("debug", "account not found for username '%s' at host '%s'", username, module.host);
+ return nil, "Auth failed. Invalid username";
+ end
+ return true;
+ end
+
+ function provider.create_user(username, password)
+ if is_cyrus(host) then return nil, "Account creation/modification not available with Cyrus SASL."; end
+ return datamanager.store(username, host, "accounts", {password = password});
+ end
+
+ function provider.get_sasl_handler()
+ local realm = module:get_option("sasl_realm") or module.host;
+ local getpass_authentication_profile = {
+ plain = function(username, realm)
+ local prepped_username = nodeprep(username);
+ if not prepped_username then
+ log("debug", "NODEprep failed on username: %s", username);
+ return "", nil;
+ end
+ local password = usermanager.get_password(prepped_username, realm);
+ if not password then
+ return "", nil;
+ end
+ return password, true;
+ end
+ };
+ return new_sasl(realm, getpass_authentication_profile);
+ end
+
+ return provider;
+end
+
+module:add_item("auth-provider", new_default_provider(module.host));
+
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua
index 66a79785..e03b2bbd 100644
--- a/plugins/mod_bosh.lua
+++ b/plugins/mod_bosh.lua
@@ -190,6 +190,11 @@ function stream_callbacks.streamopened(request, attr)
local r, send_buffer = session.requests, session.send_buffer;
local response = { headers = default_headers }
function session.send(s)
+ -- We need to ensure that outgoing stanzas have the jabber:client xmlns
+ if s.attr and not s.attr.xmlns then
+ s = st.clone(s);
+ s.attr.xmlns = "jabber:client";
+ end
--log("debug", "Sending BOSH data: %s", tostring(s));
local oldest_request = r[1];
if oldest_request then
diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua
index 7efb4f9c..03331743 100644
--- a/plugins/mod_component.lua
+++ b/plugins/mod_component.lua
@@ -50,6 +50,8 @@ function handle_component_auth(session, stanza)
-- Authenticated now
log("info", "Component authenticated: %s", session.host);
+ session.component_validate_from = module:get_option_boolean("validate_from_addresses") ~= false;
+
-- If component not already created for this host, create one now
if not hosts[session.host].connected then
local send = session.send;
diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua
index 53341492..d890931f 100644
--- a/plugins/mod_compression.lua
+++ b/plugins/mod_compression.lua
@@ -14,6 +14,7 @@ local xmlns_compression_feature = "http://jabber.org/features/compress"
local xmlns_compression_protocol = "http://jabber.org/protocol/compress"
local xmlns_stream = "http://etherx.jabber.org/streams";
local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up();
+local add_filter = require "util.filters".add_filter;
local compression_level = module:get_option("compression_level");
-- if not defined assume admin wants best compression
@@ -94,43 +95,36 @@ end
-- setup compression for a stream
local function setup_compression(session, deflate_stream)
- local old_send = (session.sends2s or session.send);
-
- local new_send = function(t)
- --TODO: Better code injection in the sending process
- local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
- if status == false then
- session:close({
- condition = "undefined-condition";
- text = compressed;
- extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
- });
- module:log("warn", "%s", tostring(compressed));
- return;
- end
- session.conn:write(compressed);
- end;
-
- if session.sends2s then session.sends2s = new_send
- elseif session.send then session.send = new_send end
+ add_filter(session, "bytes/out", function(t)
+ local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
+ if status == false then
+ module:log("warn", "%s", tostring(compressed));
+ session:close({
+ condition = "undefined-condition";
+ text = compressed;
+ extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
+ });
+ return;
+ end
+ return compressed;
+ end);
end
-- setup decompression for a stream
local function setup_decompression(session, inflate_stream)
- local old_data = session.data
- session.data = function(conn, data)
- local status, decompressed, eof = pcall(inflate_stream, data);
- if status == false then
- session:close({
- condition = "undefined-condition";
- text = decompressed;
- extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
- });
- module:log("warn", "%s", tostring(decompressed));
- return;
- end
- old_data(conn, decompressed);
- end;
+ add_filter(session, "bytes/in", function(data)
+ local status, decompressed, eof = pcall(inflate_stream, data);
+ if status == false then
+ module:log("warn", "%s", tostring(decompressed));
+ session:close({
+ condition = "undefined-condition";
+ text = decompressed;
+ extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
+ });
+ return;
+ end
+ return decompressed;
+ end);
end
module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compression_protocol,
@@ -148,12 +142,6 @@ module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compressio
-- setup decompression for session.data
setup_decompression(session, inflate_stream);
- local session_reset_stream = session.reset_stream;
- session.reset_stream = function(session)
- session_reset_stream(session);
- setup_decompression(session, inflate_stream);
- return true;
- end;
session:reset_stream();
local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams",
["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host};
@@ -195,12 +183,6 @@ module:add_handler({"c2s_unauthed", "c2s", "s2sin_unauthed", "s2sin"}, "compress
-- setup decompression for session.data
setup_decompression(session, inflate_stream);
- local session_reset_stream = session.reset_stream;
- session.reset_stream = function(session)
- session_reset_stream(session);
- setup_decompression(session, inflate_stream);
- return true;
- end;
session.compressed = true;
elseif method then
session.log("debug", "%s compression selected, but we don't support it.", tostring(method));
diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua
index ee0043f1..8bb0afec 100644
--- a/plugins/mod_disco.lua
+++ b/plugins/mod_disco.lua
@@ -11,6 +11,7 @@ local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed
local jid_split = require "util.jid".split;
local jid_bare = require "util.jid".bare;
local st = require "util.stanza"
+local calculate_hash = require "util.caps".calculate_hash;
local disco_items = module:get_option("disco_items") or {};
do -- validate disco_items
@@ -35,27 +36,61 @@ module:add_identity("server", "im", "Prosody"); -- FIXME should be in the non-ex
module:add_feature("http://jabber.org/protocol/disco#info");
module:add_feature("http://jabber.org/protocol/disco#items");
-module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(event)
- local origin, stanza = event.origin, event.stanza;
- if stanza.attr.type ~= "get" then return; end
- local node = stanza.tags[1].attr.node;
- if node and node ~= "" then return; end -- TODO fire event?
-
- local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info");
+-- Generate and cache disco result and caps hash
+local _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash;
+local function build_server_disco_info()
+ local query = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" });
local done = {};
for _,identity in ipairs(module:get_host_items("identity")) do
local identity_s = identity.category.."\0"..identity.type;
if not done[identity_s] then
- reply:tag("identity", identity):up();
+ query:tag("identity", identity):up();
done[identity_s] = true;
end
end
for _,feature in ipairs(module:get_host_items("feature")) do
if not done[feature] then
- reply:tag("feature", {var=feature}):up();
+ query:tag("feature", {var=feature}):up();
done[feature] = true;
end
end
+ _cached_server_disco_info = query;
+ _cached_server_caps_hash = calculate_hash(query);
+ _cached_server_caps_feature = st.stanza("c", {
+ xmlns = "http://jabber.org/protocol/caps";
+ hash = "sha-1";
+ node = "http://prosody.im";
+ ver = _cached_server_caps_hash;
+ });
+end
+local function clear_disco_cache()
+ _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash = nil, nil, nil;
+end
+local function get_server_disco_info()
+ if not _cached_server_disco_info then build_server_disco_info(); end
+ return _cached_server_disco_info;
+end
+local function get_server_caps_feature()
+ if not _cached_server_caps_feature then build_server_disco_info(); end
+ return _cached_server_caps_feature;
+end
+local function get_server_caps_hash()
+ if not _cached_server_caps_hash then build_server_disco_info(); end
+ return _cached_server_caps_hash;
+end
+
+module:hook("item-added/identity", clear_disco_cache);
+module:hook("item-added/feature", clear_disco_cache);
+
+-- 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);
@@ -75,6 +110,13 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve
origin.send(reply);
return true;
end);
+
+-- Handle caps stream feature
+module:hook("stream-features", function (event)
+ event.features:add_child(get_server_caps_feature());
+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
diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua
index 5f821cbc..7a876f1d 100644
--- a/plugins/mod_groups.lua
+++ b/plugins/mod_groups.lua
@@ -29,6 +29,9 @@ function inject_roster_contacts(username, host, roster)
if jid ~= bare_jid then
if not roster[jid] then roster[jid] = {}; end
roster[jid].subscription = "both";
+ if groups[group_name][jid] then
+ roster[jid].name = groups[group_name][jid];
+ end
if not roster[jid].groups then
roster[jid].groups = { [group_name] = true };
end
@@ -100,10 +103,13 @@ function module.load()
groups[curr_group] = groups[curr_group] or {};
else
-- Add JID
- local jid = jid_prep(line:match("%S+"));
+ local entryjid, name = line:match("([^=]*)=?(.*)");
+ module:log("debug", "entryjid = '%s', name = '%s'", entryjid, name);
+ local jid;
+ jid = jid_prep(entryjid:match("%S+"));
if jid then
module:log("debug", "New member of %s: %s", tostring(curr_group), tostring(jid));
- groups[curr_group][jid] = true;
+ groups[curr_group][jid] = name or false;
members[jid] = members[jid] or {};
members[jid][#members[jid]+1] = curr_group;
end
diff --git a/plugins/mod_httpserver.lua b/plugins/mod_httpserver.lua
index c55bd20f..654aff06 100644
--- a/plugins/mod_httpserver.lua
+++ b/plugins/mod_httpserver.lua
@@ -8,9 +8,11 @@
local httpserver = require "net.httpserver";
+local lfs = require "lfs";
local open = io.open;
local t_concat = table.concat;
+local stat = lfs.attributes;
local http_base = config.get("*", "core", "http_path") or "www_files";
@@ -48,7 +50,14 @@ local function preprocess_path(path)
end
function serve_file(path)
- local f, err = open(http_base..path, "rb");
+ local full_path = http_base..path;
+ if stat(full_path, "mode") == "directory" then
+ if stat(full_path.."/index.html", "mode") == "file" then
+ return serve_file(path.."/index.html");
+ end
+ return response_403;
+ end
+ local f, err = open(full_path, "rb");
if not f then return response_404; end
local data = f:read("*a");
f:close();
diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua
index b3001fe5..e90af781 100644
--- a/plugins/mod_iq.lua
+++ b/plugins/mod_iq.lua
@@ -9,7 +9,6 @@
local st = require "util.stanza";
local jid_split = require "util.jid".split;
-local user_exists = require "core.usermanager".user_exists;
local full_sessions = full_sessions;
local bare_sessions = bare_sessions;
@@ -34,16 +33,6 @@ module:hook("iq/bare", function(data)
-- IQ to bare JID recieved
local origin, stanza = data.origin, data.stanza;
- local to = stanza.attr.to;
- if to and not bare_sessions[to] then -- quick check for account existance
- local node, host = jid_split(to);
- if not user_exists(node, host) then -- full check for account existance
- if stanza.attr.type == "get" or stanza.attr.type == "set" then
- origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
- end
- return true;
- end
- end
-- TODO fire post processing events
if stanza.attr.type == "get" or stanza.attr.type == "set" then
return module:fire_event("iq/bare/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data);
diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua
index 0134d736..2e1ca328 100644
--- a/plugins/mod_legacyauth.lua
+++ b/plugins/mod_legacyauth.lua
@@ -50,7 +50,7 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:auth",
username = nodeprep(username);
resource = resourceprep(resource)
local reply = st.reply(stanza);
- if usermanager.validate_credentials(session.host, username, password) then
+ if usermanager.test_password(username, session.host, password) then
-- Authentication successful!
local success, err = sessionmanager.make_authenticated(session, username);
if success then
diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua
new file mode 100644
index 00000000..f323e606
--- /dev/null
+++ b/plugins/mod_motd.lua
@@ -0,0 +1,25 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- Copyright (C) 2010 Jeff Mitchell
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local host = module:get_host();
+local motd_text = module:get_option("motd_text") or "MOTD: (blank)";
+local motd_jid = module:get_option("motd_jid") or host;
+
+local st = require "util.stanza";
+
+module:hook("resource-bind",
+ function (event)
+ local session = event.session;
+ local motd_stanza =
+ st.message({ to = session.username..'@'..session.host, from = motd_jid })
+ :tag("body"):text(motd_text);
+ core_route_stanza(hosts[host], motd_stanza);
+ module:log("debug", "MOTD send to user %s@%s", session.username, session.host);
+
+end);
diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua
new file mode 100644
index 00000000..24aef9ed
--- /dev/null
+++ b/plugins/mod_offline.lua
@@ -0,0 +1,56 @@
+-- Prosody IM
+-- Copyright (C) 2008-2009 Matthew Wild
+-- Copyright (C) 2008-2009 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
+local datamanager = require "util.datamanager";
+local st = require "util.stanza";
+local datetime = require "util.datetime";
+local ipairs = ipairs;
+local jid_split = require "util.jid".split;
+
+module:add_feature("msgoffline");
+
+module:hook("message/offline/store", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ local to = stanza.attr.to;
+ local node, host;
+ if to then
+ node, host = jid_split(to)
+ else
+ node, host = origin.username, origin.host;
+ end
+
+ stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy();
+ local result = datamanager.list_append(node, host, "offline", st.preserialize(stanza));
+ stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
+
+ return true;
+end);
+
+module:hook("message/offline/broadcast", function(event)
+ local origin = event.origin;
+ local node, host = origin.username, origin.host;
+
+ local data = datamanager.list_load(node, host, "offline");
+ if not data then return true; end
+ for _, stanza in ipairs(data) do
+ stanza = st.deserialize(stanza);
+ stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = stanza.attr.stamp}):up(); -- XEP-0203
+ stanza:tag("x", {xmlns = "jabber:x:delay", from = host, stamp = stanza.attr.stamp_legacy}):up(); -- XEP-0091 (deprecated)
+ stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
+ origin.send(stanza);
+ end
+ return true;
+end);
+
+module:hook("message/offline/delete", function(event)
+ local origin = event.origin;
+ local node, host = origin.username, origin.host;
+
+ return datamanager.list_store(node, host, "offline", nil);
+end);
diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua
index aa46d2d3..5d476645 100644
--- a/plugins/mod_pep.lua
+++ b/plugins/mod_pep.lua
@@ -16,9 +16,7 @@ local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed
local pairs, ipairs = pairs, ipairs;
local next = next;
local type = type;
-local load_roster = require "core.rostermanager".load_roster;
-local sha1 = require "util.hashes".sha1;
-local base64 = require "util.encodings".base64.encode;
+local calculate_hash = require "util.caps".calculate_hash;
local NULL = {};
local data = {};
@@ -40,8 +38,8 @@ module:add_feature("http://jabber.org/protocol/pubsub#publish");
local function subscription_presence(user_bare, recipient)
local recipient_bare = jid_bare(recipient);
if (recipient_bare == user_bare) then return true end
- local item = load_roster(jid_split(user_bare))[recipient_bare];
- return item and (item.subscription == 'from' or item.subscription == 'both');
+ local username, host = jid_split(user_bare);
+ return is_contact_subscribed(username, host, recipient_bare);
end
local function publish(session, node, id, item)
@@ -118,27 +116,32 @@ module:hook("presence/bare", function(event)
-- inbound presence to bare JID recieved
local origin, stanza = event.origin, event.stanza;
local user = stanza.attr.to or (origin.username..'@'..origin.host);
+ local t = stanza.attr.type;
- if not stanza.attr.to or subscription_presence(user, stanza.attr.from) then
- local recipient = stanza.attr.from;
- local current = recipients[user] and recipients[user][recipient];
- local hash = get_caps_hash_from_presence(stanza, current);
- if current == hash then return; end
- if not hash then
- if recipients[user] then recipients[user][recipient] = nil; end
- else
- recipients[user] = recipients[user] or {};
- if hash_map[hash] then
- recipients[user][recipient] = hash_map[hash];
- publish_all(user, recipient, origin);
+ if not t then -- available presence
+ if not stanza.attr.to or subscription_presence(user, stanza.attr.from) then
+ local recipient = stanza.attr.from;
+ local current = recipients[user] and recipients[user][recipient];
+ local hash = get_caps_hash_from_presence(stanza, current);
+ if current == hash then return; end
+ if not hash then
+ if recipients[user] then recipients[user][recipient] = nil; end
else
- recipients[user][recipient] = hash;
- origin.send(
- st.stanza("iq", {from=stanza.attr.to, to=stanza.attr.from, id="disco", type="get"})
- :query("http://jabber.org/protocol/disco#info")
- );
+ recipients[user] = recipients[user] or {};
+ if hash_map[hash] then
+ recipients[user][recipient] = hash_map[hash];
+ publish_all(user, recipient, origin);
+ else
+ recipients[user][recipient] = hash;
+ origin.send(
+ st.stanza("iq", {from=stanza.attr.to, to=stanza.attr.from, id="disco", type="get"})
+ :query("http://jabber.org/protocol/disco#info")
+ );
+ end
end
end
+ elseif t == "unavailable" then
+ if recipients[user] then recipients[user][stanza.attr.from] = nil; end
end
end, 10);
@@ -205,50 +208,6 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event)
end
end);
-local function calculate_hash(disco_info)
- local identities, features, extensions = {}, {}, {};
- for _, tag in pairs(disco_info) do
- if tag.name == "identity" then
- table.insert(identities, (tag.attr.category or "").."\0"..(tag.attr.type or "").."\0"..(tag.attr["xml:lang"] or "").."\0"..(tag.attr.name or ""));
- elseif tag.name == "feature" then
- table.insert(features, tag.attr.var or "");
- elseif tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then
- local form = {};
- local FORM_TYPE;
- for _, field in pairs(tag.tags) do
- if field.name == "field" and field.attr.var then
- local values = {};
- for _, val in pairs(field.tags) do
- val = #val.tags == 0 and table.concat(val); -- FIXME use get_text?
- if val then table.insert(values, val); end
- end
- table.sort(values);
- if field.attr.var == "FORM_TYPE" then
- FORM_TYPE = values[1];
- elseif #values > 0 then
- table.insert(form, field.attr.var.."\0"..table.concat(values, "<"));
- else
- table.insert(form, field.attr.var);
- end
- end
- end
- table.sort(form);
- form = table.concat(form, "<");
- if FORM_TYPE then form = FORM_TYPE.."\0"..form; end
- table.insert(extensions, form);
- end
- end
- table.sort(identities);
- table.sort(features);
- table.sort(extensions);
- if #identities > 0 then identities = table.concat(identities, "<"):gsub("%z", "/").."<"; else identities = ""; end
- if #features > 0 then features = table.concat(features, "<").."<"; else features = ""; end
- if #extensions > 0 then extensions = table.concat(extensions, "<"):gsub("%z", "<").."<"; else extensions = ""; end
- local S = identities..features..extensions;
- local ver = base64(sha1(S));
- return ver, S;
-end
-
module:hook("iq/bare/disco", function(event)
local session, stanza = event.origin, event.stanza;
if stanza.attr.type == "result" then
@@ -285,8 +244,8 @@ module:hook("account-disco-info", function(event)
end);
module:hook("account-disco-items", function(event)
- local session, stanza = event.session, event.stanza;
- local bare = session.username..'@'..session.host;
+ local stanza = event.stanza;
+ local bare = stanza.attr.to;
local user_data = data[bare];
if user_data then
diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua
index c38f7eba..77b2f2a4 100644
--- a/plugins/mod_posix.lua
+++ b/plugins/mod_posix.lua
@@ -54,16 +54,16 @@ module:add_event_hook("server-started", function ()
end);
-- Don't even think about it!
-module:add_event_hook("server-starting", function ()
- local suid = module:get_option("setuid");
- if not suid or suid == 0 or suid == "root" then
- if pposix.getuid() == 0 and not module:get_option("run_as_root") then
- module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!");
- module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root");
- prosody.shutdown("Refusing to run as root");
- end
+if not prosody.start_time then -- server-starting
+ local suid = module:get_option("setuid");
+ if not suid or suid == 0 or suid == "root" then
+ if pposix.getuid() == 0 and not module:get_option("run_as_root") then
+ module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!");
+ module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root");
+ prosody.shutdown("Refusing to run as root");
end
- end);
+ end
+end
local pidfile;
local pidfile_handle;
@@ -95,8 +95,17 @@ local function write_pidfile()
pidfile_handle = nil;
prosody.shutdown("Prosody already running");
else
- pidfile_handle:write(tostring(pposix.getpid()));
- pidfile_handle:flush();
+ pidfile_handle:close();
+ pidfile_handle, err = io.open(pidfile, "w+");
+ if not pidfile_handle then
+ module:log("error", "Couldn't write pidfile at %s; %s", pidfile, err);
+ prosody.shutdown("Couldn't write pidfile");
+ else
+ if lfs.lock(pidfile_handle, "w") then
+ pidfile_handle:write(tostring(pposix.getpid()));
+ pidfile_handle:flush();
+ end
+ end
end
end
end
@@ -141,7 +150,9 @@ if daemonize then
write_pidfile();
end
end
- module:add_event_hook("server-starting", daemonize_server);
+ if not prosody.start_time then -- server-starting
+ daemonize_server();
+ end
else
-- Not going to daemonize, so write the pid of this process
write_pidfile();
diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua
index 4fb8c3e4..8e6ef85a 100644
--- a/plugins/mod_presence.lua
+++ b/plugins/mod_presence.lua
@@ -24,20 +24,6 @@ local rostermanager = require "core.rostermanager";
local sessionmanager = require "core.sessionmanager";
local offlinemanager = require "core.offlinemanager";
-local _core_route_stanza = core_route_stanza;
-local core_route_stanza;
-function core_route_stanza(origin, stanza)
- if stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" and stanza.attr.type ~= "error" then
- local node, host = jid_split(stanza.attr.to);
- host = hosts[host];
- if node and host and host.type == "local" then
- handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
- return;
- end
- end
- _core_route_stanza(origin, stanza);
-end
-
local function select_top_resources(user)
local priority = 0;
local recipients = {};
@@ -64,7 +50,7 @@ end
local ignore_presence_priority = module:get_option("ignore_presence_priority");
-function handle_normal_presence(origin, stanza, core_route_stanza)
+function handle_normal_presence(origin, stanza)
if ignore_presence_priority then
local priority = stanza:child_with_name("priority");
if priority and priority[1] ~= "0" then
@@ -82,13 +68,13 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
for _, res in pairs(user and user.sessions or NULL) do -- broadcast to all resources
if res ~= origin and res.presence then -- to resource
stanza.attr.to = res.full_jid;
- core_route_stanza(origin, stanza);
+ core_post_stanza(origin, stanza, true);
end
end
for jid, item in pairs(roster) do -- broadcast to all interested contacts
if item.subscription == "both" or item.subscription == "from" then
stanza.attr.to = jid;
- core_route_stanza(origin, stanza);
+ core_post_stanza(origin, stanza, true);
end
end
if stanza.attr.type == nil and not origin.presence then -- initial presence
@@ -97,13 +83,13 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
for jid, item in pairs(roster) do -- probe all contacts we are subscribed to
if item.subscription == "both" or item.subscription == "to" then
probe.attr.to = jid;
- core_route_stanza(origin, probe);
+ core_post_stanza(origin, probe, true);
end
end
for _, res in pairs(user and user.sessions or NULL) do -- broadcast from all available resources
if res ~= origin and res.presence then
res.presence.attr.to = origin.full_jid;
- core_route_stanza(res, res.presence);
+ core_post_stanza(res, res.presence, true);
res.presence.attr.to = nil;
end
end
@@ -116,7 +102,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
for jid, item in pairs(roster) do -- resend outgoing subscription requests
if item.ask then
request.attr.to = jid;
- core_route_stanza(origin, request);
+ core_post_stanza(origin, request, true);
end
end
local offline = offlinemanager.load(node, host);
@@ -136,7 +122,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
if origin.directed then
for jid in pairs(origin.directed) do
stanza.attr.to = jid;
- core_route_stanza(origin, stanza);
+ core_post_stanza(origin, stanza, true);
end
origin.directed = nil;
end
@@ -159,7 +145,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
stanza.attr.to = nil; -- reset it
end
-function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza, stanza)
+function send_presence_of_available_resources(user, host, jid, recipient_session, stanza)
local h = hosts[host];
local count = 0;
if h and h.type == "local" then
@@ -170,7 +156,7 @@ function send_presence_of_available_resources(user, host, jid, recipient_session
if pres then
if stanza then pres = stanza; pres.attr.from = session.full_jid; end
pres.attr.to = jid;
- core_route_stanza(session, pres);
+ core_post_stanza(session, pres, true);
pres.attr.to = nil;
count = count + 1;
end
@@ -181,26 +167,29 @@ function send_presence_of_available_resources(user, host, jid, recipient_session
return count;
end
-function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza)
+function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
local node, host = jid_split(from_bare);
- if to_bare == origin.username.."@"..origin.host then return; end -- No self contacts
+ if to_bare == from_bare then return; end -- No self contacts
local st_from, st_to = stanza.attr.from, stanza.attr.to;
stanza.attr.from, stanza.attr.to = from_bare, to_bare;
log("debug", "outbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);
- if stanza.attr.type == "subscribe" then
+ if stanza.attr.type == "probe" then
+ stanza.attr.from, stanza.attr.to = st_from, st_to;
+ return;
+ elseif stanza.attr.type == "subscribe" then
-- 1. route stanza
-- 2. roster push (subscription = none, ask = subscribe)
if rostermanager.set_contact_pending_out(node, host, to_bare) then
rostermanager.roster_push(node, host, to_bare);
end -- else file error
- core_route_stanza(origin, stanza);
+ core_post_stanza(origin, stanza);
elseif stanza.attr.type == "unsubscribe" then
-- 1. route stanza
-- 2. roster push (subscription = none or from)
if rostermanager.unsubscribe(node, host, to_bare) then
rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed?
end -- else file error
- core_route_stanza(origin, stanza);
+ core_post_stanza(origin, stanza);
elseif stanza.attr.type == "subscribed" then
-- 1. route stanza
-- 2. roster_push ()
@@ -208,20 +197,21 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_
if rostermanager.subscribed(node, host, to_bare) then
rostermanager.roster_push(node, host, to_bare);
end
- core_route_stanza(origin, stanza);
- send_presence_of_available_resources(node, host, to_bare, origin, core_route_stanza);
+ core_post_stanza(origin, stanza);
+ send_presence_of_available_resources(node, host, to_bare, origin);
elseif stanza.attr.type == "unsubscribed" then
-- 1. route stanza
-- 2. roster push (subscription = none or to)
if rostermanager.unsubscribed(node, host, to_bare) then
rostermanager.roster_push(node, host, to_bare);
end
- core_route_stanza(origin, stanza);
+ core_post_stanza(origin, stanza);
end
stanza.attr.from, stanza.attr.to = st_from, st_to;
+ return true;
end
-function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza)
+function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
local node, host = jid_split(to_bare);
local st_from, st_to = stanza.attr.from, stanza.attr.to;
stanza.attr.from, stanza.attr.to = from_bare, to_bare;
@@ -230,21 +220,21 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b
if stanza.attr.type == "probe" then
local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
if result then
- if 0 == send_presence_of_available_resources(node, host, st_from, origin, core_route_stanza) then
- core_route_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"})); -- TODO send last activity
+ if 0 == send_presence_of_available_resources(node, host, st_from, origin) then
+ core_post_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"}), true); -- TODO send last activity
end
elseif not err then
- core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}));
+ core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}), true);
end
elseif stanza.attr.type == "subscribe" then
if rostermanager.is_contact_subscribed(node, host, from_bare) then
- core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"})); -- already subscribed
+ core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"}), true); -- already subscribed
-- Sending presence is not clearly stated in the RFC, but it seems appropriate
- if 0 == send_presence_of_available_resources(node, host, from_bare, origin, core_route_stanza) then
- core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- TODO send last activity
+ if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then
+ core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- TODO send last activity
end
else
- core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- acknowledging receipt
+ core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt
if not rostermanager.is_contact_pending_in(node, host, from_bare) then
if rostermanager.set_contact_pending_in(node, host, from_bare) then
sessionmanager.send_to_available_resources(node, host, stanza);
@@ -268,6 +258,7 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b
end
end -- discard any other type
stanza.attr.from, stanza.attr.to = st_from, st_to;
+ return true;
end
local outbound_presence_handler = function(data)
@@ -278,12 +269,12 @@ local outbound_presence_handler = function(data)
if to then
local t = stanza.attr.type;
if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes
- handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
- return true;
+ return handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
end
local to_bare = jid_bare(to);
- if not(origin.roster[to_bare] and (origin.roster[to_bare].subscription == "both" or origin.roster[to_bare].subscription == "from")) then -- directed presence
+ local roster = origin.roster;
+ if roster and not(roster[to_bare] and (roster[to_bare].subscription == "both" or roster[to_bare].subscription == "from")) then -- directed presence
origin.directed = origin.directed or {};
if t then -- removing from directed presence list on sending an error or unavailable
origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to?
@@ -306,8 +297,7 @@ module:hook("presence/bare", function(data)
local t = stanza.attr.type;
if to then
if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID
- handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
- return true;
+ return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
end
local user = bare_sessions[to];
@@ -319,7 +309,7 @@ module:hook("presence/bare", function(data)
end
end -- no resources not online, discard
elseif not t or t == "unavailable" then
- handle_normal_presence(origin, stanza, core_route_stanza);
+ handle_normal_presence(origin, stanza);
end
return true;
end);
@@ -329,8 +319,7 @@ module:hook("presence/full", function(data)
local t = stanza.attr.type;
if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID
- handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
- return true;
+ return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
end
local session = full_sessions[stanza.attr.to];
@@ -347,10 +336,10 @@ module:hook("presence/host", function(data)
local from_bare = jid_bare(stanza.attr.from);
local t = stanza.attr.type;
if t == "probe" then
- core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
+ core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
elseif t == "subscribe" then
- core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" }));
- core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
+ core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" }));
+ core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
end
return true;
end);
@@ -369,7 +358,7 @@ module:hook("resource-unbind", function(event)
pres:tag("status"):text("Disconnected: "..err):up();
for jid in pairs(session.directed) do
pres.attr.to = jid;
- core_route_stanza(session, pres);
+ core_post_stanza(session, pres, true);
end
session.directed = nil;
end
diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua
index aa953310..5355e527 100644
--- a/plugins/mod_privacy.lua
+++ b/plugins/mod_privacy.lua
@@ -442,7 +442,9 @@ function preCheckOutgoing(e)
e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource;
end
end
- return checkIfNeedToBeBlocked(e, session);
+ if session.username then -- FIXME do properly
+ return checkIfNeedToBeBlocked(e, session);
+ end
end
module:hook("pre-message/full", preCheckOutgoing, 500);
diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua
index 190d30be..61359444 100644
--- a/plugins/mod_proxy65.lua
+++ b/plugins/mod_proxy65.lua
@@ -14,7 +14,7 @@ if module:get_host_type() ~= "component" then
error("proxy65 should be loaded as a component, please see http://prosody.im/doc/components", 0);
end
-local jid_split, jid_join = require "util.jid".split, require "util.jid".join;
+local jid_split, jid_join, jid_compare = require "util.jid".split, require "util.jid".join, require "util.jid".compare;
local st = require "util.stanza";
local componentmanager = require "core.componentmanager";
local config_get = require "core.configmanager".get;
@@ -151,24 +151,11 @@ local function get_stream_host(origin, stanza)
local err_reply = replies_cache.stream_host_err;
local sid = stanza.tags[1].attr.sid;
local allow = false;
- local jid_node, jid_host, jid_resource = jid_split(stanza.attr.from);
-
- if stanza.attr.from == nil then
- jid_node = origin.username;
- jid_host = origin.host;
- jid_resource = origin.resource;
- end
+ local jid = stanza.attr.from;
if proxy_acl and #proxy_acl > 0 then
- if host ~= nil then -- at least a domain is needed.
- for _, acl in ipairs(proxy_acl) do
- local acl_node, acl_host, acl_resource = jid_split(acl);
- if ((acl_node ~= nil and acl_node == jid_node) or acl_node == nil) and
- ((acl_host ~= nil and acl_host == jid_host) or acl_host == nil) and
- ((acl_resource ~= nil and acl_resource == jid_resource) or acl_resource == nil) then
- allow = true;
- end
- end
+ for _, acl in ipairs(proxy_acl) do
+ if jid_compare(jid, acl) then allow = true; end
end
else
allow = true;
@@ -181,7 +168,7 @@ local function get_stream_host(origin, stanza)
replies_cache.stream_host = reply;
end
else
- module:log("warn", "Denying use of proxy for %s", tostring(jid_join(jid_node, jid_host, jid_resource)));
+ module:log("warn", "Denying use of proxy for %s", tostring(jid));
if err_reply == nil then
err_reply = st.iq({type="error", from=host})
:query("http://jabber.org/protocol/bytestreams")
diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua
index 2818e336..25f47a9e 100644
--- a/plugins/mod_register.lua
+++ b/plugins/mod_register.lua
@@ -13,7 +13,6 @@ local datamanager = require "util.datamanager";
local usermanager_user_exists = require "core.usermanager".user_exists;
local usermanager_create_user = require "core.usermanager".create_user;
local usermanager_set_password = require "core.usermanager".set_password;
-local datamanager_store = require "util.datamanager".store;
local os_time = os.time;
local nodeprep = require "util.encodings".stringprep.nodeprep;
@@ -35,7 +34,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza)
local username, host = session.username, session.host;
--session.send(st.error_reply(stanza, "cancel", "not-allowed"));
--return;
- --usermanager_set_password(username, host, nil); -- Disable account
+ usermanager_set_password(username, nil, host); -- Disable account
-- FIXME the disabling currently allows a different user to recreate the account
-- we should add an in-memory account block mode when we have threading
session.send(st.reply(stanza));
@@ -71,7 +70,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza)
username = nodeprep(table.concat(username));
password = table.concat(password);
if username == session.username then
- if usermanager_set_password(username, session.host, password) then
+ if usermanager_set_password(username, password, session.host) then
session.send(st.reply(stanza));
else
-- TODO unable to write file, file may be locked, etc, what's the correct error?
diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua
index ddf02f2f..af92e24e 100644
--- a/plugins/mod_roster.lua
+++ b/plugins/mod_roster.lua
@@ -42,15 +42,15 @@ module:add_iq_handler("c2s", "jabber:iq:roster",
if not (client_ver and server_ver) or client_ver ~= server_ver then
roster:query("jabber:iq:roster");
-- Client does not support versioning, or has stale roster
- for jid in pairs(session.roster) do
+ for jid, item in pairs(session.roster) do
if jid ~= "pending" and jid then
roster:tag("item", {
jid = jid,
- subscription = session.roster[jid].subscription,
- ask = session.roster[jid].ask,
- name = session.roster[jid].name,
+ subscription = item.subscription,
+ ask = item.ask,
+ name = item.name,
});
- for group in pairs(session.roster[jid].groups) do
+ for group in pairs(item.groups) do
roster:tag("group"):text(group):up();
end
roster:up(); -- move out from item
diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua
index d407e5da..a02c1ec4 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -14,19 +14,14 @@ local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
local base64 = require "util.encodings".base64;
local nodeprep = require "util.encodings".stringprep.nodeprep;
-local datamanager_load = require "util.datamanager".load;
-local usermanager_validate_credentials = require "core.usermanager".validate_credentials;
-local usermanager_get_supported_methods = require "core.usermanager".get_supported_methods;
+local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler;
local usermanager_user_exists = require "core.usermanager".user_exists;
-local usermanager_get_password = require "core.usermanager".get_password;
local t_concat, t_insert = table.concat, table.insert;
local tostring = tostring;
-local jid_split = require "util.jid".split;
-local md5 = require "util.hashes".md5;
-local config = require "core.configmanager";
local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
local sasl_backend = module:get_option("sasl_backend") or "builtin";
+local anonymous_login = module:get_option("anonymous_login");
-- Cyrus config options
local require_provisioning = module:get_option("cyrus_require_provisioning") or false;
@@ -66,21 +61,6 @@ else
error("Unknown SASL backend");
end
-local default_authentication_profile = {
- plain = function(username, realm)
- local prepped_username = nodeprep(username);
- if not prepped_username then
- log("debug", "NODEprep failed on username: %s", username);
- return "", nil;
- end
- local password = usermanager_get_password(prepped_username, realm);
- if not password then
- return "", nil;
- end
- return password, true;
- end
-};
-
local anonymous_authentication_profile = {
anonymous = function(username, realm)
return true; -- for normal usage you should always return true here
@@ -111,8 +91,8 @@ local function handle_status(session, status, ret, err_msg)
local username = nodeprep(session.sasl_handler.username);
if not(require_provisioning) or usermanager_user_exists(username, session.host) then
- local aret, err = sm_make_authenticated(session, session.sasl_handler.username);
- if aret then
+ local ok, err = sm_make_authenticated(session, session.sasl_handler.username);
+ if ok then
session.sasl_handler = nil;
session:reset_stream();
else
@@ -132,7 +112,7 @@ end
local function sasl_handler(session, stanza)
if stanza.name == "auth" then
-- FIXME ignoring duplicates because ejabberd does
- if config.get(session.host or "*", "core", "anonymous_login") then
+ if anonymous_login then
if stanza.attr.mechanism ~= "ANONYMOUS" then
return session.send(build_reply("failure", "invalid-mechanism"));
end
@@ -179,18 +159,17 @@ module:hook("stream-features", function(event)
if secure_auth_only and not origin.secure then
return;
end
- local realm = module:get_option("sasl_realm") or origin.host;
- if module:get_option("anonymous_login") then
- origin.sasl_handler = new_sasl(realm, anonymous_authentication_profile);
+ if anonymous_login then
+ origin.sasl_handler = new_sasl(module.host, anonymous_authentication_profile);
else
- origin.sasl_handler = new_sasl(realm, default_authentication_profile);
+ origin.sasl_handler = usermanager_get_sasl_handler(module.host);
if not (module:get_option("allow_unencrypted_plain_auth")) and not origin.secure then
origin.sasl_handler:forbidden({"PLAIN"});
end
end
features:tag("mechanisms", mechanisms_attr);
- for k, v in pairs(origin.sasl_handler:mechanisms()) do
- features:tag("mechanism"):text(v):up();
+ for k in pairs(origin.sasl_handler:mechanisms()) do
+ features:tag("mechanism"):text(k):up();
end
features:up();
else
diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua
index 8b96aa15..a2667ff6 100644
--- a/plugins/mod_tls.lua
+++ b/plugins/mod_tls.lua
@@ -83,7 +83,7 @@ module:hook_stanza(xmlns_starttls, "proceed", function (session, stanza)
module:log("debug", "Proceeding with TLS on s2sout...");
session:reset_stream();
local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx;
- session.conn:starttls(ssl_ctx, true);
+ session.conn:starttls(ssl_ctx);
session.secure = false;
return true;
end);
diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua
index 24d10180..c3860af6 100644
--- a/plugins/mod_uptime.lua
+++ b/plugins/mod_uptime.lua
@@ -11,6 +11,7 @@ local st = require "util.stanza";
local start_time = prosody.start_time;
prosody.events.add_handler("server-started", function() start_time = prosody.start_time end);
+-- XEP-0012: Last activity
module:add_feature("jabber:iq:last");
module:hook("iq/host/jabber:iq:last:query", function(event)
@@ -20,3 +21,28 @@ module:hook("iq/host/jabber:iq:last:query", function(event)
return true;
end
end);
+
+-- Ad-hoc command
+local adhoc_new = module:require "adhoc".new;
+
+function uptime_text()
+ local t = os.time()-prosody.start_time;
+ local seconds = t%60;
+ t = (t - seconds)/60;
+ local minutes = t%60;
+ t = (t - minutes)/60;
+ local hours = t%24;
+ t = (t - hours)/24;
+ local days = t;
+ return string.format("This server has been running for %d day%s, %d hour%s and %d minute%s (since %s)",
+ days, (days ~= 1 and "s") or "", hours, (hours ~= 1 and "s") or "",
+ minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time));
+end
+
+function uptime_command_handler (self, data, state)
+ return { info = uptime_text(), status = "completed" };
+end
+
+local descriptor = adhoc_new("Get uptime", "uptime", uptime_command_handler);
+
+module:add_item ("adhoc", descriptor);
diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua
index de23aebb..a9d9336c 100644
--- a/plugins/muc/mod_muc.lua
+++ b/plugins/muc/mod_muc.lua
@@ -31,8 +31,11 @@ local rooms = {};
local persistent_rooms = datamanager.load(nil, muc_host, "persistent") or {};
local component;
+-- Configurable options
+local max_history_messages = module:get_option_number("max_history_messages");
+
local function is_admin(jid)
- return um_is_admin(jid) or um_is_admin(jid, module.host);
+ return um_is_admin(jid, module.host);
end
local function room_route_stanza(room, stanza) core_post_stanza(component, stanza); end
@@ -58,15 +61,20 @@ end
for jid in pairs(persistent_rooms) do
local node = jid_split(jid);
local data = datamanager.load(node, muc_host, "config") or {};
- local room = muc_new_room(jid);
+ local room = muc_new_room(jid, {
+ history_length = max_history_messages;
+ });
room._data = data._data;
+ room._data.history_length = max_history_messages; --TODO: Need to allow per-room with a global limit
room._affiliations = data._affiliations;
room.route_stanza = room_route_stanza;
room.save = room_save;
rooms[jid] = room;
end
-local host_room = muc_new_room(muc_host);
+local host_room = muc_new_room(muc_host, {
+ history_length = max_history_messages;
+});
host_room.route_stanza = room_route_stanza;
host_room.save = room_save;
@@ -78,7 +86,7 @@ end
local function get_disco_items(stanza)
local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
for jid, room in pairs(rooms) do
- if not room._data.hidden then
+ if not room:is_hidden() then
reply:tag("item", {jid=jid, name=jid}):up();
end
end
@@ -113,7 +121,9 @@ component = register_component(muc_host, function(origin, stanza)
local room = rooms[bare];
if not room then
if not(restrict_room_creation) or is_admin(stanza.attr.from) then
- room = muc_new_room(bare);
+ room = muc_new_room(bare, {
+ history_length = max_history_messages;
+ });
room.route_stanza = room_route_stanza;
room.save = room_save;
rooms[bare] = room;
diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua
index 18c80325..9cdf4d18 100644
--- a/plugins/muc/muc.lib.lua
+++ b/plugins/muc/muc.lib.lua
@@ -6,6 +6,9 @@
-- COPYING file in the source package for more information.
--
+local select = select;
+local pairs, ipairs = pairs, ipairs;
+
local datamanager = require "util.datamanager";
local datetime = require "util.datetime";
@@ -21,7 +24,7 @@ local base64 = require "util.encodings".base64;
local md5 = require "util.hashes".md5;
local muc_domain = nil; --module:get_host();
-local history_length = 20;
+local default_history_length = 20;
------------
local function filter_xmlns_from_array(array, filters)
@@ -88,8 +91,12 @@ room_mt.__index = room_mt;
function room_mt:get_default_role(affiliation)
if affiliation == "owner" or affiliation == "admin" then
return "moderator";
- elseif affiliation == "member" or not affiliation then
+ elseif affiliation == "member" then
return "participant";
+ elseif not affiliation then
+ if not self:is_members_only() then
+ return self:is_moderated() and "visitor" or "participant";
+ end
end
end
@@ -122,10 +129,14 @@ function room_mt:broadcast_message(stanza, historic)
local history = self._data['history'];
if not history then history = {}; self._data['history'] = history; end
stanza = st.clone(stanza);
- stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203
+ stanza.attr.to = "";
+ local stamp = datetime.datetime();
+ local chars = #tostring(stanza);
+ stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203
stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
- t_insert(history, st.preserialize(stanza));
- while #history > history_length do t_remove(history, 1) end
+ local entry = { stanza = stanza, stamp = stamp };
+ t_insert(history, entry);
+ while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end
end
end
function room_mt:broadcast_except_nick(stanza, nick)
@@ -151,24 +162,65 @@ function room_mt:send_occupant_list(to)
end
end
end
-function room_mt:send_history(to)
+function room_mt:send_history(to, stanza)
local history = self._data['history']; -- send discussion history
if history then
- for _, msg in ipairs(history) do
- msg = st.deserialize(msg);
- msg.attr.to=to;
+ local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc");
+ local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc");
+
+ local maxchars = history_tag and tonumber(history_tag.attr.maxchars);
+ if maxchars then maxchars = math.floor(maxchars); end
+
+ local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history);
+ if not history_tag then maxstanzas = 20; end
+
+ local seconds = history_tag and tonumber(history_tag.attr.seconds);
+ if seconds then seconds = datetime.datetime(os.time() - math.floor(seconds)); end
+
+ local since = history_tag and history_tag.attr.since;
+ if since and not since:match("^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%dZ$") then since = nil; end -- FIXME timezone support
+ if seconds and (not since or since < seconds) then since = seconds; end
+
+ local n = 0;
+ local charcount = 0;
+ local stanzacount = 0;
+
+ for i=#history,1,-1 do
+ local entry = history[i];
+ if maxchars then
+ if not entry.chars then
+ entry.stanza.attr.to = "";
+ entry.chars = #tostring(entry.stanza);
+ end
+ charcount = charcount + entry.chars + #to;
+ if charcount > maxchars then break; end
+ end
+ if since and since > entry.stamp then break; end
+ if n + 1 > maxstanzas then break; end
+ n = n + 1;
+ end
+ for i=#history-n+1,#history do
+ local msg = history[i].stanza;
+ msg.attr.to = to;
self:_route_stanza(msg);
end
end
if self._data['subject'] then
- self:_route_stanza(st.message({type='groupchat', from=self.jid, to=to}):tag("subject"):text(self._data['subject']));
+ self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject']));
end
end
function room_mt:get_disco_info(stanza)
return st.reply(stanza):query("http://jabber.org/protocol/disco#info")
:tag("identity", {category="conference", type="text"}):up()
- :tag("feature", {var="http://jabber.org/protocol/muc"});
+ :tag("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()
+ ;
end
function room_mt:get_disco_items(stanza)
local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
@@ -181,6 +233,7 @@ function room_mt:set_subject(current_nick, subject)
-- TODO check nick's authority
if subject == "" then subject = nil; end
self._data['subject'] = subject;
+ self._data['subject_from'] = current_nick;
if self.save then self:save(); end
local msg = st.message({type='groupchat', from=current_nick})
:tag('subject'):text(subject):up();
@@ -198,6 +251,57 @@ local function build_unavailable_presence_from_error(stanza)
:tag('status'):text(error_message);
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:handle_to_occupant(origin, stanza) -- PM, vCards, etc
local from, to = stanza.attr.from, stanza.attr.to;
local room = jid_bare(to);
@@ -290,7 +394,15 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
end
is_merge = true;
end
- if not new_nick then
+ local password = stanza:get_child("x", "http://jabber.org/protocol/muc");
+ password = password and password:get_child("password", "http://jabber.org/protocol/muc");
+ password = password and password[1] ~= "" and password[1];
+ if self:get_password() and self:get_password() ~= password then
+ log("debug", "%s couldn't join due to invalid password: %s", from, to);
+ local reply = st.error_reply(stanza, "auth", "not-authorized"):up();
+ reply.tags[1].attr.code = "401";
+ origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ elseif not new_nick then
log("debug", "%s couldn't join due to nick conflict: %s", from, to);
local reply = st.error_reply(stanza, "cancel", "conflict"):up();
reply.tags[1].attr.code = "409";
@@ -319,12 +431,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
:tag("item", {affiliation=affiliation or "none", role=role or "none"}):up()
:tag("status", {code='110'}));
end
- if self._data.whois == 'anyone' then -- non-anonymous?
- self:_route_stanza(st.stanza("message", {from=to, to=from, type='groupchat'})
- :tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
- :tag("status", {code='100'}));
- end
- self:send_history(from);
+ self:send_history(from, stanza);
else -- banned
local reply = st.error_reply(stanza, "auth", "forbidden"):up();
reply.tags[1].attr.code = "403";
@@ -392,10 +499,10 @@ function room_mt:send_form(origin, stanza)
:tag("instructions"):text(title):up()
:tag("field", {type='hidden', var='FORM_TYPE'}):tag("value"):text("http://jabber.org/protocol/muc#roomconfig"):up():up()
:tag("field", {type='boolean', label='Make Room Persistent?', var='muc#roomconfig_persistentroom'})
- :tag("value"):text(self._data.persistent and "1" or "0"):up()
+ :tag("value"):text(self:is_persistent() and "1" or "0"):up()
:up()
:tag("field", {type='boolean', label='Make Room Publicly Searchable?', var='muc#roomconfig_publicroom'})
- :tag("value"):text(self._data.hidden and "0" or "1"):up()
+ :tag("value"):text(self:is_hidden() and "0" or "1"):up()
:up()
:tag("field", {type='list-single', label='Who May Discover Real JIDs?', var='muc#roomconfig_whois'})
:tag("value"):text(self._data.whois or 'moderators'):up()
@@ -406,6 +513,15 @@ function room_mt:send_form(origin, stanza)
:tag("value"):text('anyone'):up()
:up()
:up()
+ :tag("field", {type='text-private', label='Password', var='muc#roomconfig_roomsecret'})
+ :tag("value"):text(self:get_password() or ""):up()
+ :up()
+ :tag("field", {type='boolean', label='Make Room Moderated?', var='muc#roomconfig_moderatedroom'})
+ :tag("value"):text(self:is_moderated() and "1" or "0"):up()
+ :up()
+ :tag("field", {type='boolean', label='Make Room Members-Only?', var='muc#roomconfig_membersonly'})
+ :tag("value"):text(self:is_members_only() and "1" or "0"):up()
+ :up()
);
end
@@ -434,15 +550,25 @@ function room_mt:process_form(origin, stanza)
local persistent = fields['muc#roomconfig_persistentroom'];
if persistent == "0" or persistent == "false" then persistent = nil; elseif persistent == "1" or persistent == "true" then persistent = true;
else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
- dirty = dirty or (self._data.persistent ~= persistent)
- self._data.persistent = persistent;
+ dirty = dirty or (self:is_persistent() ~= persistent)
module:log("debug", "persistent=%s", tostring(persistent));
+ local moderated = fields['muc#roomconfig_moderatedroom'];
+ if moderated == "0" or moderated == "false" then moderated = nil; elseif moderated == "1" or moderated == "true" then moderated = true;
+ else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ dirty = dirty or (self:is_moderated() ~= moderated)
+ module:log("debug", "moderated=%s", tostring(moderated));
+
+ local membersonly = fields['muc#roomconfig_membersonly'];
+ if membersonly == "0" or membersonly == "false" then membersonly = nil; elseif membersonly == "1" or membersonly == "true" then membersonly = true;
+ else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ dirty = dirty or (self:is_members_only() ~= membersonly)
+ module:log("debug", "membersonly=%s", tostring(membersonly));
+
local public = fields['muc#roomconfig_publicroom'];
if public == "0" or public == "false" then public = nil; elseif public == "1" or public == "true" then public = true;
else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
- dirty = dirty or (self._data.hidden ~= (not public and true or nil))
- self._data.hidden = not public and true or nil;
+ dirty = dirty or (self:is_hidden() ~= (not public and true or nil))
local whois = fields['muc#roomconfig_whois'];
if not valid_whois[whois] then
@@ -451,7 +577,16 @@ function room_mt:process_form(origin, stanza)
end
local whois_changed = self._data.whois ~= whois
self._data.whois = whois
- module:log('debug', 'whois=%s', tostring(whois))
+ module:log('debug', 'whois=%s', whois)
+
+ local password = fields['muc#roomconfig_roomsecret'];
+ if 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);
if self.save then self:save(true); end
origin.send(st.reply(stanza));
@@ -488,8 +623,7 @@ function room_mt:destroy(newjid, reason, password)
end
self._occupants[nick] = nil;
end
- self._data.persistent = nil;
- if self.save then self:save(true); end
+ self:set_persistent(false);
end
function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
@@ -654,8 +788,11 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
:tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
:tag('invite', {from=_from})
:tag('reason'):text(_reason or ""):up()
- :up()
- :up()
+ :up();
+ if self:get_password() then
+ invite:tag("password"):text(self:get_password()):up();
+ end
+ invite:up()
:tag('x', {xmlns="jabber:x:conference", jid=_to}) -- COMPAT: Some older clients expect this
:text(_reason or "")
:up()
@@ -748,13 +885,29 @@ function room_mt:get_role(nick)
local session = self._occupants[nick];
return session and session.role or nil;
end
+function room_mt:can_set_role(actor_jid, occupant_jid, role)
+ local actor = self._occupants[self._jid_nick[actor_jid]];
+ local occupant = self._occupants[occupant_jid];
+
+ if not occupant or not actor then return nil, "modify", "not-acceptable"; end
+
+ if actor.role == "moderator" then
+ if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then
+ if actor.affiliation == "owner" or actor.affiliation == "admin" then
+ return true;
+ elseif occupant.role ~= "moderator" and role ~= "moderator" then
+ return true;
+ end
+ end
+ end
+ return nil, "cancel", "not-allowed";
+end
function room_mt:set_role(actor, occupant_jid, role, callback, reason)
if role == "none" then role = nil; end
if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end
- if self:get_role(self._jid_nick[actor]) ~= "moderator" then return nil, "cancel", "not-allowed"; end
+ local allowed, err_type, err_condition = self:can_set_role(actor, occupant_jid, role);
+ if not allowed then return allowed, err_type, err_condition; end
local occupant = self._occupants[occupant_jid];
- if not occupant then return nil, "modify", "not-acceptable"; end
- if occupant.affiliation == "owner" or occupant.affiliation == "admin" then return nil, "cancel", "not-allowed"; end
local p = st.presence({from = occupant_jid})
:tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
:tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"})
@@ -804,6 +957,9 @@ function room_mt:_route_stanza(stanza)
end
end
end
+ if self._data.whois == 'anyone' then
+ muc_child:tag('status', { code = '100' });
+ end
end
self:route_stanza(stanza);
if muc_child then
@@ -817,13 +973,14 @@ end
local _M = {}; -- module "muc"
-function _M.new_room(jid)
+function _M.new_room(jid, config)
return setmetatable({
jid = jid;
_jid_nick = {};
_occupants = {};
_data = {
- whois = 'moderators',
+ whois = 'moderators';
+ history_length = (config and config.history_length);
};
_affiliations = {};
}, room_mt);
diff --git a/plugins/storage/ejabberdstore.lib.lua b/plugins/storage/ejabberdstore.lib.lua
new file mode 100644
index 00000000..7e8592a8
--- /dev/null
+++ b/plugins/storage/ejabberdstore.lib.lua
@@ -0,0 +1,190 @@
+
+local handlers = {};
+
+handlers.accounts = {
+ get = function(self, user)
+ local select = self:query("select password from users where username=?", user);
+ local row = select and select:fetch();
+ if row then return { password = row[1] }; end
+ end;
+ set = function(self, user, data)
+ if data and data.password then
+ return self:modify("update users set password=? where username=?", data.password, user)
+ or self:modify("insert into users (username, password) values (?, ?)", user, data.password);
+ else
+ return self:modify("delete from users where username=?", user);
+ end
+ end;
+};
+handlers.vcard = {
+ get = function(self, user)
+ local select = self:query("select vcard from vcard where username=?", user);
+ local row = select and select:fetch();
+ if row then return parse_xml(row[1]); end
+ end;
+ set = function(self, user, data)
+ if data then
+ data = unparse_xml(data);
+ return self:modify("update vcard set vcard=? where username=?", data, user)
+ or self:modify("insert into vcard (username, vcard) values (?, ?)", user, data);
+ else
+ return self:modify("delete from vcard where username=?", user);
+ end
+ end;
+};
+handlers.private = {
+ get = function(self, user)
+ local select = self:query("select namespace,data from private_storage where username=?", user);
+ if select then
+ local data = {};
+ for row in select:rows() do
+ data[row[1]] = parse_xml(row[2]);
+ end
+ return data;
+ end
+ end;
+ set = function(self, user, data)
+ if data then
+ self:modify("delete from private_storage where username=?", user);
+ for namespace,text in pairs(data) do
+ self:modify("insert into private_storage (username, namespace, data) values (?, ?, ?)", user, namespace, unparse_xml(text));
+ end
+ return true;
+ else
+ return self:modify("delete from private_storage where username=?", user);
+ end
+ end;
+ -- TODO map_set, map_get
+};
+local subscription_map = { N = "none", B = "both", F = "from", T = "to" };
+local subscription_map_reverse = { none = "N", both = "B", from = "F", to = "T" };
+handlers.roster = {
+ get = function(self, user)
+ local select = self:query("select jid,nick,subscription,ask,server,subscribe,type from rosterusers where username=?", user);
+ if select then
+ local roster = { pending = {} };
+ for row in select:rows() do
+ local jid,nick,subscription,ask,server,subscribe,typ = unpack(row);
+ local item = { groups = {} };
+ if nick == "" then nick = nil; end
+ item.nick = nick;
+ item.subscription = subscription_map[subscription];
+ if ask == "N" then ask = nil;
+ elseif ask == "O" then ask = "subscribe"
+ elseif ask == "I" then roster.pending[jid] = true; ask = nil;
+ elseif ask == "B" then roster.pending[jid] = true; ask = "subscribe";
+ else module:log("debug", "bad roster_item.ask: %s", ask); ask = nil; end
+ item.ask = ask;
+ roster[jid] = item;
+ end
+
+ select = self:query("select jid,grp from rostergroups where username=?", user);
+ if select then
+ for row in select:rows() do
+ local jid,grp = unpack(rows);
+ if roster[jid] then roster[jid].groups[grp] = true; end
+ end
+ end
+ select = self:query("select version from roster_version where username=?", user);
+ local row = select and select:fetch();
+ if row then
+ roster[false] = { version = row[1]; };
+ end
+ return roster;
+ end
+ end;
+ set = function(self, user, data)
+ if data and next(data) ~= nil then
+ self:modify("delete from rosterusers where username=?", user);
+ self:modify("delete from rostergroups where username=?", user);
+ self:modify("delete from roster_version where username=?", user);
+ local done = {};
+ local pending = data.pending or {};
+ for jid,item in pairs(data) do
+ if jid and jid ~= "pending" then
+ local subscription = subscription_map_reverse[item.subscription];
+ local ask;
+ if pending[jid] then
+ if item.ask then ask = "B"; else ask = "I"; end
+ else
+ if item.ask then ask = "O"; else ask = "N"; end
+ end
+ local r = self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?, '', '', '')", user, jid, item.nick or "", subscription, ask);
+ if not r then module:log("debug", "--- :( %s", tostring(r)); end
+ done[jid] = true;
+ for group in pairs(item.groups) do
+ self:modify("insert into rostergroups (username,jid,grp) values (?, ?, ?)", user, jid, group);
+ end
+ end
+ end
+ for jid in pairs(pending) do
+ if not done[jid] then
+ self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?. ''. ''. '')", user, jid, "", "N", "I");
+ end
+ end
+ local version = data[false] and data[false].version;
+ if version then
+ self:modify("insert into roster_version (username,version) values (?, ?)", user, version);
+ end
+ return true;
+ else
+ self:modify("delete from rosterusers where username=?", user);
+ self:modify("delete from rostergroups where username=?", user);
+ self:modify("delete from roster_version where username=?", user);
+ end
+ end;
+};
+
+-----------------------------
+local driver = {};
+driver.__index = driver;
+
+function driver:prepare(sql)
+ module:log("debug", "query: %s", sql);
+ local err;
+ if not self.sqlcache then self.sqlcache = {}; end
+ local r = self.sqlcache[sql];
+ if r then return r; end
+ r, err = self.database:prepare(sql);
+ if not r then error("Unable to prepare SQL statement: "..err); end
+ self.sqlcache[sql] = r;
+ return r;
+end
+
+function driver:query(sql, ...)
+ local stmt = self:prepare(sql);
+ if stmt:execute(...) then return stmt; end
+end
+function driver:modify(sql, ...)
+ local stmt = self:query(sql, ...);
+ if stmt and stmt:affected() > 0 then return stmt; end
+end
+
+function driver:open(host, datastore, typ)
+ local cache_key = host.." "..datastore;
+ if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
+ local instance = setmetatable({}, self);
+ instance.host = host;
+ instance.datastore = datastore;
+ local handler = handlers[datastore];
+ if not handler then return nil; end
+ for key,val in pairs(handler) do
+ instance[key] = val;
+ end
+ if instance.init then instance:init(); end
+ self.ds_cache[cache_key] = instance;
+ return instance;
+end
+
+-----------------------------
+local _M = {};
+
+function _M.new(dbtype, dbname, ...)
+ local instance = setmetatable({}, driver);
+ instance.__index = instance;
+ instance.database = get_database(dbtype, dbname, ...);
+ instance.ds_cache = {};
+ return instance;
+end
+
+return _M;
diff --git a/plugins/storage/mod_storage.lua b/plugins/storage/mod_storage.lua
new file mode 100644
index 00000000..e9f5f923
--- /dev/null
+++ b/plugins/storage/mod_storage.lua
@@ -0,0 +1,83 @@
+
+module:set_global();
+
+local cache = { data = {} };
+function cache:get(key) return self.data[key]; end
+function cache:set(key, val) self.data[key] = val; return val; end
+
+local _,DBI = pcall(require, "DBI");
+function get_database(driver, db, ...)
+ local uri = "dbi:"..driver..":"..db;
+ return cache:get(uri) or cache:set(uri, (function(...)
+ module:log("debug", "Opening database: %s", uri);
+ prosody.unlock_globals();
+ local dbh = assert(DBI.Connect(...));
+ prosody.lock_globals();
+ dbh:autocommit(true)
+ return dbh;
+ end)(driver, db, ...));
+end
+
+local st = require "util.stanza";
+local _parse_xml = module:require("xmlparse");
+parse_xml_real = _parse_xml;
+function parse_xml(str)
+ local s = _parse_xml(str);
+ if s and not s.gsub then
+ return st.preserialize(s);
+ end
+end
+function unparse_xml(s)
+ return tostring(st.deserialize(s));
+end
+
+local drivers = {};
+
+--local driver = module:require("sqlbasic").new("SQLite3", "hello.sqlite");
+local option_datastore = module:get_option("datastore");
+local option_datastore_params = module:get_option("datastore_params") or {};
+if option_datastore then
+ local driver = module:require(option_datastore).new(unpack(option_datastore_params));
+ table.insert(drivers, driver);
+end
+
+local datamanager = require "util.datamanager";
+local olddm = {};
+local dm = {};
+for key,val in pairs(datamanager) do olddm[key] = val; end
+
+do -- driver based on old datamanager
+ local dmd = {};
+ dmd.__index = dmd;
+ function dmd:open(host, datastore)
+ return setmetatable({ host = host, datastore = datastore }, dmd);
+ end
+ function dmd:get(user) return olddm.load(user, self.host, self.datastore); end
+ function dmd:set(user, data) return olddm.store(user, self.host, self.datastore, data); end
+ table.insert(drivers, dmd);
+end
+
+local function open(...)
+ for _,driver in pairs(drivers) do
+ local ds = driver:open(...);
+ if ds then return ds; end
+ end
+end
+
+local _data_path;
+--function dm.set_data_path(path) _data_path = path; end
+--function dm.add_callback(...) end
+--function dm.remove_callback(...) end
+--function dm.getpath(...) end
+function dm.load(username, host, datastore)
+ local x = open(host, datastore);
+ return x:get(username);
+end
+function dm.store(username, host, datastore, data)
+ return open(host, datastore):set(username, data);
+end
+--function dm.list_append(...) return driver:list_append(...); end
+--function dm.list_store(...) return driver:list_store(...); end
+--function dm.list_load(...) return driver:list_load(...); end
+
+for key,val in pairs(dm) do datamanager[key] = val; end
diff --git a/plugins/storage/sqlbasic.lib.lua b/plugins/storage/sqlbasic.lib.lua
new file mode 100644
index 00000000..f1202287
--- /dev/null
+++ b/plugins/storage/sqlbasic.lib.lua
@@ -0,0 +1,97 @@
+
+-- Basic SQL driver
+-- This driver stores data as simple key-values
+
+local ser = require "util.serialization".serialize;
+local deser = function(data)
+ module:log("debug", "deser: %s", tostring(data));
+ if not data then return nil; end
+ local f = loadstring("return "..data);
+ if not f then return nil; end
+ setfenv(f, {});
+ local s, d = pcall(f);
+ if not s then return nil; end
+ return d;
+end;
+
+local driver = {};
+driver.__index = driver;
+
+driver.item_table = "item";
+driver.list_table = "list";
+
+function driver:prepare(sql)
+ module:log("debug", "query: %s", sql);
+ local err;
+ if not self.sqlcache then self.sqlcache = {}; end
+ local r = self.sqlcache[sql];
+ if r then return r; end
+ r, err = self.connection:prepare(sql);
+ if not r then error("Unable to prepare SQL statement: "..err); end
+ self.sqlcache[sql] = r;
+ return r;
+end
+
+function driver:load(username, host, datastore)
+ local select = self:prepare("select data from "..self.item_table.." where username=? and host=? and datastore=?");
+ select:execute(username, host, datastore);
+ local row = select:fetch();
+ return row and deser(row[1]) or nil;
+end
+
+function driver:store(username, host, datastore, data)
+ if not data or next(data) == nil then
+ local delete = self:prepare("delete from "..self.item_table.." where username=? and host=? and datastore=?");
+ delete:execute(username, host, datastore);
+ return true;
+ else
+ local d = self:load(username, host, datastore);
+ if d then -- update
+ local update = self:prepare("update "..self.item_table.." set data=? where username=? and host=? and datastore=?");
+ return update:execute(ser(data), username, host, datastore);
+ else -- insert
+ local insert = self:prepare("insert into "..self.item_table.." values (?, ?, ?, ?)");
+ return insert:execute(username, host, datastore, ser(data));
+ end
+ end
+end
+
+function driver:list_append(username, host, datastore, data)
+ if not data then return; end
+ local insert = self:prepare("insert into "..self.list_table.." values (?, ?, ?, ?)");
+ return insert:execute(username, host, datastore, ser(data));
+end
+
+function driver:list_store(username, host, datastore, data)
+ -- remove existing data
+ local delete = self:prepare("delete from "..self.list_table.." where username=? and host=? and datastore=?");
+ delete:execute(username, host, datastore);
+ if data and next(data) ~= nil then
+ -- add data
+ for _, d in ipairs(data) do
+ self:list_append(username, host, datastore, ser(d));
+ end
+ end
+ return true;
+end
+
+function driver:list_load(username, host, datastore)
+ local select = self:prepare("select data from "..self.list_table.." where username=? and host=? and datastore=?");
+ select:execute(username, host, datastore);
+ local r = {};
+ for row in select:rows() do
+ table.insert(r, deser(row[1]));
+ end
+ return r;
+end
+
+local _M = {};
+function _M.new(dbtype, dbname, ...)
+ local d = {};
+ setmetatable(d, driver);
+ local dbh = get_database(dbtype, dbname, ...);
+ --d:set_connection(dbh);
+ d.connection = dbh;
+ return d;
+end
+return _M;
diff --git a/plugins/storage/xep227store.lib.lua b/plugins/storage/xep227store.lib.lua
new file mode 100644
index 00000000..5ef8df54
--- /dev/null
+++ b/plugins/storage/xep227store.lib.lua
@@ -0,0 +1,168 @@
+
+local st = require "util.stanza";
+
+local function getXml(user, host)
+ local jid = user.."@"..host;
+ local path = "data/"..jid..".xml";
+ local f = io.open(path);
+ if not f then return; end
+ local s = f:read("*a");
+ return parse_xml_real(s);
+end
+local function setXml(user, host, xml)
+ local jid = user.."@"..host;
+ local path = "data/"..jid..".xml";
+ if xml then
+ local f = io.open(path, "w");
+ if not f then return; end
+ local s = tostring(xml);
+ f:write(s);
+ f:close();
+ return true;
+ else
+ return os.remove(path);
+ end
+end
+local function getUserElement(xml)
+ if xml and xml.name == "server-data" then
+ local host = xml.tags[1];
+ if host and host.name == "host" then
+ local user = host.tags[1];
+ if user and user.name == "user" then
+ return user;
+ end
+ end
+ end
+end
+local function createOuterXml(user, host)
+ return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'})
+ :tag("host", {jid=host})
+ :tag("user", {name = user});
+end
+local function removeFromArray(array, value)
+ for i,item in ipairs(array) do
+ if item == value then
+ table.remove(array, i);
+ return;
+ end
+ end
+end
+local function removeStanzaChild(s, child)
+ removeFromArray(s.tags, child);
+ removeFromArray(s, child);
+end
+
+local handlers = {};
+
+handlers.accounts = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user and user.attr.password then
+ return { password = user.attr.password };
+ end
+ end;
+ set = function(self, user, data)
+ if data and data.password then
+ local xml = getXml(user, self.host);
+ if not xml then xml = createOuterXml(user, self.host); end
+ local usere = getUserElement(xml);
+ usere.attr.password = data.password;
+ return setXml(user, self.host, xml);
+ else
+ return setXml(user, self.host, nil);
+ end
+ end;
+};
+handlers.vcard = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user then
+ local vcard = user:get_child("vCard", 'vcard-temp');
+ if vcard then
+ return st.preserialize(vcard);
+ end
+ end
+ end;
+ set = function(self, user, data)
+ local xml = getXml(user, self.host);
+ local usere = xml and getUserElement(xml);
+ if usere then
+ local vcard = usere:get_child("vCard", 'vcard-temp');
+ if vcard then
+ removeStanzaChild(usere, vcard);
+ elseif not data then
+ return true;
+ end
+ if data then
+ vcard = st.deserialize(data);
+ usere:add_child(vcard);
+ end
+ return setXml(user, self.host, xml);
+ end
+ return true;
+ end;
+};
+handlers.private = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user then
+ local private = user:get_child("query", "jabber:iq:private");
+ if private then
+ local r = {};
+ for _, tag in ipairs(private.tags) do
+ r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag);
+ end
+ return r;
+ end
+ end
+ end;
+ set = function(self, user, data)
+ local xml = getXml(user, self.host);
+ local usere = xml and getUserElement(xml);
+ if usere then
+ local private = usere:get_child("query", 'jabber:iq:private');
+ if private then removeStanzaChild(usere, private); end
+ if data and next(data) ~= nil then
+ private = st.stanza("query", {xmlns='jabber:iq:private'});
+ for _,tag in pairs(data) do
+ private:add_child(st.deserialize(tag));
+ end
+ usere:add_child(private);
+ end
+ return setXml(user, self.host, xml);
+ end
+ return true;
+ end;
+};
+
+-----------------------------
+local driver = {};
+driver.__index = driver;
+
+function driver:open(host, datastore, typ)
+ local cache_key = host.." "..datastore;
+ if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
+ local instance = setmetatable({}, self);
+ instance.host = host;
+ instance.datastore = datastore;
+ local handler = handlers[datastore];
+ if not handler then return nil; end
+ for key,val in pairs(handler) do
+ instance[key] = val;
+ end
+ if instance.init then instance:init(); end
+ self.ds_cache[cache_key] = instance;
+ return instance;
+end
+
+-----------------------------
+local _M = {};
+
+function _M.new()
+ local instance = setmetatable({}, driver);
+ instance.__index = instance;
+ instance.ds_cache = {};
+ return instance;
+end
+
+return _M;
diff --git a/plugins/storage/xmlparse.lib.lua b/plugins/storage/xmlparse.lib.lua
new file mode 100644
index 00000000..91063995
--- /dev/null
+++ b/plugins/storage/xmlparse.lib.lua
@@ -0,0 +1,56 @@
+
+local st = require "util.stanza";
+
+-- XML parser
+local parse_xml = (function()
+ local entity_map = setmetatable({
+ ["amp"] = "&";
+ ["gt"] = ">";
+ ["lt"] = "<";
+ ["apos"] = "'";
+ ["quot"] = "\"";
+ }, {__index = function(_, s)
+ if s:sub(1,1) == "#" then
+ if s:sub(2,2) == "x" then
+ return string.char(tonumber(s:sub(3), 16));
+ else
+ return string.char(tonumber(s:sub(2)));
+ end
+ end
+ end
+ });
+ local function xml_unescape(str)
+ return (str:gsub("&(.-);", entity_map));
+ end
+ local function parse_tag(s)
+ local name,sattr=(s):gmatch("([^%s]+)(.*)")();
+ local attr = {};
+ for a,b in (sattr):gmatch("([^=%s]+)=['\"]([^'\"]*)['\"]") do attr[a] = xml_unescape(b); end
+ return name, attr;
+ end
+ return function(xml)
+ local stanza = st.stanza("root");
+ local regexp = "<([^>]*)>([^<]*)";
+ for elem, text in xml:gmatch(regexp) do
+ if elem:sub(1,1) == "!" or elem:sub(1,1) == "?" then -- neglect comments and processing-instructions
+ elseif elem:sub(1,1) == "/" then -- end tag
+ elem = elem:sub(2);
+ stanza:up(); -- TODO check for start-end tag name match
+ elseif elem:sub(-1,-1) == "/" then -- empty tag
+ elem = elem:sub(1,-2);
+ local name,attr = parse_tag(elem);
+ stanza:tag(name, attr):up();
+ else -- start tag
+ local name,attr = parse_tag(elem);
+ stanza:tag(name, attr);
+ end
+ if #text ~= 0 then -- text
+ stanza:text(xml_unescape(text));
+ end
+ end
+ return stanza.tags[1];
+ end
+end)();
+-- end of XML parser
+
+return parse_xml;