aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/adhoc/adhoc.lib.lua14
-rw-r--r--plugins/adhoc/mod_adhoc.lua114
-rw-r--r--plugins/mod_admin_adhoc.lua100
-rw-r--r--plugins/mod_admin_telnet.lua500
-rw-r--r--plugins/mod_announce.lua14
-rw-r--r--plugins/mod_auth_anonymous.lua1
-rw-r--r--plugins/mod_auth_cyrus.lua1
-rw-r--r--plugins/mod_auth_internal_hashed.lua69
-rw-r--r--plugins/mod_auth_internal_plain.lua12
-rw-r--r--plugins/mod_blocklist.lua331
-rw-r--r--plugins/mod_bosh.lua237
-rw-r--r--plugins/mod_c2s.lua128
-rw-r--r--plugins/mod_carbons.lua115
-rw-r--r--plugins/mod_component.lua83
-rw-r--r--plugins/mod_compression.lua200
-rw-r--r--plugins/mod_debug_sql.lua27
-rw-r--r--plugins/mod_dialback.lua68
-rw-r--r--plugins/mod_disco.lua72
-rw-r--r--plugins/mod_groups.lua20
-rw-r--r--plugins/mod_http.lua18
-rw-r--r--plugins/mod_http_errors.lua67
-rw-r--r--plugins/mod_http_files.lua36
-rw-r--r--plugins/mod_iq.lua2
-rw-r--r--plugins/mod_lastactivity.lua5
-rw-r--r--plugins/mod_legacyauth.lua15
-rw-r--r--plugins/mod_limits.lua98
-rw-r--r--plugins/mod_mam/fallback_archive.lib.lua91
-rw-r--r--plugins/mod_mam/mamprefs.lib.lua76
-rw-r--r--plugins/mod_mam/mamprefsxml.lib.lua64
-rw-r--r--plugins/mod_mam/mod_mam.lua401
-rw-r--r--plugins/mod_message.lua19
-rw-r--r--plugins/mod_motd.lua7
-rw-r--r--plugins/mod_net_multiplex.lua2
-rw-r--r--plugins/mod_offline.lua36
-rw-r--r--plugins/mod_pep.lua43
-rw-r--r--plugins/mod_ping.lua11
-rw-r--r--plugins/mod_posix.lua80
-rw-r--r--plugins/mod_presence.lua86
-rw-r--r--plugins/mod_privacy.lua443
-rw-r--r--plugins/mod_private.lua62
-rw-r--r--plugins/mod_proxy65.lua41
-rw-r--r--plugins/mod_pubsub.lua463
-rw-r--r--plugins/mod_pubsub/mod_pubsub.lua233
-rw-r--r--plugins/mod_pubsub/pubsub.lib.lua317
-rw-r--r--plugins/mod_register.lua116
-rw-r--r--plugins/mod_roster.lua40
-rw-r--r--plugins/mod_s2s/mod_s2s.lua219
-rw-r--r--plugins/mod_s2s/s2sout.lib.lua67
-rw-r--r--plugins/mod_s2s_auth_certs.lua49
-rw-r--r--plugins/mod_saslauth.lua167
-rw-r--r--plugins/mod_server_contact_info.lua49
-rw-r--r--plugins/mod_stanza_debug.lua29
-rw-r--r--plugins/mod_storage_internal.lua155
-rw-r--r--plugins/mod_storage_none.lua21
-rw-r--r--plugins/mod_storage_sql.lua818
-rw-r--r--plugins/mod_storage_sql1.lua414
-rw-r--r--plugins/mod_storage_xep0227.lua (renamed from plugins/storage/mod_xep0227.lua)51
-rw-r--r--plugins/mod_time.lua2
-rw-r--r--plugins/mod_tls.lua113
-rw-r--r--plugins/mod_unknown.lua4
-rw-r--r--plugins/mod_uptime.lua2
-rw-r--r--plugins/mod_vcard.lua2
-rw-r--r--plugins/mod_version.lua6
-rw-r--r--plugins/mod_watchregistrations.lua7
-rw-r--r--plugins/mod_websocket.lua343
-rw-r--r--plugins/mod_welcome.lua4
-rw-r--r--plugins/mod_windows.lua4
-rw-r--r--plugins/muc/mod_muc.lua120
-rw-r--r--plugins/muc/muc.lib.lua312
-rw-r--r--plugins/sql.lib.lua9
-rw-r--r--plugins/storage/sqlbasic.lib.lua97
-rw-r--r--plugins/storage/xep227store.lib.lua168
72 files changed, 5162 insertions, 3048 deletions
diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua
index b544ddc8..87415636 100644
--- a/plugins/adhoc/adhoc.lib.lua
+++ b/plugins/adhoc/adhoc.lib.lua
@@ -25,12 +25,14 @@ function _M.new(name, node, handler, permission)
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 cmdtag = stanza.tags[1]
+ local sessionid = cmdtag.attr.sessionid or uuid.generate();
+ local dataIn = {
+ to = stanza.attr.to;
+ from = stanza.attr.from;
+ action = cmdtag.attr.action or "execute";
+ form = cmdtag:get_child("x", "jabber:x:data");
+ };
local data, state = command:handler(dataIn, states[sessionid]);
states[sessionid] = state;
diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua
index 69b2c8da..1c956021 100644
--- a/plugins/adhoc/mod_adhoc.lua
+++ b/plugins/adhoc/mod_adhoc.lua
@@ -6,86 +6,90 @@
--
local st = require "util.stanza";
+local keys = require "util.iterators".keys;
+local array_collect = require "util.array".collect;
local is_admin = require "core.usermanager".is_admin;
+local jid_split = require "util.jid".split;
local adhoc_handle_cmd = module:require "adhoc".handle_cmd;
local xmlns_cmd = "http://jabber.org/protocol/commands";
-local xmlns_disco = "http://jabber.org/protocol/disco";
local commands = {};
module:add_feature(xmlns_cmd);
-module:hook("iq/host/"..xmlns_disco.."#info:query", function (event)
- local origin, stanza = event.origin, event.stanza;
- local node = stanza.tags[1].attr.node;
- if stanza.attr.type == "get" and node then
- if commands[node] then
- local privileged = is_admin(stanza.attr.from, stanza.attr.to);
- if (commands[node].permission == "admin" and privileged)
- or (commands[node].permission == "user") then
- reply = st.reply(stanza);
- reply:tag("query", { xmlns = xmlns_disco.."#info",
- node = node });
- reply:tag("identity", { name = commands[node].name,
- category = "automation", type = "command-node" }):up();
- reply:tag("feature", { var = xmlns_cmd }):up();
- reply:tag("feature", { var = "jabber:x:data" }):up();
- else
- reply = st.error_reply(stanza, "auth", "forbidden", "This item is not available to you");
- end
- origin.send(reply);
- return true;
- elseif node == xmlns_cmd then
- reply = st.reply(stanza);
- reply:tag("query", { xmlns = xmlns_disco.."#info",
- node = node });
- reply:tag("identity", { name = "Ad-Hoc Commands",
- category = "automation", type = "command-list" }):up();
- origin.send(reply);
+module:hook("host-disco-info-node", function (event)
+ local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+ if commands[node] then
+ local from = stanza.attr.from;
+ local privileged = is_admin(from, stanza.attr.to);
+ local global_admin = is_admin(from);
+ local username, hostname = jid_split(from);
+ local command = commands[node];
+ if (command.permission == "admin" and privileged)
+ or (command.permission == "global_admin" and global_admin)
+ or (command.permission == "local_user" and hostname == module.host)
+ or (command.permission == "user") then
+ reply:tag("identity", { name = command.name,
+ category = "automation", type = "command-node" }):up();
+ reply:tag("feature", { var = xmlns_cmd }):up();
+ reply:tag("feature", { var = "jabber:x:data" }):up();
+ event.exists = true;
+ else
+ origin.send(st.error_reply(stanza, "auth", "forbidden", "This item is not available to you"));
return true;
-
end
+ elseif node == xmlns_cmd then
+ reply:tag("identity", { name = "Ad-Hoc Commands",
+ category = "automation", type = "command-list" }):up();
+ event.exists = true;
end
end);
-module:hook("iq/host/"..xmlns_disco.."#items:query", function (event)
- local origin, stanza = event.origin, event.stanza;
- if stanza.attr.type == "get" and stanza.tags[1].attr.node
- and stanza.tags[1].attr.node == xmlns_cmd then
- local admin = is_admin(stanza.attr.from, stanza.attr.to);
- local global_admin = is_admin(stanza.attr.from);
- reply = st.reply(stanza);
- reply:tag("query", { xmlns = xmlns_disco.."#items",
- node = xmlns_cmd });
- for node, command in pairs(commands) do
- if (command.permission == "admin" and admin)
- or (command.permission == "global_admin" and global_admin)
- or (command.permission == "user") then
- reply:tag("item", { name = command.name,
- node = node, jid = module:get_host() });
- reply:up();
- end
+module:hook("host-disco-items-node", function (event)
+ local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+ if node ~= xmlns_cmd then
+ return;
+ end
+
+ local from = stanza.attr.from;
+ local admin = is_admin(from, stanza.attr.to);
+ local global_admin = is_admin(from);
+ local username, hostname = jid_split(from);
+ local nodes = array_collect(keys(commands)):sort();
+ for _, node in ipairs(nodes) do
+ local command = commands[node];
+ if (command.permission == "admin" and admin)
+ or (command.permission == "global_admin" and global_admin)
+ or (command.permission == "local_user" and hostname == module.host)
+ or (command.permission == "user") then
+ reply:tag("item", { name = command.name,
+ node = node, jid = module:get_host() });
+ reply:up();
end
- origin.send(reply);
- return true;
end
-end, 500);
+ event.exists = true;
+end);
module:hook("iq/host/"..xmlns_cmd..":command", function (event)
local origin, stanza = event.origin, event.stanza;
if stanza.attr.type == "set" then
local node = stanza.tags[1].attr.node
- if commands[node] then
- local admin = is_admin(stanza.attr.from, stanza.attr.to);
- local global_admin = is_admin(stanza.attr.from);
- if (commands[node].permission == "admin" and not admin)
- or (commands[node].permission == "global_admin" and not global_admin) then
+ local command = commands[node];
+ if command then
+ local from = stanza.attr.from;
+ local admin = is_admin(from, stanza.attr.to);
+ local global_admin = is_admin(from);
+ local username, hostname = jid_split(from);
+ if (command.permission == "admin" and not admin)
+ or (command.permission == "global_admin" and not global_admin)
+ or (command.permission == "local_user" and hostname ~= module.host) then
origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up()
:add_child(commands[node]:cmdtag("canceled")
:tag("note", {type="error"}):text("You don't have permission to execute this command")));
return true
end
-- User has permission now execute the command
- return adhoc_handle_cmd(commands[node], origin, stanza);
+ adhoc_handle_cmd(commands[node], origin, stanza);
+ return true;
end
end
end, 500);
diff --git a/plugins/mod_admin_adhoc.lua b/plugins/mod_admin_adhoc.lua
index 232fa5f7..f3de6793 100644
--- a/plugins/mod_admin_adhoc.lua
+++ b/plugins/mod_admin_adhoc.lua
@@ -9,6 +9,7 @@ local _G = _G;
local prosody = _G.prosody;
local hosts = prosody.hosts;
local t_concat = table.concat;
+local t_sort = table.sort;
local module_host = module:get_host();
@@ -25,10 +26,11 @@ local st, jid = require "util.stanza", require "util.jid";
local timer_add_task = require "util.timer".add_task;
local dataforms_new = require "util.dataforms".new;
local array = require "util.array";
-local modulemanager = require "modulemanager";
+local modulemanager = require "core.modulemanager";
local core_post_stanza = prosody.core_post_stanza;
local adhoc_simple = require "util.adhoc".new_simple_form;
local adhoc_initial = require "util.adhoc".new_initial_data_form;
+local set = require"util.set";
module:depends("adhoc");
local adhoc_new = module:require "adhoc".new;
@@ -95,7 +97,7 @@ local change_user_password_command_handler = adhoc_simple(change_user_password_l
if module_host ~= host then
return { status = "completed", error = { message = "Trying to change the password of a user on " .. host .. " but command was sent to " .. module_host}};
end
- if usermanager_user_exists(username, host) and usermanager_set_password(username, fields.password, host) then
+ if usermanager_user_exists(username, host) and usermanager_set_password(username, fields.password, host, nil) then
return { status = "completed", info = "Password successfully changed" };
else
return { status = "completed", error = { message = "User does not exist" } };
@@ -245,7 +247,7 @@ local get_user_roster_handler = adhoc_simple(get_user_roster_layout, function(fi
local query = st.stanza("query", { xmlns = "jabber:iq:roster" });
for jid in pairs(roster) do
- if jid ~= "pending" and jid then
+ if jid then
query:tag("item", {
jid = jid,
subscription = roster[jid].subscription,
@@ -298,7 +300,7 @@ local get_user_stats_handler = adhoc_simple(get_user_stats_layout, function(fiel
local IPs = "";
local resources = "";
for jid in pairs(roster) do
- if jid ~= "pending" and jid then
+ if jid then
rostersize = rostersize + 1;
end
end
@@ -345,7 +347,7 @@ local get_online_users_command_handler = adhoc_simple(get_online_users_layout, f
count = count + 1;
if fields.details then
for resource, session in pairs(user.sessions or {}) do
- local status, priority = "unavailable", tostring(session.priority or "-");
+ local status, priority, ip = "unavailable", tostring(session.priority or "-"), session.ip or "<unknown>";
if session.presence then
status = session.presence:child_with_name("show");
if status then
@@ -354,13 +356,92 @@ local get_online_users_command_handler = adhoc_simple(get_online_users_layout, f
status = "available";
end
end
- users[#users+1] = " - "..resource..": "..status.."("..priority..")";
+ users[#users+1] = " - "..resource..": "..status.."("..priority.."), IP: ["..ip.."]";
end
end
end
return { status = "completed", result = {layout = get_online_users_result_layout, values = {onlineuserjids=t_concat(users, "\n")}} };
end);
+-- Getting a list of S2S connections (this host)
+local list_s2s_this_result = dataforms_new {
+ title = "List of S2S connections on this host";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/s2s#list" };
+ { name = "sessions", type = "text-multi", label = "Connections:" };
+ { name = "num_in", type = "text-single", label = "#incomming connections:" };
+ { name = "num_out", type = "text-single", label = "#outgoing connections:" };
+};
+
+local function session_flags(session, line)
+ line = line or {};
+
+ if session.id then
+ line[#line+1] = "["..session.id.."]"
+ else
+ line[#line+1] = "["..session.type..(tostring(session):match("%x*$")).."]"
+ end
+
+ local flags = {};
+ if session.cert_identity_status == "valid" then
+ flags[#flags+1] = "authenticated";
+ end
+ if session.secure then
+ flags[#flags+1] = "encrypted";
+ end
+ if session.compressed then
+ flags[#flags+1] = "compressed";
+ end
+ if session.smacks then
+ flags[#flags+1] = "sm";
+ end
+ if session.ip and session.ip:match(":") then
+ flags[#flags+1] = "IPv6";
+ end
+ line[#line+1] = "("..t_concat(flags, ", ")..")";
+
+ return t_concat(line, " ");
+end
+
+local function list_s2s_this_handler(self, data, state)
+ local count_in, count_out = 0, 0;
+ local s2s_list = {};
+
+ local s2s_sessions = module:shared"/*/s2s/sessions";
+ for _, session in pairs(s2s_sessions) do
+ local remotehost, localhost, direction;
+ if session.direction == "outgoing" then
+ direction = "->";
+ count_out = count_out + 1;
+ remotehost, localhost = session.to_host or "?", session.from_host or "?";
+ else
+ direction = "<-";
+ count_in = count_in + 1;
+ remotehost, localhost = session.from_host or "?", session.to_host or "?";
+ end
+ local sess_lines = { r = remotehost,
+ session_flags(session, { "", direction, remotehost or "?" })};
+
+ if localhost == module_host then
+ s2s_list[#s2s_list+1] = sess_lines;
+ end
+ end
+
+ t_sort(s2s_list, function(a, b)
+ return a.r < b.r;
+ end);
+
+ for i, sess_lines in ipairs(s2s_list) do
+ s2s_list[i] = sess_lines[1];
+ end
+
+ return { status = "completed", result = { layout = list_s2s_this_result; values = {
+ sessions = t_concat(s2s_list, "\n"),
+ num_in = tostring(count_in),
+ num_out = tostring(count_out)
+ } } };
+end
+
-- Getting a list of loaded modules
local list_modules_result = dataforms_new {
title = "List of loaded modules";
@@ -489,7 +570,7 @@ local globally_reload_module_handler = adhoc_initial(globally_reload_module_layo
for _, host in pairs(hosts) do
loaded_modules:append(array(keys(host.modules)));
end
- loaded_modules = array(keys(set.new(loaded_modules):items())):sort();
+ loaded_modules = array(set.new(loaded_modules):items()):sort();
return { module = loaded_modules };
end, function(fields, err)
local is_global = false;
@@ -533,6 +614,7 @@ end, function(fields, err)
end);
local function send_to_online(message, server)
+ local sessions;
if server then
sessions = { [server] = hosts[server] };
else
@@ -631,7 +713,7 @@ local globally_unload_module_handler = adhoc_initial(globally_unload_module_layo
for _, host in pairs(hosts) do
loaded_modules:append(array(keys(host.modules)));
end
- loaded_modules = array(keys(set.new(loaded_modules):items())):sort();
+ loaded_modules = array(set.new(loaded_modules):items()):sort();
return { module = loaded_modules };
end, function(fields, err)
local is_global = false;
@@ -727,6 +809,7 @@ local get_user_password_desc = adhoc_new("Get User Password", "http://jabber.org
local get_user_roster_desc = adhoc_new("Get User Roster","http://jabber.org/protocol/admin#get-user-roster", get_user_roster_handler, "admin");
local get_user_stats_desc = adhoc_new("Get User Statistics","http://jabber.org/protocol/admin#user-stats", get_user_stats_handler, "admin");
local get_online_users_desc = adhoc_new("Get List of Online Users", "http://jabber.org/protocol/admin#get-online-users-list", get_online_users_command_handler, "admin");
+local list_s2s_this_desc = adhoc_new("List S2S connections", "http://prosody.im/protocol/s2s#list", list_s2s_this_handler, "admin");
local list_modules_desc = adhoc_new("List loaded modules", "http://prosody.im/protocol/modules#list", list_modules_handler, "admin");
local load_module_desc = adhoc_new("Load module", "http://prosody.im/protocol/modules#load", load_module_handler, "admin");
local globally_load_module_desc = adhoc_new("Globally load module", "http://prosody.im/protocol/modules#global-load", globally_load_module_handler, "global_admin");
@@ -747,6 +830,7 @@ module:provides("adhoc", get_user_password_desc);
module:provides("adhoc", get_user_roster_desc);
module:provides("adhoc", get_user_stats_desc);
module:provides("adhoc", get_online_users_desc);
+module:provides("adhoc", list_s2s_this_desc);
module:provides("adhoc", list_modules_desc);
module:provides("adhoc", load_module_desc);
module:provides("adhoc", globally_load_module_desc);
diff --git a/plugins/mod_admin_telnet.lua b/plugins/mod_admin_telnet.lua
index 86403606..9d0e4c74 100644
--- a/plugins/mod_admin_telnet.lua
+++ b/plugins/mod_admin_telnet.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -17,22 +17,21 @@ local _G = _G;
local prosody = _G.prosody;
local hosts = prosody.hosts;
-local incoming_s2s = prosody.incoming_s2s;
local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" };
local iterators = require "util.iterators";
local keys, values = iterators.keys, iterators.values;
-local jid_bare, jid_split = import("util.jid", "bare", "prepped_split");
+local jid_bare, jid_split, jid_join = import("util.jid", "bare", "prepped_split", "join");
local set, array = require "util.set", require "util.array";
local cert_verify_identity = require "util.x509".verify_identity;
local envload = require "util.envload".envload;
local envloadfile = require "util.envload".envloadfile;
+local has_pposix, pposix = pcall(require, "util.pposix");
local commands = module:shared("commands")
local def_env = module:shared("env");
local default_env_mt = { __index = def_env };
-local core_post_stanza = prosody.core_post_stanza;
local function redirect_output(_G, session)
local env = setmetatable({ print = session.print }, { __index = function (t, k) return rawget(_G, k); end });
@@ -60,20 +59,20 @@ function console:new_session(conn)
disconnect = function () conn:close(); end;
};
session.env = setmetatable({}, default_env_mt);
-
+
-- Load up environment with helper objects
for name, t in pairs(def_env) do
if type(t) == "table" then
session.env[name] = setmetatable({ session = session }, { __index = t });
end
end
-
+
return session;
end
function console:process_line(session, line)
local useglobalenv;
-
+
if line:match("^>") then
line = line:gsub("^>", "");
useglobalenv = true;
@@ -87,9 +86,9 @@ function console:process_line(session, line)
return;
end
end
-
+
session.env._ = line;
-
+
local chunkname = "=console";
local env = (useglobalenv and redirect_output(_G, session)) or session.env or nil
local chunk, err = envload("return "..line, chunkname, env);
@@ -103,20 +102,20 @@ function console:process_line(session, line)
return;
end
end
-
+
local ranok, taskok, message = pcall(chunk);
-
+
if not (ranok or message or useglobalenv) and commands[line:lower()] then
commands[line:lower()](session, line);
return;
end
-
+
if not ranok then
session.print("Fatal error while running command, it did not complete");
session.print("Error: "..taskok);
return;
end
-
+
if not message then
session.print("Result: "..tostring(taskok));
return;
@@ -125,7 +124,7 @@ function console:process_line(session, line)
session.print("Message: "..tostring(message));
return;
end
-
+
session.print("OK: "..tostring(message));
end
@@ -155,6 +154,14 @@ function console_listener.onincoming(conn, data)
session.partial_data = data:match("[^\n]+$");
end
+function console_listener.onreadtimeout(conn)
+ local session = sessions[conn];
+ if session then
+ session.send("\0");
+ return true;
+ end
+end
+
function console_listener.ondisconnect(conn, err)
local session = sessions[conn];
if session then
@@ -217,9 +224,11 @@ function commands.help(session, data)
print [[c2s:show(jid) - Show all client sessions with the specified JID (or all if no JID given)]]
print [[c2s:show_insecure() - Show all unencrypted client connections]]
print [[c2s:show_secure() - Show all encrypted client connections]]
+ print [[c2s:show_tls() - Show TLS cipher info for encrypted sessions]]
print [[c2s:close(jid) - Close all sessions for the specified JID]]
elseif section == "s2s" then
print [[s2s:show(domain) - Show all s2s connections for the given domain (or all if no domain given)]]
+ print [[s2s:show_tls(domain) - Show TLS cipher info for encrypted sessions]]
print [[s2s:close(from, to) - Close a connection from one domain to another]]
print [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]]
elseif section == "module" then
@@ -272,6 +281,8 @@ end
-- Session environment --
-- Anything in def_env will be accessible within the session as a global variable
+--luacheck: ignore 212/self
+
def_env.server = {};
function def_env.server:insane_reload()
@@ -313,9 +324,8 @@ local function human(kb)
end
function def_env.server:memory()
- local pposix = require("util.pposix");
- if not pposix.meminfo then
- return true, "Lua is using "..collectgarbage("count");
+ if not has_pposix or not pposix.meminfo then
+ return true, "Lua is using "..human(collectgarbage("count"));
end
local mem, lua_mem = pposix.meminfo(), collectgarbage("count");
local print = self.session.print;
@@ -337,10 +347,9 @@ local function get_hosts_set(hosts, module)
elseif type(hosts) == "string" then
return set.new { hosts };
elseif hosts == nil then
- local mm = require "modulemanager";
local hosts_set = set.new(array.collect(keys(prosody.hosts)))
- / function (host) return (prosody.hosts[host].type == "local" or module and mm.is_loaded(host, module)) and host or nil; end;
- if module and mm.get_module("*", module) then
+ / function (host) return (prosody.hosts[host].type == "local" or module and modulemanager.is_loaded(host, module)) and host or nil; end;
+ if module and modulemanager.get_module("*", module) then
hosts_set:add("*");
end
return hosts_set;
@@ -348,15 +357,13 @@ local function get_hosts_set(hosts, module)
end
function def_env.module:load(name, hosts, config)
- local mm = require "modulemanager";
-
hosts = get_hosts_set(hosts);
-
+
-- Load the module for each host
local ok, err, count, mod = true, nil, 0, nil;
for host in hosts do
- if (not mm.is_loaded(host, name)) then
- mod, err = mm.load(host, name, config);
+ if (not modulemanager.is_loaded(host, name)) then
+ mod, err = modulemanager.load(host, name, config);
if not mod then
ok = false;
if err == "global-module-already-loaded" then
@@ -372,20 +379,18 @@ function def_env.module:load(name, hosts, config)
end
end
end
-
- return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
+
+ return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
end
function def_env.module:unload(name, hosts)
- local mm = require "modulemanager";
-
hosts = get_hosts_set(hosts, name);
-
+
-- Unload the module for each host
local ok, err, count = true, nil, 0;
for host in hosts do
- if mm.is_loaded(host, name) then
- ok, err = mm.unload(host, name);
+ if modulemanager.is_loaded(host, name) then
+ ok, err = modulemanager.unload(host, name);
if not ok then
ok = false;
self.session.print(err or "Unknown error unloading module");
@@ -399,8 +404,6 @@ function def_env.module:unload(name, hosts)
end
function def_env.module:reload(name, hosts)
- local mm = require "modulemanager";
-
hosts = array.collect(get_hosts_set(hosts, name)):sort(function (a, b)
if a == "*" then return true
elseif b == "*" then return false
@@ -410,8 +413,8 @@ function def_env.module:reload(name, hosts)
-- Reload the module for each host
local ok, err, count = true, nil, 0;
for _, host in ipairs(hosts) do
- if mm.is_loaded(host, name) then
- ok, err = mm.reload(host, name);
+ if modulemanager.is_loaded(host, name) then
+ ok, err = modulemanager.reload(host, name);
if not ok then
ok = false;
self.session.print(err or "Unknown error reloading module");
@@ -438,7 +441,7 @@ function def_env.module:list(hosts)
if type(hosts) ~= "table" then
return false, "Please supply a host or a list of hosts you would like to see";
end
-
+
local print = self.session.print;
for _, host in ipairs(hosts) do
print((host == "*" and "Global" or host)..":");
@@ -477,61 +480,109 @@ function def_env.config:reload()
return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err);
end
-def_env.hosts = {};
-function def_env.hosts:list()
- for host, host_session in pairs(hosts) do
- self.session.print(host);
+local function common_info(session, line)
+ if session.id then
+ line[#line+1] = "["..session.id.."]"
+ else
+ line[#line+1] = "["..session.type..(tostring(session):match("%x*$")).."]"
end
- return true, "Done";
end
-function def_env.hosts:add(name)
+local function session_flags(session, line)
+ line = line or {};
+ common_info(session, line);
+ if session.type == "c2s" then
+ local status, priority = "unavailable", tostring(session.priority or "-");
+ if session.presence then
+ status = session.presence:get_child_text("show") or "available";
+ end
+ line[#line+1] = status.."("..priority..")";
+ end
+ if session.cert_identity_status == "valid" then
+ line[#line+1] = "(authenticated)";
+ end
+ if session.secure then
+ line[#line+1] = "(encrypted)";
+ end
+ if session.compressed then
+ line[#line+1] = "(compressed)";
+ end
+ if session.smacks then
+ line[#line+1] = "(sm)";
+ end
+ if session.ip and session.ip:match(":") then
+ line[#line+1] = "(IPv6)";
+ end
+ if session.remote then
+ line[#line+1] = "(remote)";
+ end
+ return table.concat(line, " ");
+end
+
+local function tls_info(session, line)
+ line = line or {};
+ common_info(session, line);
+ if session.secure then
+ local sock = session.conn and session.conn.socket and session.conn:socket();
+ if sock and sock.info then
+ local info = sock:info();
+ line[#line+1] = ("(%s with %s)"):format(info.protocol, info.cipher);
+ else
+ line[#line+1] = "(cipher info unavailable)";
+ end
+ else
+ line[#line+1] = "(insecure)";
+ end
+ return table.concat(line, " ");
end
def_env.c2s = {};
+local function get_jid(session)
+ if session.username then
+ return session.full_jid or jid_join(session.username, session.host, session.resource);
+ end
+
+ local conn = session.conn;
+ local ip = session.ip or "?";
+ local clientport = conn and conn:clientport() or "?";
+ local serverip = conn and conn.server and conn:server():ip() or "?";
+ local serverport = conn and conn:serverport() or "?"
+ return jid_join("["..ip.."]:"..clientport, session.host or "["..serverip.."]:"..serverport);
+end
+
local function show_c2s(callback)
- for hostname, host in pairs(hosts) do
- for username, user in pairs(host.sessions or {}) do
- for resource, session in pairs(user.sessions or {}) do
- local jid = username.."@"..hostname.."/"..resource;
- callback(jid, session);
+ local c2s = array.collect(values(module:shared"/*/c2s/sessions"));
+ c2s:sort(function(a, b)
+ if a.host == b.host then
+ if a.username == b.username then
+ return (a.resource or "") > (b.resource or "");
end
+ return (a.username or "") > (b.username or "");
end
- end
+ return (a.host or "") > (b.host or "");
+ end):map(function (session)
+ callback(get_jid(session), session)
+ end);
end
function def_env.c2s:count(match_jid)
- local count = 0;
- show_c2s(function (jid, session)
- if (not match_jid) or jid:match(match_jid) then
- count = count + 1;
- end
- end);
- return true, "Total: "..count.." clients";
+ return true, "Total: ".. iterators.count(values(module:shared"/*/c2s/sessions")) .." clients";
end
-function def_env.c2s:show(match_jid)
+function def_env.c2s:show(match_jid, annotate)
local print, count = self.session.print, 0;
- local curr_host;
+ annotate = annotate or session_flags;
+ local curr_host = false;
show_c2s(function (jid, session)
if curr_host ~= session.host then
curr_host = session.host;
- print(curr_host);
+ print(curr_host or "(not connected to any host yet)");
end
if (not match_jid) or jid:match(match_jid) then
count = count + 1;
- local status, priority = "unavailable", tostring(session.priority or "-");
- if session.presence then
- status = session.presence:child_with_name("show");
- if status then
- status = status:get_text() or "[invalid!]";
- else
- status = "available";
- end
- end
- print(" "..jid.." - "..status.."("..priority..")");
- end
+ print(annotate(session, { " ", jid }));
+ end
end);
return true, "Total: "..count.." clients";
end
@@ -542,7 +593,7 @@ function def_env.c2s:show_insecure(match_jid)
if ((not match_jid) or jid:match(match_jid)) and not session.secure then
count = count + 1;
print(jid);
- end
+ end
end);
return true, "Total: "..count.." insecure client connections";
end
@@ -553,11 +604,15 @@ function def_env.c2s:show_secure(match_jid)
if ((not match_jid) or jid:match(match_jid)) and session.secure then
count = count + 1;
print(jid);
- end
+ end
end);
return true, "Total: "..count.." secure client connections";
end
+function def_env.c2s:show_tls(match_jid)
+ return self:show(match_jid, tls_info);
+end
+
function def_env.c2s:close(match_jid)
local count = 0;
show_c2s(function (jid, session)
@@ -569,99 +624,87 @@ function def_env.c2s:close(match_jid)
return true, "Total: "..count.." sessions closed";
end
-local function session_flags(session, line)
- if session.cert_identity_status == "valid" then
- line[#line+1] = "(secure)";
- elseif session.secure then
- line[#line+1] = "(encrypted)";
- end
- if session.compressed then
- line[#line+1] = "(compressed)";
- end
- if session.smacks then
- line[#line+1] = "(sm)";
- end
- if session.conn and session.conn:ip():match(":") then
- line[#line+1] = "(IPv6)";
- end
- return table.concat(line, " ");
-end
def_env.s2s = {};
-function def_env.s2s:show(match_jid)
- local _print = self.session.print;
+function def_env.s2s:show(match_jid, annotate)
local print = self.session.print;
-
+ annotate = annotate or session_flags;
+
local count_in, count_out = 0,0;
-
- for host, host_session in pairs(hosts) do
- print = function (...) _print(host); _print(...); print = _print; end
- for remotehost, session in pairs(host_session.s2sout) do
- if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then
- count_out = count_out + 1;
- print(session_flags(session, {" ", host, "->", remotehost}));
- if session.sendq then
- print(" There are "..#session.sendq.." queued outgoing stanzas for this connection");
- end
- if session.type == "s2sout_unauthed" then
- if session.connecting then
- print(" Connection not yet established");
- if not session.srv_hosts then
- if not session.conn then
- print(" We do not yet have a DNS answer for this host's SRV records");
- else
- print(" This host has no SRV records, using A record instead");
- end
- elseif session.srv_choice then
- print(" We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts);
- local srv_choice = session.srv_hosts[session.srv_choice];
- print(" Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269));
+ local s2s_list = { };
+
+ local s2s_sessions = module:shared"/*/s2s/sessions";
+ for _, session in pairs(s2s_sessions) do
+ local remotehost, localhost, direction;
+ if session.direction == "outgoing" then
+ direction = "->";
+ count_out = count_out + 1;
+ remotehost, localhost = session.to_host or "?", session.from_host or "?";
+ else
+ direction = "<-";
+ count_in = count_in + 1;
+ remotehost, localhost = session.from_host or "?", session.to_host or "?";
+ end
+ local sess_lines = { l = localhost, r = remotehost,
+ annotate(session, { "", direction, remotehost or "?" })};
+
+ if (not match_jid) or remotehost:match(match_jid) or localhost:match(match_jid) then
+ table.insert(s2s_list, sess_lines);
+ local print = function (s) table.insert(sess_lines, " "..s); end
+ if session.sendq then
+ print("There are "..#session.sendq.." queued outgoing stanzas for this connection");
+ end
+ if session.type == "s2sout_unauthed" then
+ if session.connecting then
+ print("Connection not yet established");
+ if not session.srv_hosts then
+ if not session.conn then
+ print("We do not yet have a DNS answer for this host's SRV records");
+ else
+ print("This host has no SRV records, using A record instead");
end
- elseif session.notopen then
- print(" The <stream> has not yet been opened");
- elseif not session.dialback_key then
- print(" Dialback has not been initiated yet");
- elseif session.dialback_key then
- print(" Dialback has been requested, but no result received");
+ elseif session.srv_choice then
+ print("We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts);
+ local srv_choice = session.srv_hosts[session.srv_choice];
+ print("Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269));
end
+ elseif session.notopen then
+ print("The <stream> has not yet been opened");
+ elseif not session.dialback_key then
+ print("Dialback has not been initiated yet");
+ elseif session.dialback_key then
+ print("Dialback has been requested, but no result received");
end
end
- end
- local subhost_filter = function (h)
- return (match_jid and h:match(match_jid));
- end
- for session in pairs(incoming_s2s) do
- if session.to_host == host and ((not match_jid) or host:match(match_jid)
- or (session.from_host and session.from_host:match(match_jid))
- -- Pft! is what I say to list comprehensions
- or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then
- count_in = count_in + 1;
- print(session_flags(session, {" ", host, "<-", session.from_host or "(unknown)"}));
- if session.type == "s2sin_unauthed" then
- print(" Connection not yet authenticated");
- end
+ if session.type == "s2sin_unauthed" then
+ print("Connection not yet authenticated");
+ elseif session.type == "s2sin" then
for name in pairs(session.hosts) do
if name ~= session.from_host then
- print(" also hosts "..tostring(name));
+ print("also hosts "..tostring(name));
end
end
end
end
-
- print = _print;
end
-
- for session in pairs(incoming_s2s) do
- if not session.to_host and ((not match_jid) or session.from_host and session.from_host:match(match_jid)) then
- count_in = count_in + 1;
- print("Other incoming s2s connections");
- print(" (unknown) <- "..(session.from_host or "(unknown)"));
- end
+
+ -- Sort by local host, then remote host
+ table.sort(s2s_list, function(a,b)
+ if a.l == b.l then return a.r < b.r; end
+ return a.l < b.l;
+ end);
+ local lasthost;
+ for _, sess_lines in ipairs(s2s_list) do
+ if sess_lines.l ~= lasthost then print(sess_lines.l); lasthost=sess_lines.l end
+ for _, line in ipairs(sess_lines) do print(line); end
end
-
return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections";
end
+function def_env.s2s:show_tls(match_jid)
+ return self:show(match_jid, tls_info);
+end
+
local function print_subject(print, subject)
for _, entry in ipairs(subject) do
print(
@@ -688,16 +731,10 @@ local function print_errors(print, errors)
end
function def_env.s2s:showcert(domain)
- local ser = require "util.serialization".serialize;
local print = self.session.print;
- local domain_sessions = set.new(array.collect(keys(incoming_s2s)))
- /function(session) return session.from_host == domain and session or nil; end;
- for local_host in values(prosody.hosts) do
- local s2sout = local_host.s2sout;
- if s2sout and s2sout[domain] then
- domain_sessions:add(s2sout[domain]);
- end
- end
+ local s2s_sessions = module:shared"/*/s2s/sessions";
+ local domain_sessions = set.new(array.collect(values(s2s_sessions)))
+ /function(session) return (session.to_host == domain or session.from_host == domain) and session or nil; end;
local cert_set = {};
for session in domain_sessions do
local conn = session.conn;
@@ -736,18 +773,18 @@ function def_env.s2s:showcert(domain)
local domain_certs = array.collect(values(cert_set));
-- Phew. We now have a array of unique certificates presented by domain.
local n_certs = #domain_certs;
-
+
if n_certs == 0 then
return "No certificates found for "..domain;
end
-
+
local function _capitalize_and_colon(byte)
return string.upper(byte)..":";
end
local function pretty_fingerprint(hash)
return hash:gsub("..", _capitalize_and_colon):sub(1, -2);
end
-
+
for cert_info in values(domain_certs) do
local certs = cert_info.certs;
local cert = certs[1];
@@ -788,76 +825,38 @@ end
function def_env.s2s:close(from, to)
local print, count = self.session.print, 0;
-
- if not (from and to) then
+ local s2s_sessions = module:shared"/*/s2s/sessions";
+
+ local match_id;
+ if from and not to then
+ match_id, from = from;
+ elseif not to then
return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'";
elseif from == to then
return false, "Both from and to are the same... you can't do that :)";
end
-
- if hosts[from] and not hosts[to] then
- -- Is an outgoing connection
- local session = hosts[from].s2sout[to];
- if not session then
- print("No outgoing connection from "..from.." to "..to)
- else
+
+ for _, session in pairs(s2s_sessions) do
+ local id = session.type..tostring(session):match("[a-f0-9]+$");
+ if (match_id and match_id == id)
+ or (session.from_host == from and session.to_host == to) then
+ print(("Closing connection from %s to %s [%s]"):format(session.from_host, session.to_host, id));
(session.close or s2smanager.destroy_session)(session);
- count = count + 1;
- print("Closed outgoing session from "..from.." to "..to);
+ count = count + 1 ;
end
- elseif hosts[to] and not hosts[from] then
- -- Is an incoming connection
- for session in pairs(incoming_s2s) do
- if session.to_host == to and session.from_host == from then
- (session.close or s2smanager.destroy_session)(session);
- count = count + 1;
- end
- end
-
- if count == 0 then
- print("No incoming connections from "..from.." to "..to);
- else
- print("Closed "..count.." incoming session"..((count == 1 and "") or "s").." from "..from.." to "..to);
- end
- elseif hosts[to] and hosts[from] then
- return false, "Both of the hostnames you specified are local, there are no s2s sessions to close";
- else
- return false, "Neither of the hostnames you specified are being used on this server";
end
-
return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s");
end
function def_env.s2s:closeall(host)
- local count = 0;
-
- if not host or type(host) ~= "string" then return false, "wrong syntax: please use s2s:closeall('hostname.tld')"; end
- if hosts[host] then
- for session in pairs(incoming_s2s) do
- if session.to_host == host then
- (session.close or s2smanager.destroy_session)(session);
- count = count + 1;
- end
- end
- for _, session in pairs(hosts[host].s2sout) do
- (session.close or s2smanager.destroy_session)(session);
- count = count + 1;
- end
- else
- for session in pairs(incoming_s2s) do
- if session.from_host == host then
- (session.close or s2smanager.destroy_session)(session);
- count = count + 1;
- end
- end
- for _, h in pairs(hosts) do
- if h.s2sout[host] then
- (h.s2sout[host].close or s2smanager.destroy_session)(h.s2sout[host]);
- count = count + 1;
- end
+ local count = 0;
+ local s2s_sessions = module:shared"/*/s2s/sessions";
+ for _,session in pairs(s2s_sessions) do
+ if not host or session.from_host == host or session.to_host == host then
+ session:close();
+ count = count + 1;
end
- end
-
+ end
if count == 0 then return false, "No sessions to close.";
else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end
end
@@ -874,9 +873,19 @@ end
function def_env.host:list()
local print = self.session.print;
local i = 0;
+ local type;
for host in values(array.collect(keys(prosody.hosts)):sort()) do
i = i + 1;
- print(host);
+ type = hosts[host].type;
+ if type == "local" then
+ print(host);
+ else
+ type = module:context(host):get_option_string("component_module", type);
+ if type ~= "component" then
+ type = type .. " component";
+ end
+ print(("%s (%s)"):format(host, type));
+ end
end
return true, i.." hosts";
end
@@ -946,11 +955,11 @@ local function check_muc(jid)
end
function def_env.muc:create(room_jid)
- local room, host = check_muc(room_jid);
+ local room_name, host = check_muc(room_jid);
if not room_name then
return room_name, host;
end
- if not room then return nil, host end
+ if not room_name then return nil, host end
if hosts[host].modules.muc.rooms[room_jid] then return nil, "Room exists already" end
return hosts[host].modules.muc.create_room(room_jid);
end
@@ -967,6 +976,20 @@ function def_env.muc:room(room_jid)
return setmetatable({ room = room_obj }, console_room_mt);
end
+function def_env.muc:list(host)
+ local host_session = hosts[host];
+ if not host_session or not host_session.modules.muc then
+ return nil, "Please supply the address of a local MUC component";
+ end
+ local print = self.session.print;
+ local c = 0;
+ for name in keys(host_session.modules.muc.rooms) do
+ print(name);
+ c = c + 1;
+ end
+ return true, c.." rooms";
+end
+
local um = require"core.usermanager";
def_env.user = {};
@@ -1007,7 +1030,7 @@ function def_env.user:password(jid, password)
elseif not um.user_exists(username, host) then
return nil, "No such user";
end
- local ok, err = um.set_password(username, password, host);
+ local ok, err = um.set_password(username, password, host, nil);
if ok then
return true, "User password changed";
else
@@ -1038,9 +1061,8 @@ def_env.xmpp = {};
local st = require "util.stanza";
function def_env.xmpp:ping(localhost, remotehost)
if hosts[localhost] then
- core_post_stanza(hosts[localhost],
- st.iq{ from=localhost, to=remotehost, type="get", id="ping" }
- :tag("ping", {xmlns="urn:xmpp:ping"}));
+ module:send(st.iq{ from=localhost, to=remotehost, type="get", id="ping" }
+ :tag("ping", {xmlns="urn:xmpp:ping"}), hosts[localhost]);
return true, "Sent ping";
else
return nil, "No such host";
@@ -1089,17 +1111,17 @@ function def_env.http:list()
for host in pairs(prosody.hosts) do
local http_apps = modulemanager.get_items("http-provider", host);
if #http_apps > 0 then
- local http_host = module:context(host):get_option("http_host");
+ local http_host = module:context(host):get_option_string("http_host");
print("HTTP endpoints on "..host..(http_host and (" (using "..http_host.."):") or ":"));
for _, provider in ipairs(http_apps) do
- local url = module:context(host):http_url(provider.name);
+ local url = module:context(host):http_url(provider.name, provider.default_path);
print("", url);
end
print("");
end
end
- local default_host = module:get_option("http_default_host");
+ local default_host = module:get_option_string("http_default_host");
if not default_host then
print("HTTP requests to unknown hosts will return 404 Not Found");
else
@@ -1108,32 +1130,34 @@ function def_env.http:list()
return true;
end
+module:hook("server-stopping", function(event)
+ for conn, session in pairs(sessions) do
+ session.print("Shutting down: "..(event.reason or "unknown reason"));
+ end
+end);
+
-------------
function printbanner(session)
- local option = module:get_option("console_banner");
- if option == nil or option == "full" or option == "graphic" then
+ local option = module:get_option_string("console_banner", "full");
+ if option == "full" or option == "graphic" then
session.print [[
- ____ \ / _
- | _ \ _ __ ___ ___ _-_ __| |_ _
+ ____ \ / _
+ | _ \ _ __ ___ ___ _-_ __| |_ _
| |_) | '__/ _ \/ __|/ _ \ / _` | | | |
| __/| | | (_) \__ \ |_| | (_| | |_| |
|_| |_| \___/|___/\___/ \__,_|\__, |
- A study in simplicity |___/
+ A study in simplicity |___/
]]
end
- if option == nil or option == "short" or option == "full" then
+ if option == "short" or option == "full" then
session.print("Welcome to the Prosody administration console. For a list of commands, type: help");
session.print("You may find more help on using this console in our online documentation at ");
session.print("http://prosody.im/doc/console\n");
end
- if option and option ~= "short" and option ~= "full" and option ~= "graphic" then
- if type(option) == "string" then
- session.print(option)
- elseif type(option) == "function" then
- module:log("warn", "Using functions as value for the console_banner option is no longer supported");
- end
+ if option ~= "short" and option ~= "full" and option ~= "graphic" then
+ session.print(option);
end
end
diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua
index 96976d6f..9327556c 100644
--- a/plugins/mod_announce.lua
+++ b/plugins/mod_announce.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -39,22 +39,22 @@ end
function handle_announcement(event)
local origin, stanza = event.origin, event.stanza;
local node, host, resource = jid.split(stanza.attr.to);
-
+
if resource ~= "announce/online" then
return; -- Not an announcement
end
-
+
if not is_admin(stanza.attr.from) then
-- Not an admin? Not allowed!
module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from);
return;
end
-
+
module:log("info", "Sending server announcement to all online users");
local message = st.clone(stanza);
message.attr.type = "headline";
message.attr.from = host;
-
+
local c = send_to_online(message, host);
module:log("info", "Announcement sent to %d online users", c);
return true;
@@ -83,9 +83,9 @@ function announce_handler(self, data, state)
module:log("info", "Sending server announcement to all online users");
local message = st.message({type = "headline"}, fields.announcement):up()
:tag("subject"):text(fields.subject or "Announcement");
-
+
local count = send_to_online(message, data.to);
-
+
module:log("info", "Announcement sent to %d online users", count);
return { status = "completed", info = ("Announcement sent to %d online users"):format(count) };
else
diff --git a/plugins/mod_auth_anonymous.lua b/plugins/mod_auth_anonymous.lua
index 8de46f8c..1f2bceb3 100644
--- a/plugins/mod_auth_anonymous.lua
+++ b/plugins/mod_auth_anonymous.lua
@@ -5,6 +5,7 @@
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
+-- luacheck: ignore 212
local new_sasl = require "util.sasl".new;
local datamanager = require "util.datamanager";
diff --git a/plugins/mod_auth_cyrus.lua b/plugins/mod_auth_cyrus.lua
index 7668f8c4..0debc287 100644
--- a/plugins/mod_auth_cyrus.lua
+++ b/plugins/mod_auth_cyrus.lua
@@ -5,6 +5,7 @@
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
+-- luacheck: ignore 212
local log = require "util.logger".init("auth_cyrus");
diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua
index 2b041e43..35764afb 100644
--- a/plugins/mod_auth_internal_hashed.lua
+++ b/plugins/mod_auth_internal_hashed.lua
@@ -7,44 +7,30 @@
-- COPYING file in the source package for more information.
--
-local log = require "util.logger".init("auth_internal_hashed");
+local max = math.max;
+
local getAuthenticationDatabaseSHA1 = require "util.sasl.scram".getAuthenticationDatabaseSHA1;
local usermanager = require "core.usermanager";
local generate_uuid = require "util.uuid".generate;
local new_sasl = require "util.sasl".new;
+local hex = require"util.hex";
+local to_hex, from_hex = hex.to, hex.from;
+
+local log = module._log;
+local host = module.host;
local accounts = module:open_store("accounts");
-local to_hex;
-do
- local function replace_byte_with_hex(byte)
- return ("%02x"):format(byte:byte());
- end
- function to_hex(binary_string)
- return binary_string:gsub(".", replace_byte_with_hex);
- end
-end
-
-local from_hex;
-do
- local function replace_hex_with_byte(hex)
- return string.char(tonumber(hex, 16));
- end
- function from_hex(hex_string)
- return hex_string:gsub("..", replace_hex_with_byte);
- end
-end
-- Default; can be set per-user
-local iteration_count = 4096;
+local default_iteration_count = 4096;
-local host = module.host;
-- define auth provider
local provider = {};
-log("debug", "initializing internal_hashed authentication provider for host '%s'", host);
function provider.test_password(username, password)
+ log("debug", "test password for user '%s'", username);
local credentials = accounts:get(username) or {};
if credentials.password ~= nil and string.len(credentials.password) ~= 0 then
@@ -62,12 +48,12 @@ function provider.test_password(username, password)
if credentials.iteration_count == nil or credentials.salt == nil or string.len(credentials.salt) == 0 then
return nil, "Auth failed. Stored salt and iteration count information is not complete.";
end
-
+
local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, credentials.salt, credentials.iteration_count);
-
+
local stored_key_hex = to_hex(stored_key);
local server_key_hex = to_hex(server_key);
-
+
if valid and stored_key_hex == credentials.stored_key and server_key_hex == credentials.server_key then
return true;
else
@@ -76,14 +62,15 @@ function provider.test_password(username, password)
end
function provider.set_password(username, password)
+ log("debug", "set_password for username '%s'", username);
local account = accounts:get(username);
if account then
- account.salt = account.salt or generate_uuid();
- account.iteration_count = account.iteration_count or iteration_count;
+ account.salt = generate_uuid();
+ account.iteration_count = max(account.iteration_count or 0, default_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
@@ -96,7 +83,7 @@ end
function provider.user_exists(username)
local account = accounts:get(username);
if not account then
- log("debug", "account not found for username '%s' at host '%s'", username, host);
+ log("debug", "account not found for username '%s'", username);
return nil, "Auth failed. Invalid username";
end
return true;
@@ -111,10 +98,13 @@ function provider.create_user(username, password)
return accounts:set(username, {});
end
local salt = generate_uuid();
- local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, iteration_count);
+ local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, default_iteration_count);
local stored_key_hex = to_hex(stored_key);
local server_key_hex = to_hex(server_key);
- return accounts:set(username, {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = iteration_count});
+ return accounts:set(username, {
+ stored_key = stored_key_hex, server_key = server_key_hex,
+ salt = salt, iteration_count = default_iteration_count
+ });
end
function provider.delete_user(username)
@@ -123,19 +113,22 @@ end
function provider.get_sasl_handler()
local testpass_authentication_profile = {
- plain_test = function(sasl, username, password, realm)
+ plain_test = function(_, username, password, realm)
return usermanager.test_password(username, realm, password), true;
end,
- scram_sha_1 = function(sasl, username, realm)
+ scram_sha_1 = function(_, username)
local credentials = accounts:get(username);
if not credentials then return; end
if credentials.password then
- usermanager.set_password(username, credentials.password, host);
+ if provider.set_password(username, credentials.password) == nil then
+ return nil, "Auth failed. Could not set hashed password from plaintext.";
+ end
credentials = accounts:get(username);
if not credentials then return; end
end
-
- local stored_key, server_key, iteration_count, salt = credentials.stored_key, credentials.server_key, credentials.iteration_count, credentials.salt;
+
+ local stored_key, server_key = credentials.stored_key, credentials.server_key;
+ local iteration_count, salt = 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;
@@ -143,6 +136,6 @@ function provider.get_sasl_handler()
};
return new_sasl(host, testpass_authentication_profile);
end
-
+
module:provides("auth", provider);
diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua
index d226fdbe..276efb64 100644
--- a/plugins/mod_auth_internal_plain.lua
+++ b/plugins/mod_auth_internal_plain.lua
@@ -16,10 +16,9 @@ local accounts = module:open_store("accounts");
-- define auth provider
local provider = {};
-log("debug", "initializing internal_plain authentication provider for host '%s'", host);
function provider.test_password(username, password)
- log("debug", "test password for user %s at host %s", username, host);
+ log("debug", "test password for user '%s'", username);
local credentials = accounts:get(username) or {};
if password == credentials.password then
@@ -30,11 +29,12 @@ function provider.test_password(username, password)
end
function provider.get_password(username)
- log("debug", "get_password for username '%s' at host '%s'", username, host);
+ log("debug", "get_password for username '%s'", username);
return (accounts:get(username) or {}).password;
end
function provider.set_password(username, password)
+ log("debug", "set_password for username '%s'", username);
local account = accounts:get(username);
if account then
account.password = password;
@@ -46,7 +46,7 @@ end
function provider.user_exists(username)
local account = accounts:get(username);
if not account then
- log("debug", "account not found for username '%s' at host '%s'", username, host);
+ log("debug", "account not found for username '%s'", username);
return nil, "Auth failed. Invalid username";
end
return true;
@@ -66,7 +66,7 @@ end
function provider.get_sasl_handler()
local getpass_authentication_profile = {
- plain = function(sasl, username, realm)
+ plain = function(_, username, realm)
local password = usermanager.get_password(username, realm);
if not password then
return "", nil;
@@ -76,6 +76,6 @@ function provider.get_sasl_handler()
};
return new_sasl(host, getpass_authentication_profile);
end
-
+
module:provides("auth", provider);
diff --git a/plugins/mod_blocklist.lua b/plugins/mod_blocklist.lua
new file mode 100644
index 00000000..3c6708f0
--- /dev/null
+++ b/plugins/mod_blocklist.lua
@@ -0,0 +1,331 @@
+-- Prosody IM
+-- Copyright (C) 2009-2010 Matthew Wild
+-- Copyright (C) 2009-2010 Waqas Hussain
+-- Copyright (C) 2014-2015 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- This module implements XEP-0191: Blocking Command
+--
+
+local user_exists = require"core.usermanager".user_exists;
+local rostermanager = require"core.rostermanager";
+local is_contact_subscribed = rostermanager.is_contact_subscribed;
+local is_contact_pending_in = rostermanager.is_contact_pending_in;
+local load_roster = rostermanager.load_roster;
+local save_roster = rostermanager.save_roster;
+local st = require"util.stanza";
+local st_error_reply = st.error_reply;
+local jid_prep = require"util.jid".prep;
+local jid_split = require"util.jid".split;
+
+local storage = module:open_store();
+local sessions = prosody.hosts[module.host].sessions;
+local full_sessions = prosody.full_sessions;
+
+-- First level cache of blocklists by username.
+-- Weak table so may randomly expire at any time.
+local cache = setmetatable({}, { __mode = "v" });
+
+-- Second level of caching, keeps a fixed number of items, also anchors
+-- items in the above cache.
+--
+-- The size of this affects how often we will need to load a blocklist from
+-- disk, which we want to avoid during routing. On the other hand, we don't
+-- want to use too much memory either, so this can be tuned by advanced
+-- users. TODO use science to figure out a better default, 64 is just a guess.
+local cache_size = module:get_option_number("blocklist_cache_size", 64);
+local cache2 = require"util.cache".new(cache_size);
+
+local null_blocklist = {};
+
+module:add_feature("urn:xmpp:blocking");
+
+local function set_blocklist(username, blocklist)
+ local ok, err = storage:set(username, blocklist);
+ if not ok then
+ return ok, err;
+ end
+ -- Successful save, update the cache
+ cache2:set(username, blocklist);
+ cache[username] = blocklist;
+ return true;
+end
+
+-- Migrates from the old mod_privacy storage
+local function migrate_privacy_list(username)
+ local legacy_data = module:open_store("privacy"):get(username);
+ if not legacy_data or not legacy_data.lists or not legacy_data.default then return; end
+ local default_list = legacy_data.lists[legacy_data.default];
+ if not default_list or not default_list.items then return; end
+
+ local migrated_data = { [false] = { created = os.time(); migrated = "privacy" }};
+
+ module:log("info", "Migrating blocklist from mod_privacy storage for user '%s'", username);
+ for _, item in ipairs(default_list.items) do
+ if item.type == "jid" and item.action == "deny" then
+ local jid = jid_prep(item.value);
+ if not jid then
+ module:log("warn", "Invalid JID in privacy store for user '%s' not migrated: %s", username, tostring(item.value));
+ else
+ migrated_data[jid] = true;
+ end
+ end
+ end
+ set_blocklist(username, migrated_data);
+ return migrated_data;
+end
+
+local function get_blocklist(username)
+ local blocklist = cache2:get(username);
+ if not blocklist then
+ if not user_exists(username, module.host) then
+ return null_blocklist;
+ end
+ blocklist = storage:get(username);
+ if not blocklist then
+ blocklist = migrate_privacy_list(username);
+ end
+ if not blocklist then
+ blocklist = { [false] = { created = os.time(); }; };
+ end
+ cache2:set(username, blocklist);
+ end
+ cache[username] = blocklist;
+ return blocklist;
+end
+
+module:hook("iq-get/self/urn:xmpp:blocking:blocklist", function (event)
+ local origin, stanza = event.origin, event.stanza;
+ local username = origin.username;
+ local reply = st.reply(stanza):tag("blocklist", { xmlns = "urn:xmpp:blocking" });
+ local blocklist = cache[username] or get_blocklist(username);
+ for jid in pairs(blocklist) do
+ if jid then
+ reply:tag("item", { jid = jid }):up();
+ end
+ end
+ origin.interested_blocklist = true; -- Gets notified about changes
+ origin.send(reply);
+ return true;
+end, -1);
+
+-- Add or remove some jid(s) from the blocklist
+-- We want this to be atomic and not do a partial update
+local function edit_blocklist(event)
+ local origin, stanza = event.origin, event.stanza;
+ local username = origin.username;
+ local action = stanza.tags[1]; -- "block" or "unblock"
+ local is_blocking = action.name == "block" or nil; -- nil if unblocking
+ local new = {}; -- JIDs to block depending or unblock on action
+
+ -- XEP-0191 sayeth:
+ -- > When the user blocks communications with the contact, the user's
+ -- > server MUST send unavailable presence information to the contact (but
+ -- > only if the contact is allowed to receive presence notifications [...]
+ -- So contacts we need to do that for are added to the set below.
+ local send_unavailable = is_blocking and {};
+
+ -- Because blocking someone currently also blocks the ability to reject
+ -- subscription requests, we'll preemptively reject such
+ local remove_pending = is_blocking and {};
+
+ for item in action:childtags("item") do
+ local jid = jid_prep(item.attr.jid);
+ if not jid then
+ origin.send(st_error_reply(stanza, "modify", "jid-malformed"));
+ return true;
+ end
+ item.attr.jid = jid; -- echo back prepped
+ new[jid] = true;
+ if is_blocking then
+ if is_contact_subscribed(username, module.host, jid) then
+ send_unavailable[jid] = true;
+ elseif is_contact_pending_in(username, module.host, jid) then
+ remove_pending[jid] = true;
+ end
+ end
+ end
+
+ if is_blocking and not next(new) then
+ -- <block/> element does not contain at least one <item/> child element
+ origin.send(st_error_reply(stanza, "modify", "bad-request"));
+ return true;
+ end
+
+ local blocklist = cache[username] or get_blocklist(username);
+
+ local new_blocklist = {
+ -- We set the [false] key to someting as a signal not to migrate privacy lists
+ [false] = blocklist[false] or { created = os.time(); };
+ };
+ if type(blocklist[false]) == "table" then
+ new_blocklist[false].modified = os.time();
+ end
+
+ if is_blocking or next(new) then
+ for jid in pairs(blocklist) do
+ if jid then new_blocklist[jid] = true; end
+ end
+ for jid in pairs(new) do
+ new_blocklist[jid] = is_blocking;
+ end
+ -- else empty the blocklist
+ end
+
+ local ok, err = set_blocklist(username, new_blocklist);
+ if ok then
+ origin.send(st.reply(stanza));
+ else
+ origin.send(st_error_reply(stanza, "wait", "internal-server-error", err));
+ return true;
+ end
+
+ if is_blocking then
+ for jid in pairs(send_unavailable) do
+ if not blocklist[jid] then
+ for _, session in pairs(sessions[username].sessions) do
+ if session.presence then
+ module:send(st.presence({ type = "unavailable", to = jid, from = session.full_jid }));
+ end
+ end
+ end
+ end
+
+ if next(remove_pending) then
+ local roster = load_roster(username, module.host);
+ for jid in pairs(remove_pending) do
+ roster[false].pending[jid] = nil;
+ end
+ save_roster(username, module.host, roster);
+ -- Not much we can do about save failing here
+ end
+ end
+
+ local blocklist_push = st.iq({ type = "set", id = "blocklist-push" })
+ :add_child(action); -- I am lazy
+
+ for _, session in pairs(sessions[username].sessions) do
+ if session.interested_blocklist then
+ blocklist_push.attr.to = session.full_jid;
+ session.send(blocklist_push);
+ end
+ end
+
+ return true;
+end
+
+module:hook("iq-set/self/urn:xmpp:blocking:block", edit_blocklist, -1);
+module:hook("iq-set/self/urn:xmpp:blocking:unblock", edit_blocklist, -1);
+
+-- Cache invalidation, solved!
+module:hook_global("user-deleted", function (event)
+ if event.host == module.host then
+ cache2:set(event.username, nil);
+ cache[event.username] = nil;
+ end
+end);
+
+-- Buggy clients
+module:hook("iq-error/self/blocklist-push", function (event)
+ local origin, stanza = event.origin, event.stanza;
+ local _, condition, text = stanza:get_error();
+ local log = (origin.log or module._log);
+ log("warn", "Client returned an error in response to notification from mod_%s: %s%s%s",
+ module.name, condition, text and ": " or "", text or "");
+ return true;
+end);
+
+local function is_blocked(user, jid)
+ local blocklist = cache[user] or get_blocklist(user);
+ if blocklist[jid] then return true; end
+ local node, host = jid_split(jid);
+ return blocklist[host] or node and blocklist[node..'@'..host];
+end
+
+-- Event handlers for bouncing or dropping stanzas
+local function drop_stanza(event)
+ local stanza = event.stanza;
+ local attr = stanza.attr;
+ local to, from = attr.to, attr.from;
+ to = to and jid_split(to);
+ if to and from then
+ return is_blocked(to, from);
+ end
+end
+
+local function bounce_stanza(event)
+ local origin, stanza = event.origin, event.stanza;
+ if drop_stanza(event) then
+ origin.send(st_error_reply(stanza, "cancel", "service-unavailable"));
+ return true;
+ end
+end
+
+local function bounce_iq(event)
+ local type = event.stanza.attr.type;
+ if type == "set" or type == "get" then
+ return bounce_stanza(event);
+ end
+ return drop_stanza(event); -- result or error
+end
+
+local function bounce_message(event)
+ local stanza = event.stanza;
+ local type = stanza.attr.type;
+ if type == "chat" or not type or type == "normal" then
+ if full_sessions[stanza.attr.to] then
+ -- See #690
+ return drop_stanza(event);
+ end
+ return bounce_stanza(event);
+ end
+ return drop_stanza(event); -- drop headlines, groupchats etc
+end
+
+local function drop_outgoing(event)
+ local origin, stanza = event.origin, event.stanza;
+ local username = origin.username or jid_split(stanza.attr.from);
+ if not username then return end
+ local to = stanza.attr.to;
+ if to then return is_blocked(username, to); end
+ -- nil 'to' means a self event, don't bock those
+end
+
+local function bounce_outgoing(event)
+ local origin, stanza = event.origin, event.stanza;
+ local type = stanza.attr.type;
+ if type == "error" or stanza.name == "iq" and type == "result" then
+ return drop_outgoing(event);
+ end
+ if drop_outgoing(event) then
+ origin.send(st_error_reply(stanza, "cancel", "not-acceptable", "You have blocked this JID")
+ :tag("blocked", { xmlns = "urn:xmpp:blocking:errors" }));
+ return true;
+ end
+end
+
+-- Hook all the events!
+local prio_in, prio_out = 100, 100;
+module:hook("presence/bare", drop_stanza, prio_in);
+module:hook("presence/full", drop_stanza, prio_in);
+
+module:hook("message/bare", bounce_message, prio_in);
+module:hook("message/full", bounce_message, prio_in);
+
+module:hook("iq/bare", bounce_iq, prio_in);
+module:hook("iq/full", bounce_iq, prio_in);
+
+module:hook("pre-message/bare", bounce_outgoing, prio_out);
+module:hook("pre-message/full", bounce_outgoing, prio_out);
+module:hook("pre-message/host", bounce_outgoing, prio_out);
+
+module:hook("pre-presence/bare", bounce_outgoing, -1);
+module:hook("pre-presence/host", bounce_outgoing, -1);
+module:hook("pre-presence/full", bounce_outgoing, prio_out);
+
+module:hook("pre-iq/bare", bounce_outgoing, prio_out);
+module:hook("pre-iq/full", bounce_outgoing, prio_out);
+module:hook("pre-iq/host", bounce_outgoing, prio_out);
+
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua
index 1eb95e90..8cda4a23 100644
--- a/plugins/mod_bosh.lua
+++ b/plugins/mod_bosh.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -13,7 +13,6 @@ local new_xmpp_stream = require "util.xmppstream".new;
local sm = require "core.sessionmanager";
local sm_destroy_session = sm.destroy_session;
local new_uuid = require "util.uuid".generate;
-local fire_event = prosody.events.fire_event;
local core_process_stanza = prosody.core_process_stanza;
local st = require "util.stanza";
local logger = require "util.logger";
@@ -22,6 +21,7 @@ local initialize_filters = require "util.filters".initialize;
local math_min = math.min;
local xpcall, tostring, type = xpcall, tostring, type;
local traceback = debug.traceback;
+local nameprep = require "util.encodings".stringprep.nameprep;
local xmlns_streams = "http://etherx.jabber.org/streams";
local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
@@ -30,33 +30,25 @@ local xmlns_bosh = "http://jabber.org/protocol/httpbind"; -- (hard-coded into a
local stream_callbacks = {
stream_ns = xmlns_bosh, stream_tag = "body", default_ns = "jabber:client" };
-local BOSH_DEFAULT_HOLD = module:get_option_number("bosh_default_hold", 1);
-local BOSH_DEFAULT_INACTIVITY = module:get_option_number("bosh_max_inactivity", 60);
-local BOSH_DEFAULT_POLLING = module:get_option_number("bosh_max_polling", 5);
-local BOSH_DEFAULT_REQUESTS = module:get_option_number("bosh_max_requests", 2);
+-- These constants are implicitly assumed within the code, and cannot be changed
+local BOSH_HOLD = 1;
+local BOSH_MAX_REQUESTS = 2;
+
+-- The number of seconds a BOSH session should remain open with no requests
+local bosh_max_inactivity = module:get_option_number("bosh_max_inactivity", 60);
+-- The minimum amount of time between requests with no payload
+local bosh_max_polling = module:get_option_number("bosh_max_polling", 5);
+-- The maximum amount of time that the server will hold onto a request before replying
+-- (the client can set this to a lower value when it connects, if it chooses)
local bosh_max_wait = module:get_option_number("bosh_max_wait", 120);
local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure");
-
-local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" };
-
local cross_domain = module:get_option("cross_domain_bosh", false);
-if cross_domain then
- default_headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
- default_headers["Access-Control-Allow-Headers"] = "Content-Type";
- default_headers["Access-Control-Max-Age"] = "7200";
-
- if cross_domain == true then
- default_headers["Access-Control-Allow-Origin"] = "*";
- elseif type(cross_domain) == "table" then
- cross_domain = table.concat(cross_domain, ", ");
- end
- if type(cross_domain) == "string" then
- default_headers["Access-Control-Allow-Origin"] = cross_domain;
- end
-end
-local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items;
+if cross_domain == true then cross_domain = "*"; end
+if type(cross_domain) == "table" then cross_domain = table.concat(cross_domain, ", "); end
+
+local trusted_proxies = module:get_option_set("trusted_proxies", { "127.0.0.1", "::1" })._items;
local function get_ip_from_request(request)
local ip = request.conn:ip();
@@ -79,7 +71,7 @@ local os_time = os.time;
local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions");
-- Used to respond to idle sessions (those with waiting requests)
-local waiting_requests = {};
+local waiting_requests = module:shared("waiting_requests");
function on_destroy_request(request)
log("debug", "Request destroyed: %s", tostring(request));
waiting_requests[request] = nil;
@@ -92,7 +84,7 @@ function on_destroy_request(request)
break;
end
end
-
+
-- If this session now has no requests open, mark it as inactive
local max_inactive = session.bosh_max_inactive;
if max_inactive and #requests == 0 then
@@ -102,11 +94,20 @@ function on_destroy_request(request)
end
end
-function handle_OPTIONS(request)
- local headers = {};
- for k,v in pairs(default_headers) do headers[k] = v; end
- headers["Content-Type"] = nil;
- return { headers = headers, body = "" };
+local function set_cross_domain_headers(response)
+ local headers = response.headers;
+ headers.access_control_allow_methods = "GET, POST, OPTIONS";
+ headers.access_control_allow_headers = "Content-Type";
+ headers.access_control_max_age = "7200";
+ headers.access_control_allow_origin = cross_domain;
+ return response;
+end
+
+function handle_OPTIONS(event)
+ if cross_domain and event.request.headers.origin then
+ set_cross_domain_headers(event.response);
+ end
+ return "";
end
function handle_POST(event)
@@ -119,14 +120,27 @@ function handle_POST(event)
local context = { request = request, response = response, notopen = true };
local stream = new_xmpp_stream(context, stream_callbacks);
response.context = context;
-
+
+ local headers = response.headers;
+ headers.content_type = "text/xml; charset=utf-8";
+
+ if cross_domain and event.request.headers.origin then
+ set_cross_domain_headers(response);
+ end
+
-- stream:feed() calls the stream_callbacks, so all stanzas in
-- the body are processed in this next line before it returns.
-- In particular, the streamopened() stream callback is where
-- much of the session logic happens, because it's where we first
-- get to see the 'sid' of this request.
- stream:feed(body);
-
+ local ok, err = stream:feed(body);
+ if not ok then
+ module:log("warn", "Error parsing BOSH payload; %s", err)
+ local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+ ["xmlns:stream"] = xmlns_streams, condition = "bad-request" });
+ return tostring(close_reply);
+ end
+
-- Stanzas (if any) in the request have now been processed, and
-- we take care of the high-level BOSH logic here, including
-- giving a response or putting the request "on hold".
@@ -139,12 +153,9 @@ function handle_POST(event)
end
local r = session.requests;
- log("debug", "Session %s has %d out of %d requests open", context.sid, #r, session.bosh_hold);
+ log("debug", "Session %s has %d out of %d requests open", context.sid, #r, BOSH_HOLD);
log("debug", "and there are %d things in the send_buffer:", #session.send_buffer);
- for i, thing in ipairs(session.send_buffer) do
- log("debug", " %s", tostring(thing));
- end
- if #r > session.bosh_hold then
+ if #r > BOSH_HOLD then
-- We are holding too many requests, send what's in the buffer,
log("debug", "We are holding too many requests, so...");
if #session.send_buffer > 0 then
@@ -162,7 +173,7 @@ function handle_POST(event)
session.send_buffer = {};
session.send(resp);
end
-
+
if not response.finished then
-- We're keeping this request open, to respond later
log("debug", "Have nothing to say, so leaving request unanswered for now");
@@ -170,7 +181,7 @@ function handle_POST(event)
waiting_requests[response] = os_time() + session.bosh_wait;
end
end
-
+
if session.bosh_terminate then
session.log("debug", "Closing session with %d requests open", #session.requests);
session:close();
@@ -178,7 +189,13 @@ function handle_POST(event)
else
return true; -- Inform http server we shall reply later
end
+ elseif response.finished then
+ return; -- A response has been sent already
end
+ module:log("warn", "Unable to associate request with a session (incomplete request?)");
+ local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+ ["xmlns:stream"] = xmlns_streams, condition = "item-not-found" });
+ return tostring(close_reply) .. "\n";
end
@@ -188,10 +205,10 @@ local stream_xmlns_attr = { xmlns = "urn:ietf:params:xml:ns:xmpp-streams" };
local function bosh_close_stream(session, reason)
(session.log or log)("info", "BOSH client disconnected");
-
+
local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
["xmlns:stream"] = xmlns_streams });
-
+
if reason then
close_reply.attr.condition = "remote-stream-error";
@@ -217,10 +234,9 @@ local function bosh_close_stream(session, reason)
local response_body = tostring(close_reply);
for _, held_request in ipairs(session.requests) do
- held_request.headers = default_headers;
held_request:send(response_body);
end
- sessions[session.sid] = nil;
+ sessions[session.sid] = nil;
inactive_sessions[session] = nil;
sm_destroy_session(session);
end
@@ -233,9 +249,17 @@ function stream_callbacks.streamopened(context, attr)
if not sid then
-- New session request
context.notopen = nil; -- Signals that we accept this opening tag
-
- -- TODO: Sanity checks here (rid, to, known host, etc.)
- if not hosts[attr.to] then
+
+ local to_host = nameprep(attr.to);
+ local rid = tonumber(attr.rid);
+ local wait = tonumber(attr.wait);
+ if not to_host then
+ log("debug", "BOSH client tried to connect to invalid host: %s", tostring(attr.to));
+ local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+ ["xmlns:stream"] = xmlns_streams, condition = "improper-addressing" });
+ response:send(tostring(close_reply));
+ return;
+ elseif not hosts[to_host] then
-- Unknown host
log("debug", "BOSH client tried to connect to unknown host: %s", tostring(attr.to));
local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
@@ -243,25 +267,37 @@ function stream_callbacks.streamopened(context, attr)
response:send(tostring(close_reply));
return;
end
-
+ if not rid or (not wait and attr.wait or wait < 0 or wait % 1 ~= 0) then
+ log("debug", "BOSH client sent invalid rid or wait attributes: rid=%s, wait=%s", tostring(attr.rid), tostring(attr.wait));
+ local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+ ["xmlns:stream"] = xmlns_streams, condition = "bad-request" });
+ response:send(tostring(close_reply));
+ return;
+ end
+
+ rid = rid - 1;
+ wait = math_min(wait, bosh_max_wait);
+
-- New session
sid = new_uuid();
local session = {
- type = "c2s_unauthed", conn = request.conn, sid = sid, rid = tonumber(attr.rid)-1, host = attr.to,
- bosh_version = attr.ver, bosh_wait = math_min(attr.wait, bosh_max_wait), streamid = sid,
- bosh_hold = BOSH_DEFAULT_HOLD, bosh_max_inactive = BOSH_DEFAULT_INACTIVITY,
+ type = "c2s_unauthed", conn = request.conn, sid = sid, rid = rid, host = to_host,
+ bosh_version = attr.ver, bosh_wait = wait, streamid = sid,
+ bosh_max_inactive = bosh_max_inactivity,
requests = { }, send_buffer = {}, reset_stream = bosh_reset_stream,
close = bosh_close_stream, dispatch_stanza = core_process_stanza, notopen = true,
log = logger.init("bosh"..sid), secure = consider_bosh_secure or request.secure,
ip = get_ip_from_request(request);
};
sessions[sid] = session;
-
+
local filter = initialize_filters(session);
-
+
session.log("debug", "BOSH session created for request from %s", session.ip);
log("info", "New BOSH session, assigned it sid '%s'", sid);
+ hosts[session.host].events.fire_event("bosh-session", { session = session, request = request });
+
-- Send creation response
local creating_session = true;
@@ -274,12 +310,12 @@ function stream_callbacks.streamopened(context, attr)
end
s = filter("stanzas/out", s);
--log("debug", "Sending BOSH data: %s", tostring(s));
+ if not s then return true end
t_insert(session.send_buffer, tostring(s));
local oldest_request = r[1];
if oldest_request and not session.bosh_processing then
log("debug", "We have an open request, so sending on that");
- oldest_request.headers = default_headers;
local body_attr = { xmlns = "http://jabber.org/protocol/httpbind",
["xmlns:stream"] = "http://etherx.jabber.org/streams";
type = session.bosh_terminate and "terminate" or nil;
@@ -287,11 +323,11 @@ function stream_callbacks.streamopened(context, attr)
};
if creating_session then
creating_session = nil;
- body_attr.inactivity = tostring(BOSH_DEFAULT_INACTIVITY);
- body_attr.polling = tostring(BOSH_DEFAULT_POLLING);
- body_attr.requests = tostring(BOSH_DEFAULT_REQUESTS);
+ body_attr.requests = tostring(BOSH_MAX_REQUESTS);
+ body_attr.hold = tostring(BOSH_HOLD);
+ body_attr.inactivity = tostring(bosh_max_inactivity);
+ body_attr.polling = tostring(bosh_max_polling);
body_attr.wait = tostring(session.bosh_wait);
- body_attr.hold = tostring(session.bosh_hold);
body_attr.authid = sid;
body_attr.secure = "true";
body_attr.ver = '1.6';
@@ -299,43 +335,55 @@ function stream_callbacks.streamopened(context, attr)
body_attr["xmlns:xmpp"] = "urn:xmpp:xbosh";
body_attr["xmpp:version"] = "1.0";
end
- oldest_request:send(st.stanza("body", body_attr):top_tag()..t_concat(session.send_buffer).."</body>");
+ session.bosh_last_response = st.stanza("body", body_attr):top_tag()..t_concat(session.send_buffer).."</body>";
+ oldest_request:send(session.bosh_last_response);
session.send_buffer = {};
end
return true;
end
request.sid = sid;
end
-
+
local session = sessions[sid];
if not session then
-- Unknown sid
log("info", "Client tried to use sid '%s' which we don't know about", sid);
- response.headers = default_headers;
response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" })));
context.notopen = nil;
return;
end
session.conn = request.conn;
-
+
if session.rid then
local rid = tonumber(attr.rid);
local diff = rid - session.rid;
- if diff > 1 then
- session.log("warn", "rid too large (means a request was lost). Last rid: %d New rid: %s", session.rid, attr.rid);
- elseif diff <= 0 then
- -- Repeated, ignore
- session.log("debug", "rid repeated, ignoring: %s (diff %d)", session.rid, diff);
+ -- Diff should be 1 for a healthy request
+ if diff ~= 1 then
+ context.sid = sid;
context.notopen = nil;
+ if diff == 2 then
+ -- Hold request, but don't process it (ouch!)
+ session.log("debug", "rid skipped: %d, deferring this request", rid-1)
+ context.defer = true;
+ session.bosh_deferred = { context = context, sid = sid, rid = rid, terminate = attr.type == "terminate" };
+ return;
+ end
context.ignore = true;
- context.sid = sid;
- t_insert(session.requests, response);
+ if diff == 0 then
+ -- Re-send previous response, ignore stanzas in this request
+ session.log("debug", "rid repeated, ignoring: %s (diff %d)", session.rid, diff);
+ response:send(session.bosh_last_response);
+ return;
+ end
+ -- Session broken, destroy it
+ session.log("debug", "rid out of range: %d (diff %d)", rid, diff);
+ response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" })));
return;
end
session.rid = rid;
end
-
+
if attr.type == "terminate" then
-- Client wants to end this session, which we'll do
-- after processing any stanzas in this request
@@ -350,8 +398,7 @@ function stream_callbacks.streamopened(context, attr)
if session.notopen then
local features = st.stanza("stream:features");
hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
- fire_event("stream-features", session, features);
- session.send(tostring(features));
+ session.send(features);
session.notopen = nil;
end
end
@@ -365,16 +412,38 @@ function stream_callbacks.handlestanza(context, stanza)
if stanza.attr.xmlns == xmlns_bosh then
stanza.attr.xmlns = nil;
end
- stanza = session.filter("stanzas/in", stanza);
- if stanza then
- return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
+ if context.defer and session.bosh_deferred then
+ log("debug", "Deferring this stanza");
+ t_insert(session.bosh_deferred, stanza);
+ else
+ stanza = session.filter("stanzas/in", stanza);
+ if stanza then
+ return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
+ end
end
+ else
+ log("debug", "No session for this stanza! (sid: %s)", context.sid or "none!");
end
end
-function stream_callbacks.streamclosed(request)
- local session = sessions[request.sid];
+function stream_callbacks.streamclosed(context)
+ local session = sessions[context.sid];
if session then
+ if not context.defer and session.bosh_deferred then
+ -- Handle deferred stanzas now
+ local deferred_stanzas = session.bosh_deferred;
+ local context = deferred_stanzas.context;
+ session.bosh_deferred = nil;
+ log("debug", "Handling deferred stanzas from rid %d", deferred_stanzas.rid);
+ session.rid = deferred_stanzas.rid;
+ t_insert(session.requests, context.response);
+ for _, stanza in ipairs(deferred_stanzas) do
+ stream_callbacks.handlestanza(context, stanza);
+ end
+ if deferred_stanzas.terminate then
+ session.bosh_terminate = true;
+ end
+ end
session.bosh_processing = false;
if #session.send_buffer > 0 then
session.send("");
@@ -386,12 +455,12 @@ function stream_callbacks.error(context, error)
log("debug", "Error parsing BOSH request payload; %s", error);
if not context.sid then
local response = context.response;
- response.headers = default_headers;
- response.status_code = 400;
- response:send();
+ local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
+ ["xmlns:stream"] = xmlns_streams, condition = "bad-request" });
+ response:send(tostring(close_reply));
return;
end
-
+
local session = sessions[context.sid];
if error == "stream-error" then -- Remote stream error, we close normally
session:close();
@@ -400,7 +469,7 @@ function stream_callbacks.error(context, error)
end
end
-local dead_sessions = {};
+local dead_sessions = module:shared("dead_sessions");
function on_timer()
-- log("debug", "Checking for requests soon to timeout...");
-- Identify requests timing out within the next few seconds
@@ -415,7 +484,7 @@ function on_timer()
end
end
end
-
+
now = now - 3;
local n_dead_sessions = 0;
for session, close_after in pairs(inactive_sessions) do
diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua
index 2848f92f..7f0d1b01 100644
--- a/plugins/mod_c2s.lua
+++ b/plugins/mod_c2s.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -23,20 +23,29 @@ local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
local log = module._log;
-local c2s_timeout = module:get_option_number("c2s_timeout");
+local c2s_timeout = module:get_option_number("c2s_timeout", 300);
local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5);
local opt_keepalives = module:get_option_boolean("c2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true));
+local measure_connections = module:measure("connections", "amount");
+
local sessions = module:shared("sessions");
local core_process_stanza = prosody.core_process_stanza;
local hosts = prosody.hosts;
-local stream_callbacks = { default_ns = "jabber:client", handlestanza = core_process_stanza };
+local stream_callbacks = { default_ns = "jabber:client" };
local listener = {};
+module:hook("stats-update", function ()
+ local count = 0;
+ for _ in pairs(sessions) do
+ count = count + 1;
+ end
+ measure_connections(count);
+end);
+
--- Stream events handlers
local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
-local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
function stream_callbacks.streamopened(session, attr)
local send = session.send;
@@ -57,15 +66,13 @@ function stream_callbacks.streamopened(session, attr)
session.streamid = uuid_generate();
(session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host);
- if not hosts[session.host] or not hosts[session.host].users then
+ if not hosts[session.host] or not hosts[session.host].modules.c2s then
-- We don't serve this host...
session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)};
return;
end
- send("<?xml version='1.0'?>"..st.stanza("stream:stream", {
- xmlns = 'jabber:client', ["xmlns:stream"] = 'http://etherx.jabber.org/streams';
- id = session.streamid, from = session.host, version = '1.0', ["xml:lang"] = 'en' }):top_tag());
+ session:open_stream();
(session.log or log)("debug", "Sent reply <stream:stream> to client");
session.notopen = nil;
@@ -74,21 +81,27 @@ function stream_callbacks.streamopened(session, attr)
-- since we now have a new stream header, session is secured
if session.secure == false then
session.secure = true;
+ session.encrypted = true;
- -- Check if TLS compression is used
local sock = session.conn:socket();
if sock.info then
- session.compressed = sock:info"compression";
- elseif sock.compression then
- session.compressed = sock:compression(); --COMPAT mw/luasec-hg
+ local info = sock:info();
+ (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
+ session.compressed = info.compression;
+ else
+ (session.log or log)("info", "Stream encrypted");
+ session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg
end
end
local features = st.stanza("stream:features");
hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
- module:fire_event("stream-features", session, features);
-
- send(features);
+ if features.tags[1] or session.full_jid then
+ send(features);
+ else
+ (session.log or log)("warn", "No stream features to offer");
+ session:close{ condition = "undefined-condition", text = "No stream features to proceed with" };
+ end
end
function stream_callbacks.streamclosed(session)
@@ -134,8 +147,7 @@ local function session_close(session, reason)
local log = session.log or log;
if session.conn then
if session.notopen then
- session.send("<?xml version='1.0'?>");
- session.send(st.stanza("stream:stream", default_stream_attr):top_tag());
+ session:open_stream();
end
if reason then -- nil == no err, initiated by us, false == initiated by client
local stream_error = st.stanza("stream:error");
@@ -158,31 +170,31 @@ local function session_close(session, reason)
log("debug", "Disconnecting client, <stream:error> is: %s", stream_error);
session.send(stream_error);
end
-
+
session.send("</stream:stream>");
function session.send() return false; end
-
- local reason = (reason and (reason.name or reason.text or reason.condition)) or reason;
- session.log("info", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed");
+
+ local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason;
+ session.log("debug", "c2s stream for %s closed: %s", session.full_jid or session.ip or "<unknown>", reason_text or "session closed");
-- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
local conn = session.conn;
- if reason == nil and not session.notopen and session.type == "c2s" then
+ if reason_text == nil and not session.notopen and session.type == "c2s" then
-- Grace time to process data from authenticated cleanly-closed stream
add_task(stream_close_timeout, function ()
if not session.destroyed then
session.log("warn", "Failed to receive a stream close response, closing connection anyway...");
- sm_destroy_session(session, reason);
+ sm_destroy_session(session, reason_text);
conn:close();
end
end);
else
- sm_destroy_session(session, reason);
+ sm_destroy_session(session, reason_text);
conn:close();
end
else
- local reason = (reason and (reason.name or reason.text or reason.condition)) or reason;
- sm_destroy_session(session, reason);
+ local reason_text = (reason and (reason.name or reason.text or reason.condition)) or reason;
+ sm_destroy_session(session, reason_text);
end
end
@@ -190,22 +202,35 @@ module:hook_global("user-deleted", function(event)
local username, host = event.username, event.host;
local user = hosts[host].sessions[username];
if user and user.sessions then
- for jid, session in pairs(user.sessions) do
+ for _, session in pairs(user.sessions) do
session:close{ condition = "not-authorized", text = "Account deleted" };
end
end
end, 200);
+module:hook_global("user-password-changed", function(event)
+ local username, host, resource = event.username, event.host, event.resource;
+ local user = hosts[host].sessions[username];
+ if user and user.sessions then
+ for r, session in pairs(user.sessions) do
+ if r ~= resource then
+ session:close{ condition = "reset", text = "Password changed" };
+ end
+ end
+ end
+end, 200);
+
--- Port listener
function listener.onconnect(conn)
local session = sm_new_session(conn);
sessions[conn] = session;
-
+
session.log("info", "Client connected");
-
+
-- Client is using legacy SSL (otherwise mod_tls sets this flag)
if conn:ssl() then
session.secure = true;
+ session.encrypted = true;
-- Check if TLS compression is used
local sock = conn:socket();
@@ -215,34 +240,37 @@ function listener.onconnect(conn)
session.compressed = sock:compression(); --COMPAT mw/luasec-hg
end
end
-
+
if opt_keepalives then
conn:setoption("keepalive", opt_keepalives);
end
-
+
session.close = session_close;
-
+
local stream = new_xmpp_stream(session, stream_callbacks);
session.stream = stream;
session.notopen = true;
-
+
function session.reset_stream()
session.notopen = true;
session.stream:reset();
end
-
+
local filter = session.filter;
function session.data(data)
- data = filter("bytes/in", data);
+ -- Parse the data, which will store stanzas in session.pending_stanzas
if data then
- local ok, err = stream:feed(data);
- if ok then return; end
- log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
- session:close("not-well-formed");
+ data = filter("bytes/in", data);
+ if data then
+ local ok, err = stream:feed(data);
+ if not ok then
+ log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
+ session:close("not-well-formed");
+ end
+ end
end
end
-
if c2s_timeout then
add_task(c2s_timeout, function ()
if session.type == "c2s_unauthed" then
@@ -271,14 +299,30 @@ function listener.ondisconnect(conn, err)
end
end
+function listener.onreadtimeout(conn)
+ local session = sessions[conn];
+ if session then
+ return (hosts[session.host] or prosody).events.fire_event("c2s-read-timeout", { session = session });
+ end
+end
+
+local function keepalive(event)
+ local session = event.session;
+ if not session.notopen then
+ return event.session.send(' ');
+ end
+end
+
function listener.associate_session(conn, session)
sessions[conn] = session;
end
-function listener.ondetach(conn)
- sessions[conn] = nil;
+function module.add_host(module)
+ module:hook("c2s-read-timeout", keepalive, -1);
end
+module:hook("c2s-read-timeout", keepalive, -1);
+
module:hook("server-stopping", function(event)
local reason = event.reason;
for _, session in pairs(sessions) do
diff --git a/plugins/mod_carbons.lua b/plugins/mod_carbons.lua
new file mode 100644
index 00000000..1dcd4a07
--- /dev/null
+++ b/plugins/mod_carbons.lua
@@ -0,0 +1,115 @@
+-- XEP-0280: Message Carbons implementation for Prosody
+-- Copyright (C) 2011-2016 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+
+local st = require "util.stanza";
+local jid_bare = require "util.jid".bare;
+local xmlns_carbons = "urn:xmpp:carbons:2";
+local xmlns_forward = "urn:xmpp:forward:0";
+local full_sessions, bare_sessions = prosody.full_sessions, prosody.bare_sessions;
+
+local function toggle_carbons(event)
+ local origin, stanza = event.origin, event.stanza;
+ local state = stanza.tags[1].name;
+ module:log("debug", "%s %sd carbons", origin.full_jid, state);
+ origin.want_carbons = state == "enable" and stanza.tags[1].attr.xmlns;
+ origin.send(st.reply(stanza));
+ return true;
+end
+module:hook("iq-set/self/"..xmlns_carbons..":disable", toggle_carbons);
+module:hook("iq-set/self/"..xmlns_carbons..":enable", toggle_carbons);
+
+local function message_handler(event, c2s)
+ local origin, stanza = event.origin, event.stanza;
+ local orig_type = stanza.attr.type or "normal";
+ local orig_from = stanza.attr.from;
+ local bare_from = jid_bare(orig_from);
+ local orig_to = stanza.attr.to;
+ local bare_to = jid_bare(orig_to);
+
+ if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body"))) then
+ return -- Only chat type messages
+ end
+
+ -- Stanza sent by a local client
+ local bare_jid = bare_from; -- JID of the local user
+ local target_session = origin;
+ local top_priority = false;
+ local user_sessions = bare_sessions[bare_from];
+
+ -- Stanza about to be delivered to a local client
+ if not c2s then
+ bare_jid = bare_to;
+ target_session = full_sessions[orig_to];
+ user_sessions = bare_sessions[bare_jid];
+ if not target_session and user_sessions then
+ -- The top resources will already receive this message per normal routing rules,
+ -- so we are going to skip them in order to avoid sending duplicated messages.
+ local top_resources = user_sessions.top_resources;
+ top_priority = top_resources and top_resources[1].priority
+ end
+ end
+
+ if not user_sessions then
+ module:log("debug", "Skip carbons for offline user");
+ return -- No use in sending carbons to an offline user
+ end
+
+ if stanza:get_child("private", xmlns_carbons) then
+ if not c2s then
+ stanza:maptags(function(tag)
+ if not ( tag.attr.xmlns == xmlns_carbons and tag.name == "private" ) then
+ return tag;
+ end
+ end);
+ end
+ module:log("debug", "Message tagged private, ignoring");
+ return
+ elseif stanza:get_child("no-copy", "urn:xmpp:hints") then
+ module:log("debug", "Message has no-copy hint, ignoring");
+ return
+ elseif not c2s and bare_jid == orig_from and stanza:get_child("x", "http://jabber.org/protocol/muc#user") then
+ module:log("debug", "MUC PM, ignoring");
+ return
+ end
+
+ -- Create the carbon copy and wrap it as per the Stanza Forwarding XEP
+ local copy = st.clone(stanza);
+ if c2s and not orig_to then
+ stanza.attr.to = bare_from;
+ end
+ copy.attr.xmlns = "jabber:client";
+ local carbon = st.message{ from = bare_jid, type = orig_type, }
+ :tag(c2s and "sent" or "received", { xmlns = xmlns_carbons })
+ :tag("forwarded", { xmlns = xmlns_forward })
+ :add_child(copy):reset();
+
+ user_sessions = user_sessions and user_sessions.sessions;
+ for _, session in pairs(user_sessions) do
+ -- Carbons are sent to resources that have enabled it
+ if session.want_carbons
+ -- but not the resource that sent the message, or the one that it's directed to
+ and session ~= target_session
+ -- and isn't among the top resources that would receive the message per standard routing rules
+ and (c2s or session.priority ~= top_priority) then
+ carbon.attr.to = session.full_jid;
+ module:log("debug", "Sending carbon to %s", session.full_jid);
+ session.send(carbon);
+ end
+ end
+end
+
+local function c2s_message_handler(event)
+ return message_handler(event, true)
+end
+
+-- Stanzas sent by local clients
+module:hook("pre-message/host", c2s_message_handler, -0.5);
+module:hook("pre-message/bare", c2s_message_handler, -0.5);
+module:hook("pre-message/full", c2s_message_handler, -0.5);
+-- Stanzas to local clients
+module:hook("message/bare", message_handler, -0.5);
+module:hook("message/full", message_handler, -0.5);
+
+module:add_feature(xmlns_carbons);
diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua
index acd70c60..15f405e5 100644
--- a/plugins/mod_component.lua
+++ b/plugins/mod_component.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -29,41 +29,50 @@ local opt_keepalives = module:get_option_boolean("component_tcp_keepalives", mod
local sessions = module:shared("sessions");
+local function keepalive(event)
+ local session = event.session;
+ if not session.notopen then
+ return event.session.send(' ');
+ end
+end
+
function module.add_host(module)
if module:get_host_type() ~= "component" then
error("Don't load mod_component manually, it should be for a component, please see http://prosody.im/doc/components", 0);
end
-
+
local env = module.environment;
env.connected = false;
+ env.session = false;
local send;
- local function on_destroy(session, err)
+ local function on_destroy(session, err) --luacheck: ignore 212/err
env.connected = false;
+ env.session = false;
send = nil;
session.on_destroy = nil;
end
-
+
-- Handle authentication attempts by component
local function handle_component_auth(event)
local session, stanza = event.origin, event.stanza;
-
+
if session.type ~= "component_unauthed" then return; end
-
+
if (not session.host) or #stanza.tags > 0 then
(session.log or log)("warn", "Invalid component handshake for host: %s", session.host);
session:close("not-authorized");
return true;
end
-
- local secret = module:get_option("component_secret");
+
+ local secret = module:get_option_string("component_secret");
if not secret then
(session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.host);
session:close("not-authorized");
return true;
end
-
+
local supplied_token = t_concat(stanza);
local calculated_token = sha1(session.streamid..secret, true);
if supplied_token:lower() ~= calculated_token:lower() then
@@ -71,14 +80,20 @@ function module.add_host(module)
session:close{ condition = "not-authorized", text = "Given token does not match calculated token" };
return true;
end
-
+
if env.connected then
- module:log("error", "Second component attempted to connect, denying connection");
- session:close{ condition = "conflict", text = "Component already connected" };
- return true;
+ local policy = module:get_option_string("component_conflict_resolve", "kick_new");
+ if policy == "kick_old" then
+ env.session:close{ condition = "conflict", text = "Replaced by a new connection" };
+ else -- kick_new
+ module:log("error", "Second component attempted to connect, denying connection");
+ session:close{ condition = "conflict", text = "Component already connected" };
+ return true;
+ end
end
-
+
env.connected = true;
+ env.session = session;
send = session.send;
session.on_destroy = on_destroy;
session.component_validate_from = module:get_option_boolean("validate_from_addresses", true);
@@ -86,7 +101,7 @@ function module.add_host(module)
module:log("info", "External component successfully authenticated");
session.send(st.stanza("handshake"));
module:fire_event("component-authenticated", { session = session });
-
+
return true;
end
module:hook("stanza/jabber:component:accept:handshake", handle_component_auth, -1);
@@ -105,7 +120,8 @@ function module.add_host(module)
local name = module:get_option_string("name");
if name then
event.origin.send(st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info" })
- :tag("identity", { category = "component", type = "generic", name = module:get_option_string("name", "Prosody") }))
+ :tag("identity", { category = "component", type = "generic", name = module:get_option_string("name", "Prosody") })):up()
+ :tag("feature", { var = "http://jabber.org/protocol/disco#info" }):up()
return true;
end
end
@@ -117,7 +133,7 @@ function module.add_host(module)
end
return true;
end
-
+
module:hook("iq/bare", handle_stanza, -1);
module:hook("message/bare", handle_stanza, -1);
module:hook("presence/bare", handle_stanza, -1);
@@ -127,8 +143,12 @@ function module.add_host(module)
module:hook("iq/host", handle_stanza, -1);
module:hook("message/host", handle_stanza, -1);
module:hook("presence/host", handle_stanza, -1);
+
+ module:hook("component-read-timeout", keepalive, -1);
end
+module:hook("component-read-timeout", keepalive, -1);
+
--- Network and stream part ---
local xmlns_component = 'jabber:component:accept';
@@ -141,7 +161,7 @@ local stream_callbacks = { default_ns = xmlns_component };
local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
-function stream_callbacks.error(session, error, data, data2)
+function stream_callbacks.error(session, error, data)
if session.destroyed then return; end
module:log("warn", "Error processing component stream: %s", tostring(error));
if error == "no-stream" then
@@ -176,9 +196,7 @@ function stream_callbacks.streamopened(session, attr)
session.streamid = uuid_gen();
session.notopen = nil;
-- Return stream header
- session.send("<?xml version='1.0'?>");
- session.send(st.stanza("stream:stream", { xmlns=xmlns_component,
- ["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.host }):top_tag());
+ session:open_stream();
end
function stream_callbacks.streamclosed(session)
@@ -274,26 +292,26 @@ function listener.onconnect(conn)
if opt_keepalives then
conn:setoption("keepalive", opt_keepalives);
end
-
+
session.log("info", "Incoming Jabber component connection");
-
+
local stream = new_xmpp_stream(session, stream_callbacks);
session.stream = stream;
-
+
session.notopen = true;
-
+
function session.reset_stream()
session.notopen = true;
session.stream:reset();
end
- function session.data(conn, data)
+ function session.data(_, data)
local ok, err = stream:feed(data);
if ok then return; end
module:log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
session:close("not-well-formed");
end
-
+
session.dispatch_stanza = stream_callbacks.handlestanza;
sessions[conn] = session;
@@ -306,6 +324,9 @@ function listener.ondisconnect(conn, err)
local session = sessions[conn];
if session then
(session.log or log)("info", "component disconnected: %s (%s)", tostring(session.host), tostring(err));
+ if session.host then
+ module:context(session.host):fire_event("component-disconnected", { session = session, reason = err });
+ end
if session.on_destroy then session:on_destroy(err); end
sessions[conn] = nil;
for k in pairs(session) do
@@ -314,7 +335,6 @@ function listener.ondisconnect(conn, err)
end
end
session.destroyed = true;
- session = nil;
end
end
@@ -322,6 +342,13 @@ function listener.ondetach(conn)
sessions[conn] = nil;
end
+function listener.onreadtimeout(conn)
+ local session = sessions[conn];
+ if session then
+ return (hosts[session.host] or prosody).events.fire_event("component-read-timeout", { session = session });
+ end
+end
+
module:provides("net", {
name = "component";
private = true;
diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua
index 1ec4c85a..17be2ef2 100644
--- a/plugins/mod_compression.lua
+++ b/plugins/mod_compression.lua
@@ -1,201 +1,9 @@
-- Prosody IM
--- Copyright (C) 2009-2012 Tobias Markmann
---
+-- Copyright (C) 2016 Matthew Wild
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-local st = require "util.stanza";
-local zlib = require "zlib";
-local pcall = pcall;
-local tostring = tostring;
-
-local xmlns_compression_feature = "http://jabber.org/features/compress"
-local xmlns_compression_protocol = "http://jabber.org/protocol/compress"
-local xmlns_stream = "http://etherx.jabber.org/streams";
-local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up();
-local add_filter = require "util.filters".add_filter;
-
-local compression_level = module:get_option_number("compression_level", 7);
-
-if not compression_level or compression_level < 1 or compression_level > 9 then
- module:log("warn", "Invalid compression level in config: %s", tostring(compression_level));
- module:log("warn", "Module loading aborted. Compression won't be available.");
- return;
-end
-
-module:hook("stream-features", function(event)
- local origin, features = event.origin, event.features;
- if not origin.compressed and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then
- -- FIXME only advertise compression support when TLS layer has no compression enabled
- features:add_child(compression_stream_feature);
- end
-end);
-
-module:hook("s2s-stream-features", function(event)
- local origin, features = event.origin, event.features;
- -- FIXME only advertise compression support when TLS layer has no compression enabled
- if not origin.compressed and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then
- features:add_child(compression_stream_feature);
- end
-end);
-
--- Hook to activate compression if remote server supports it.
-module:hook_stanza(xmlns_stream, "features",
- function (session, stanza)
- if not session.compressed and (session.type == "c2s" or session.type == "s2sin" or session.type == "s2sout") then
- -- does remote server support compression?
- local comp_st = stanza:child_with_name("compression");
- if comp_st then
- -- do we support the mechanism
- for a in comp_st:children() do
- local algorithm = a[1]
- if algorithm == "zlib" then
- session.sends2s(st.stanza("compress", {xmlns=xmlns_compression_protocol}):tag("method"):text("zlib"))
- session.log("debug", "Enabled compression using zlib.")
- return true;
- end
- end
- session.log("debug", "Remote server supports no compression algorithm we support.")
- end
- end
- end
-, 250);
-
-
--- returns either nil or a fully functional ready to use inflate stream
-local function get_deflate_stream(session)
- local status, deflate_stream = pcall(zlib.deflate, compression_level);
- if status == false then
- local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
- (session.sends2s or session.send)(error_st);
- session.log("error", "Failed to create zlib.deflate filter.");
- module:log("error", "%s", tostring(deflate_stream));
- return
- end
- return deflate_stream
-end
-
--- returns either nil or a fully functional ready to use inflate stream
-local function get_inflate_stream(session)
- local status, inflate_stream = pcall(zlib.inflate);
- if status == false then
- local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
- (session.sends2s or session.send)(error_st);
- session.log("error", "Failed to create zlib.inflate filter.");
- module:log("error", "%s", tostring(inflate_stream));
- return
- end
- return inflate_stream
-end
-
--- setup compression for a stream
-local function setup_compression(session, deflate_stream)
- add_filter(session, "bytes/out", function(t)
- local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
- if status == false then
- module:log("warn", "%s", tostring(compressed));
- session:close({
- condition = "undefined-condition";
- text = compressed;
- extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
- });
- return;
- end
- return compressed;
- end);
-end
-
--- setup decompression for a stream
-local function setup_decompression(session, inflate_stream)
- add_filter(session, "bytes/in", function(data)
- local status, decompressed, eof = pcall(inflate_stream, data);
- if status == false then
- module:log("warn", "%s", tostring(decompressed));
- session:close({
- condition = "undefined-condition";
- text = decompressed;
- extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
- });
- return;
- end
- return decompressed;
- end);
-end
-
-module:hook("stanza/http://jabber.org/protocol/compress:compressed", function(event)
- local session = event.origin;
-
- if session.type == "s2sout" then
- session.log("debug", "Activating compression...")
- -- create deflate and inflate streams
- local deflate_stream = get_deflate_stream(session);
- if not deflate_stream then return true; end
-
- local inflate_stream = get_inflate_stream(session);
- if not inflate_stream then return true; end
-
- -- setup compression for session.w
- setup_compression(session, deflate_stream);
-
- -- setup decompression for session.data
- setup_decompression(session, inflate_stream);
- session:reset_stream();
- session:open_stream(session.from_host, session.to_host);
- session.compressed = true;
- return true;
- end
-end);
-
-module:hook("stanza/http://jabber.org/protocol/compress:failure", function(event)
- local err = event.stanza:get_child();
- (event.origin.log or module._log)("warn", "Compression setup failed (%s)", err and err.name or "unknown reason");
- return true;
-end);
-
-module:hook("stanza/http://jabber.org/protocol/compress:compress", function(event)
- local session, stanza = event.origin, event.stanza;
-
- if session.type == "c2s" or session.type == "s2sin" then
- -- fail if we are already compressed
- if session.compressed then
- local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
- (session.sends2s or session.send)(error_st);
- session.log("debug", "Client tried to establish another compression layer.");
- return true;
- end
-
- -- checking if the compression method is supported
- local method = stanza:child_with_name("method");
- method = method and (method[1] or "");
- if method == "zlib" then
- session.log("debug", "zlib compression enabled.");
-
- -- create deflate and inflate streams
- local deflate_stream = get_deflate_stream(session);
- if not deflate_stream then return true; end
-
- local inflate_stream = get_inflate_stream(session);
- if not inflate_stream then return true; end
-
- (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
- session:reset_stream();
-
- -- setup compression for session.w
- setup_compression(session, deflate_stream);
-
- -- setup decompression for session.data
- setup_decompression(session, inflate_stream);
-
- session.compressed = true;
- elseif method then
- session.log("debug", "%s compression selected, but we don't support it.", tostring(method));
- local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method");
- (session.sends2s or session.send)(error_st);
- else
- (session.sends2s or session.send)(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"));
- end
- return true;
- end
-end);
-
+-- COMPAT w/ pre-0.10 configs
+error("mod_compression has been removed in Prosody 0.10+. Please see https://prosody.im/doc/modules/mod_compression for more information.");
diff --git a/plugins/mod_debug_sql.lua b/plugins/mod_debug_sql.lua
new file mode 100644
index 00000000..74cc2f68
--- /dev/null
+++ b/plugins/mod_debug_sql.lua
@@ -0,0 +1,27 @@
+-- Enables SQL query logging
+--
+-- luacheck: ignore 213/uri
+
+module:set_global();
+
+local engines = module:shared("/*/sql/connections");
+
+for uri, engine in pairs(engines) do
+ engine:debug(true);
+end
+
+setmetatable(engines, {
+ __newindex = function (t, uri, engine)
+ engine:debug(true);
+ rawset(t, uri, engine);
+ end
+});
+
+function module.unload()
+ setmetatable(engines, nil);
+ for uri, engine in pairs(engines) do
+ engine:debug(false);
+ end
+end
+
+
diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua
index dc3c3f10..eddc3209 100644
--- a/plugins/mod_dialback.lua
+++ b/plugins/mod_dialback.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -14,20 +14,45 @@ local st = require "util.stanza";
local sha256_hash = require "util.hashes".sha256;
local sha256_hmac = require "util.hashes".hmac_sha256;
local nameprep = require "util.encodings".stringprep.nameprep;
+local uuid_gen = require"util.uuid".generate;
local xmlns_stream = "http://etherx.jabber.org/streams";
local dialback_requests = setmetatable({}, { __mode = 'v' });
+local dialback_secret = sha256_hash(module:get_option_string("dialback_secret", uuid_gen()), true);
+local dwd = module:get_option_boolean("dialback_without_dialback", false);
+
+--- Helper to check that a session peer's certificate is valid
+function check_cert_status(session)
+ local host = session.direction == "outgoing" and session.to_host or session.from_host
+ local conn = session.conn:socket()
+ local cert
+ if conn.getpeercertificate then
+ cert = conn:getpeercertificate()
+ end
+
+ return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert });
+end
+
+
+function module.save()
+ return { dialback_secret = dialback_secret };
+end
+
+function module.restore(state)
+ dialback_secret = state.dialback_secret;
+end
+
function generate_dialback(id, to, from)
- return sha256_hmac(sha256_hash(hosts[from].dialback_secret), to .. ' ' .. from .. ' ' .. id, true);
+ return sha256_hmac(dialback_secret, to .. ' ' .. from .. ' ' .. id, true);
end
function initiate_dialback(session)
-- generate dialback key
session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host);
session.sends2s(st.stanza("db:result", { from = session.from_host, to = session.to_host }):text(session.dialback_key));
- session.log("info", "sent dialback key on outgoing s2s stream");
+ session.log("debug", "sent dialback key on outgoing s2s stream");
end
function verify_dialback(id, to, from, key)
@@ -36,7 +61,7 @@ end
module:hook("stanza/jabber:server:dialback:verify", function(event)
local origin, stanza = event.origin, event.stanza;
-
+
if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then
-- We are being asked to verify the key, to ensure it was generated by us
origin.log("debug", "verifying that dialback key is ours...");
@@ -63,26 +88,36 @@ end);
module:hook("stanza/jabber:server:dialback:result", function(event)
local origin, stanza = event.origin, event.stanza;
-
+
if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then
-- he wants to be identified through dialback
-- We need to check the key with the Authoritative server
local attr = stanza.attr;
local to, from = nameprep(attr.to), nameprep(attr.from);
-
+
if not hosts[to] then
-- Not a host that we serve
- origin.log("info", "%s tried to connect to %s, which we don't serve", from, to);
+ origin.log("warn", "%s tried to connect to %s, which we don't serve", from, to);
origin:close("host-unknown");
return true;
elseif not from then
origin:close("improper-addressing");
end
-
+
+ if dwd and origin.secure then
+ if check_cert_status(origin, from) == false then
+ return
+ elseif origin.cert_chain_status == "valid" and origin.cert_identity_status == "valid" then
+ origin.sends2s(st.stanza("db:result", { to = from, from = to, id = attr.id, type = "valid" }));
+ module:fire_event("s2s-authenticated", { session = origin, host = from });
+ return true;
+ end
+ end
+
origin.hosts[from] = { dialback_key = stanza[1] };
-
+
dialback_requests[from.."/"..origin.streamid] = origin;
-
+
-- COMPAT: ejabberd, gmail and perhaps others do not always set 'to' and 'from'
-- on streams. We fill in the session's to/from here instead.
if not origin.from_host then
@@ -103,7 +138,7 @@ end);
module:hook("stanza/jabber:server:dialback:verify", function(event)
local origin, stanza = event.origin, event.stanza;
-
+
if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then
local attr = stanza.attr;
local dialback_verifying = dialback_requests[attr.from.."/"..(attr.id or "")];
@@ -118,7 +153,8 @@ module:hook("stanza/jabber:server:dialback:verify", function(event)
valid = "invalid";
end
if dialback_verifying.destroyed then
- log("warn", "Incoming s2s session %s was closed in the meantime, so we can't notify it of the db result", tostring(dialback_verifying):match("%w+$"));
+ log("warn", "Incoming s2s session %s was closed in the meantime, so we can't notify it of the dialback result",
+ tostring(dialback_verifying):match("%w+$"));
else
dialback_verifying.sends2s(
st.stanza("db:result", { from = attr.to, to = attr.from, id = attr.id, type = valid })
@@ -132,10 +168,10 @@ end);
module:hook("stanza/jabber:server:dialback:result", function(event)
local origin, stanza = event.origin, event.stanza;
-
+
if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then
-- Remote server is telling us whether we passed dialback
-
+
local attr = stanza.attr;
if not hosts[attr.to] then
origin:close("host-unknown");
@@ -154,7 +190,7 @@ module:hook("stanza/jabber:server:dialback:result", function(event)
end
end);
-module:hook_stanza("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza)
+module:hook_tag("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza) -- luacheck: ignore 212/stanza
if origin.external_auth == "failed" then
module:log("debug", "SASL EXTERNAL failed, falling back to dialback");
initiate_dialback(origin);
@@ -162,7 +198,7 @@ module:hook_stanza("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (orig
end
end, 100);
-module:hook_stanza(xmlns_stream, "features", function (origin, stanza)
+module:hook_tag(xmlns_stream, "features", function (origin, stanza) -- luacheck: ignore 212/stanza
if not origin.external_auth or origin.external_auth == "failed" then
module:log("debug", "Initiating dialback...");
initiate_dialback(origin);
diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua
index 71a04a2d..6d3e7753 100644
--- a/plugins/mod_disco.lua
+++ b/plugins/mod_disco.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -13,7 +13,7 @@ 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 {};
+local disco_items = module:get_option_array("disco_items", {})
do -- validate disco_items
for _, item in ipairs(disco_items) do
local err;
@@ -32,7 +32,9 @@ do -- validate disco_items
end
end
-module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router
+if module:get_host_type() == "local" then
+ module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router
+end
module:add_feature("http://jabber.org/protocol/disco#info");
module:add_feature("http://jabber.org/protocol/disco#items");
@@ -97,7 +99,18 @@ module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(even
local origin, stanza = event.origin, event.stanza;
if stanza.attr.type ~= "get" then return; end
local node = stanza.tags[1].attr.node;
- if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then return; end -- TODO fire event?
+ if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then
+ local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node});
+ local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+ local ret = module:fire_event("host-disco-info-node", node_event);
+ if ret ~= nil then return ret; end
+ if node_event.exists then
+ origin.send(reply);
+ else
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+ end
+ return true;
+ end
local reply_query = get_server_disco_info();
reply_query.attr.node = node;
local reply = st.reply(stanza):add_child(reply_query);
@@ -108,9 +121,21 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve
local origin, stanza = event.origin, event.stanza;
if stanza.attr.type ~= "get" then return; end
local node = stanza.tags[1].attr.node;
- if node and node ~= "" then return; end -- TODO fire event?
-
+ if node and node ~= "" then
+ local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node});
+ local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+ local ret = module:fire_event("host-disco-items-node", node_event);
+ if ret ~= nil then return ret; end
+ if node_event.exists then
+ origin.send(reply);
+ else
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+ end
+ return true;
+ end
local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
+ local ret = module:fire_event("host-disco-items", { origin = origin, stanza = stanza, reply = reply });
+ if ret ~= nil then return ret; end
for jid, name in pairs(get_children(module.host)) do
reply:tag("item", {jid = jid, name = name~=true and name or nil}):up();
end
@@ -123,23 +148,36 @@ end);
-- Handle caps stream feature
module:hook("stream-features", function (event)
- if event.origin.type == "c2s" then
+ if event.origin.type == "c2s" or event.origin.type == "c2s_unbound" then
event.features:add_child(get_server_caps_feature());
end
end);
-- Handle disco requests to user accounts
+if module:get_host_type() ~= "local" then return end -- skip for components
module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(event)
local origin, stanza = event.origin, event.stanza;
if stanza.attr.type ~= "get" then return; end
local node = stanza.tags[1].attr.node;
- if node and node ~= "" then return; end -- TODO fire event?
local username = jid_split(stanza.attr.to) or origin.username;
if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+ if node and node ~= "" then
+ local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node});
+ if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
+ local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+ local ret = module:fire_event("account-disco-info-node", node_event);
+ if ret ~= nil then return ret; end
+ if node_event.exists then
+ origin.send(reply);
+ else
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+ end
+ return true;
+ end
local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'});
if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
reply:tag('identity', {category='account', type='registered'}):up();
- module:fire_event("account-disco-info", { origin = origin, stanza = reply });
+ module:fire_event("account-disco-info", { origin = origin, reply = reply });
origin.send(reply);
return true;
end
@@ -148,12 +186,24 @@ module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(eve
local origin, stanza = event.origin, event.stanza;
if stanza.attr.type ~= "get" then return; end
local node = stanza.tags[1].attr.node;
- if node and node ~= "" then return; end -- TODO fire event?
local username = jid_split(stanza.attr.to) or origin.username;
if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+ if node and node ~= "" then
+ local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node});
+ if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
+ local node_event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+ local ret = module:fire_event("account-disco-items-node", node_event);
+ if ret ~= nil then return ret; end
+ if node_event.exists then
+ origin.send(reply);
+ else
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+ end
+ return true;
+ end
local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items'});
if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
- module:fire_event("account-disco-items", { origin = origin, stanza = reply });
+ module:fire_event("account-disco-items", { origin = origin, stanza = stanza, reply = reply });
origin.send(reply);
return true;
end
diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua
index f7f632c2..d696d453 100644
--- a/plugins/mod_groups.lua
+++ b/plugins/mod_groups.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -10,18 +10,18 @@
local groups;
local members;
-local groups_file;
-
local jid, datamanager = require "util.jid", require "util.datamanager";
local jid_prep = jid.prep;
local module_host = module:get_host();
-function inject_roster_contacts(username, host, roster)
+function inject_roster_contacts(event)
+ local username, host= event.username, event.host;
--module:log("debug", "Injecting group members to roster");
local bare_jid = username.."@"..host;
if not members[bare_jid] and not members[false] then return; end -- Not a member of any groups
-
+
+ local roster = event.roster;
local function import_jids_to_roster(group_name)
for jid in pairs(groups[group_name]) do
-- Add them to roster
@@ -48,7 +48,7 @@ function inject_roster_contacts(username, host, roster)
import_jids_to_roster(group_name);
end
end
-
+
-- Import public groups
if members[false] then
for _, group_name in ipairs(members[false]) do
@@ -56,7 +56,7 @@ function inject_roster_contacts(username, host, roster)
import_jids_to_roster(group_name);
end
end
-
+
if roster[false] then
roster[false].version = true;
end
@@ -80,12 +80,12 @@ function remove_virtual_contacts(username, host, datastore, data)
end
function module.load()
- groups_file = module:get_option_string("groups_file");
+ local groups_file = module:get_option_path("groups_file", nil, "config");
if not groups_file then return; end
-
+
module:hook("roster-load", inject_roster_contacts);
datamanager.add_callback(remove_virtual_contacts);
-
+
groups = { default = {} };
members = { };
local curr_group = "default";
diff --git a/plugins/mod_http.lua b/plugins/mod_http.lua
index 03b23480..a15e8cda 100644
--- a/plugins/mod_http.lua
+++ b/plugins/mod_http.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2012 Matthew Wild
-- Copyright (C) 2008-2012 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -48,6 +48,11 @@ local function get_base_path(host_module, app_name, default_app_path)
:gsub("%$(%w+)", { host = host_module.host });
end
+local function redir_handler(event)
+ event.response.headers.location = event.request.path.."/";
+ return 301;
+end
+
local ports_by_scheme = { http = 80, https = 443, };
-- Helper to deduce a module's external URL
@@ -104,6 +109,9 @@ function module.add_host(module)
local path = event.request.path:sub(base_path_len);
return _handler(event, path);
end;
+ module:hook_object_event(server, event_name:sub(1, -3), redir_handler, -1);
+ elseif event_name:sub(-1, -1) == "/" then
+ module:hook_object_event(server, event_name:sub(1, -2), redir_handler, -1);
end
if not app_handlers[event_name] then
app_handlers[event_name] = handler;
@@ -122,7 +130,7 @@ function module.add_host(module)
module:log("warn", "Not listening on any ports, '%s' will be unreachable", app_name);
end
end
-
+
local function http_app_removed(event)
local app_handlers = apps[event.item.name];
apps[event.item.name] = nil;
@@ -130,7 +138,7 @@ function module.add_host(module)
module:unhook_object_event(server, event, handler);
end
end
-
+
module:handle_items("http-provider", http_app_added, http_app_removed);
server.add_host(host);
@@ -153,7 +161,9 @@ module:provides("net", {
listener = server.listener;
default_port = 5281;
encryption = "ssl";
- ssl_config = { verify = "none" };
+ ssl_config = {
+ verify = "none";
+ };
multiplex = {
pattern = "^[A-Z]";
};
diff --git a/plugins/mod_http_errors.lua b/plugins/mod_http_errors.lua
index 2568ea80..13473219 100644
--- a/plugins/mod_http_errors.lua
+++ b/plugins/mod_http_errors.lua
@@ -2,6 +2,8 @@ module:set_global();
local server = require "net.http.server";
local codes = require "net.http.codes";
+local xml_escape = require "util.stanza".xml_escape;
+local render = require "util.interpolation".new("%b{}", xml_escape);
local show_private = module:get_option_boolean("http_errors_detailed", false);
local always_serve = module:get_option_boolean("http_errors_always_show", true);
@@ -21,55 +23,52 @@ local html = [[
<!DOCTYPE html>
<html>
<head>
- <meta charset="utf-8">
- <style>
- body{
- margin-top:14%;
- text-align:center;
- background-color:#F8F8F8;
- font-family:sans-serif;
- }
- h1{
- font-size:xx-large;
- }
- p{
- font-size:x-large;
- }
- p+p { font-size: large; font-family: courier }
- </style>
+<meta charset="utf-8">
+<title>{title}</title>
+<style>
+body{
+ margin-top:14%;
+ text-align:center;
+ background-color:#F8F8F8;
+ font-family:sans-serif;
+}
+h1{
+ font-size:xx-large;
+}
+p{
+ font-size:x-large;
+}
+p+p {
+ font-size:large;
+ font-family:courier;
+}
+</style>
</head>
<body>
- <h1>$title</h1>
- <p>$message</p>
- <p>$extra</p>
+<h1>{title}</h1>
+<p>{message}</p>
+<p>{extra?}</p>
</body>
-</html>]];
-html = html:gsub("%s%s+", "");
-
-local entities = {
- ["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;",
- ["'"] = "&apos;", ["\""] = "&quot;", ["\n"] = "<br/>",
-};
-
-local function tohtml(plain)
- return (plain:gsub("[<>&'\"\n]", entities));
-
-end
+</html>
+]];
local function get_page(code, extra)
local message = messages[code];
if always_serve or message then
message = message or default_message;
- return (html:gsub("$(%a+)", {
+ return render(html, {
title = rawget(codes, code) or ("Code "..tostring(code));
message = message[1]:gsub("%%", function ()
return message[math.random(2, math.max(#message,2))];
end);
- extra = tohtml(extra or "");
- }));
+ extra = extra;
+ });
end
end
module:hook_object_event(server, "http-error", function (event)
+ if event.response then
+ event.response.headers.content_type = "text/html; charset=utf-8";
+ end
return get_page(event.code, (show_private and event.private_message) or event.message);
end);
diff --git a/plugins/mod_http_files.lua b/plugins/mod_http_files.lua
index 53b6469b..c5986e37 100644
--- a/plugins/mod_http_files.lua
+++ b/plugins/mod_http_files.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -16,8 +16,10 @@ local stat = lfs.attributes;
local build_path = require"socket.url".build_path;
local path_sep = package.config:sub(1,1);
-local base_path = module:get_option_string("http_files_dir", module:get_option_string("http_path"));
-local dir_indices = module:get_option("http_index_files", { "index.html", "index.htm" });
+local base_path = module:get_option_path("http_files_dir", module:get_option_path("http_path"));
+local cache_size = module:get_option_number("http_files_cache_size", 128);
+local cache_max_file_size = module:get_option_number("http_files_cache_max_file_size", 4096);
+local dir_indices = module:get_option_array("http_index_files", { "index.html", "index.htm" });
local directory_index = module:get_option_boolean("http_dir_listing");
local mime_map = module:shared("/*/http_files/mime").types;
@@ -35,7 +37,7 @@ if not mime_map then
};
module:shared("/*/http_files/mime").types = mime_map;
- local mime_types, err = open(module:get_option_string("mime_types_file", "/etc/mime.types"),"r");
+ local mime_types, err = open(module:get_option_path("mime_types_file", "/etc/mime.types", "config"), "r");
if mime_types then
local mime_data = mime_types:read("*a");
mime_types:close();
@@ -81,7 +83,7 @@ function sanitize_path(path)
return "/"..table.concat(out, "/");
end
-local cache = setmetatable({}, { __mode = "kv" }); -- Let the garbage collector have it if it wants to.
+local cache = require "util.cache".new(cache_size);
function serve(opts)
if type(opts) ~= "table" then -- assume path string
@@ -109,7 +111,7 @@ function serve(opts)
local last_modified = os_date('!%a, %d %b %Y %H:%M:%S GMT', attr.modification);
response_headers.last_modified = last_modified;
- local etag = ("%02x-%x-%x-%x"):format(attr.dev or 0, attr.ino or 0, attr.size or 0, attr.modification or 0);
+ local etag = ('"%02x-%x-%x-%x"'):format(attr.dev or 0, attr.ino or 0, attr.size or 0, attr.modification or 0);
response_headers.etag = etag;
local if_none_match = request_headers.if_none_match
@@ -119,7 +121,7 @@ function serve(opts)
return 304;
end
- local data = cache[orig_path];
+ local data = cache:get(orig_path);
if data and data.etag == etag then
response_headers.content_type = data.content_type;
data = data.data;
@@ -142,23 +144,27 @@ function serve(opts)
if not data then
return 403;
end
- cache[orig_path] = { data = data, content_type = mime_map.html; etag = etag; };
+ cache:set(orig_path, { data = data, content_type = mime_map.html; etag = etag; });
response_headers.content_type = mime_map.html;
else
local f, err = open(full_path, "rb");
- if f then
- data, err = f:read("*a");
- f:close();
- end
- if not data then
- module:log("debug", "Could not open or read %s. Error was %s", full_path, err);
+ if not f then
+ module:log("debug", "Could not open %s. Error was %s", full_path, err);
return 403;
end
local ext = full_path:match("%.([^./]+)$");
local content_type = ext and mime_map[ext];
- cache[orig_path] = { data = data; content_type = content_type; etag = etag };
response_headers.content_type = content_type;
+ if attr.size > cache_max_file_size then
+ response_headers.content_length = attr.size;
+ module:log("debug", "%d > cache_max_file_size", attr.size);
+ return response:send_file(f);
+ else
+ data = f:read("*a");
+ f:close();
+ end
+ cache:set(orig_path, { data = data; content_type = content_type; etag = etag });
end
return response:send(data);
diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua
index e7901ab4..c6d62e85 100644
--- a/plugins/mod_iq.lua
+++ b/plugins/mod_iq.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
diff --git a/plugins/mod_lastactivity.lua b/plugins/mod_lastactivity.lua
index 11053709..2dd61699 100644
--- a/plugins/mod_lastactivity.lua
+++ b/plugins/mod_lastactivity.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -19,8 +19,7 @@ module:hook("pre-presence/bare", function(event)
local stanza = event.stanza;
if not(stanza.attr.to) and stanza.attr.type == "unavailable" then
local t = os.time();
- local s = stanza:child_with_name("status");
- s = s and #s.tags == 0 and s[1] or "";
+ local s = stanza:get_child_text("status");
map[event.origin.username] = {s = s, t = t};
end
end, 10);
diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua
index 5fb66441..5edc26bb 100644
--- a/plugins/mod_legacyauth.lua
+++ b/plugins/mod_legacyauth.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -11,8 +11,8 @@
local st = require "util.stanza";
local t_concat = table.concat;
-local secure_auth_only = module:get_option("c2s_require_encryption")
- or module:get_option("require_encryption")
+local secure_auth_only = module:get_option("c2s_require_encryption",
+ module:get_option("require_encryption"))
or not(module:get_option("allow_unencrypted_plain_auth"));
local sessionmanager = require "core.sessionmanager";
@@ -43,10 +43,11 @@ module:hook("stanza/iq/jabber:iq:auth:query", function(event)
session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server"));
return true;
end
-
- local username = stanza.tags[1]:child_with_name("username");
- local password = stanza.tags[1]:child_with_name("password");
- local resource = stanza.tags[1]:child_with_name("resource");
+
+ local query = stanza.tags[1];
+ local username = query:get_child("username");
+ local password = query:get_child("password");
+ local resource = query:get_child("resource");
if not (username and password and resource) then
local reply = st.reply(stanza);
session.send(reply:query("jabber:iq:auth")
diff --git a/plugins/mod_limits.lua b/plugins/mod_limits.lua
new file mode 100644
index 00000000..3fc3fcaa
--- /dev/null
+++ b/plugins/mod_limits.lua
@@ -0,0 +1,98 @@
+-- Because we deal with pre-authed sessions and streams we can't be host-specific
+module:set_global();
+
+local filters = require "util.filters";
+local throttle = require "util.throttle";
+local timer = require "util.timer";
+local ceil = math.ceil;
+
+local limits_cfg = module:get_option("limits", {});
+local limits_resolution = module:get_option_number("limits_resolution", 1);
+
+local default_bytes_per_second = 3000;
+local default_burst = 2;
+
+local rate_units = { b = 1, k = 3, m = 6, g = 9, t = 12 } -- Plan for the future.
+local function parse_rate(rate, sess_type)
+ local quantity, unit, exp;
+ if rate then
+ quantity, unit = rate:match("^(%d+) ?([^/]+)/s$");
+ exp = quantity and rate_units[unit:sub(1,1):lower()];
+ end
+ if not exp then
+ module:log("error", "Error parsing rate for %s: %q, using default rate (%d bytes/s)", sess_type, rate, default_bytes_per_second);
+ return default_bytes_per_second;
+ end
+ return quantity*(10^exp);
+end
+
+local function parse_burst(burst, sess_type)
+ if type(burst) == "string" then
+ burst = burst:match("^(%d+) ?s$");
+ end
+ local n_burst = tonumber(burst);
+ if not n_burst then
+ module:log("error", "Unable to parse burst for %s: %q, using default burst interval (%ds)", sess_type, tostring(burst), default_burst);
+ end
+ return n_burst or default_burst;
+end
+
+-- Process config option into limits table:
+-- limits = { c2s = { bytes_per_second = X, burst_seconds = Y } }
+local limits = {};
+
+for sess_type, sess_limits in pairs(limits_cfg) do
+ limits[sess_type] = {
+ bytes_per_second = parse_rate(sess_limits.rate, sess_type);
+ burst_seconds = parse_burst(sess_limits.burst, sess_type);
+ };
+end
+
+local default_filter_set = {};
+
+function default_filter_set.bytes_in(bytes, session)
+ local throttle = session.throttle;
+ if throttle then
+ local ok, balance, outstanding = throttle:poll(#bytes, true);
+ if not ok then
+ session.log("debug", "Session over rate limit (%d) with %d (by %d), pausing", throttle.max, #bytes, outstanding);
+ outstanding = ceil(outstanding);
+ session.conn:pause(); -- Read no more data from the connection until there is no outstanding data
+ local outstanding_data = bytes:sub(-outstanding);
+ bytes = bytes:sub(1, #bytes-outstanding);
+ timer.add_task(limits_resolution, function ()
+ if not session.conn then return; end
+ if throttle:peek(#outstanding_data) then
+ session.log("debug", "Resuming paused session");
+ session.conn:resume();
+ end
+ -- Handle what we can of the outstanding data
+ session.data(outstanding_data);
+ end);
+ end
+ end
+ return bytes;
+end
+
+local type_filters = {
+ c2s = default_filter_set;
+ s2sin = default_filter_set;
+ s2sout = default_filter_set;
+};
+
+local function filter_hook(session)
+ local session_type = session.type:match("^[^_]+");
+ local filter_set, opts = type_filters[session_type], limits[session_type];
+ if opts then
+ session.throttle = throttle.create(opts.bytes_per_second * opts.burst_seconds, opts.burst_seconds);
+ filters.add_filter(session, "bytes/in", filter_set.bytes_in, 1000);
+ end
+end
+
+function module.load()
+ filters.add_filter_hook(filter_hook);
+end
+
+function module.unload()
+ filters.remove_filter_hook(filter_hook);
+end
diff --git a/plugins/mod_mam/fallback_archive.lib.lua b/plugins/mod_mam/fallback_archive.lib.lua
new file mode 100644
index 00000000..71e65274
--- /dev/null
+++ b/plugins/mod_mam/fallback_archive.lib.lua
@@ -0,0 +1,91 @@
+-- Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2011-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- luacheck: ignore 212/self
+
+local uuid = require "util.uuid".generate;
+local store = module:shared("archive");
+local archive_store = { _provided_by = "mam"; name = "fallback"; };
+
+function archive_store:append(username, key, value, when, with)
+ local archive = store[username];
+ if not archive then
+ archive = { [0] = 0 };
+ store[username] = archive;
+ end
+ local index = (archive[0] or #archive)+1;
+ local item = { key = key, when = when, with = with, value = value };
+ if not key or archive[key] then
+ key = uuid();
+ item.key = key;
+ end
+ archive[index] = item;
+ archive[key] = index;
+ archive[0] = index;
+ return key;
+end
+
+function archive_store:find(username, query)
+ local archive = store[username] or {};
+ local start, stop, step = 1, archive[0] or #archive, 1;
+ local qstart, qend, qwith = -math.huge, math.huge;
+ local limit;
+
+ if query then
+ if query.reverse then
+ start, stop, step = stop, start, -1;
+ if query.before and archive[query.before] then
+ start = archive[query.before] - 1;
+ end
+ elseif query.after and archive[query.after] then
+ start = archive[query.after] + 1;
+ end
+ qwith = query.with;
+ limit = query.limit;
+ qstart = query.start or qstart;
+ qend = query["end"] or qend;
+ end
+
+ return function ()
+ if limit and limit <= 0 then return end
+ for i = start, stop, step do
+ local item = archive[i];
+ if (not qwith or qwith == item.with) and item.when >= qstart and item.when <= qend then
+ if limit then limit = limit - 1; end
+ start = i + step; -- Start on next item
+ return item.key, item.value, item.when, item.with;
+ end
+ end
+ end
+end
+
+function archive_store:delete(username, query)
+ if not query or next(query) == nil then
+ -- no specifics, delete everything
+ store[username] = nil;
+ return true;
+ end
+ local archive = store[username];
+ if not archive then return true; end -- no messages, nothing to delete
+
+ local qstart = query.start or -math.huge;
+ local qend = query["end"] or math.huge;
+ local qwith = query.with;
+ store[username] = nil;
+ for i = 1, #archive do
+ local item = archive[i];
+ local when, with = item.when, item.when;
+ -- Add things that don't match the query
+ if not ((not qwith or qwith == item.with) and item.when >= qstart and item.when <= qend) then
+ self:append(username, item.key, item.value, when, with);
+ end
+ end
+ return true;
+end
+
+return archive_store;
diff --git a/plugins/mod_mam/mamprefs.lib.lua b/plugins/mod_mam/mamprefs.lib.lua
new file mode 100644
index 00000000..dd82b626
--- /dev/null
+++ b/plugins/mod_mam/mamprefs.lib.lua
@@ -0,0 +1,76 @@
+-- Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2011-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- XEP-0313: Message Archive Management for Prosody
+--
+-- luacheck: ignore 122/prosody
+
+local global_default_policy = module:get_option_string("default_archive_policy", true);
+if global_default_policy ~= "roster" then
+ global_default_policy = module:get_option_boolean("default_archive_policy", global_default_policy);
+end
+local smart_enable = module:get_option_boolean("mam_smart_enable", false);
+
+do
+ -- luacheck: ignore 211/prefs_format
+ local prefs_format = {
+ [false] = "roster",
+ -- default ::= true | false | "roster"
+ -- true = always, false = never, nil = global default
+ ["romeo@montague.net"] = true, -- always
+ ["montague@montague.net"] = false, -- newer
+ };
+end
+
+local sessions = prosody.hosts[module.host].sessions;
+local archive_store = module:get_option_string("archive_store", "archive");
+local prefs = module:open_store(archive_store .. "_prefs");
+
+local function get_prefs(user, explicit)
+ local user_sessions = sessions[user];
+ local user_prefs = user_sessions and user_sessions.archive_prefs
+ if not user_prefs then
+ -- prefs not cached
+ user_prefs = prefs:get(user);
+ if not user_prefs then
+ -- prefs not set
+ if smart_enable and explicit then
+ -- a mam-capable client was involved in this action, set defaults
+ user_prefs = { [false] = global_default_policy };
+ prefs:set(user, user_prefs);
+ end
+ end
+ if user_sessions then
+ -- cache settings if they originate from user action
+ user_sessions.archive_prefs = user_prefs;
+ end
+ if not user_prefs then
+ if smart_enable then
+ -- not yet enabled, either explicitly or "smart"
+ user_prefs = { [false] = false };
+ else
+ -- no explicit settings, return defaults
+ user_prefs = { [false] = global_default_policy };
+ end
+ end
+ end
+ return user_prefs;
+end
+
+local function set_prefs(user, user_prefs)
+ local user_sessions = sessions[user];
+ if user_sessions then
+ user_sessions.archive_prefs = user_prefs;
+ end
+ return prefs:set(user, user_prefs);
+end
+
+return {
+ get = get_prefs,
+ set = set_prefs,
+}
diff --git a/plugins/mod_mam/mamprefsxml.lib.lua b/plugins/mod_mam/mamprefsxml.lib.lua
new file mode 100644
index 00000000..8eee78d8
--- /dev/null
+++ b/plugins/mod_mam/mamprefsxml.lib.lua
@@ -0,0 +1,64 @@
+-- Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2011-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- XEP-0313: Message Archive Management for Prosody
+--
+
+local st = require"util.stanza";
+local xmlns_mam = "urn:xmpp:mam:2";
+
+local default_attrs = {
+ always = true, [true] = "always",
+ never = false, [false] = "never",
+ roster = "roster",
+}
+
+local function tostanza(prefs)
+ local default = prefs[false];
+ default = default_attrs[default];
+ local prefstanza = st.stanza("prefs", { xmlns = xmlns_mam, default = default });
+ local always = st.stanza("always");
+ local never = st.stanza("never");
+ for jid, choice in pairs(prefs) do
+ if jid then
+ (choice and always or never):tag("jid"):text(jid):up();
+ end
+ end
+ prefstanza:add_child(always):add_child(never);
+ return prefstanza;
+end
+local function fromstanza(prefstanza)
+ local prefs = {};
+ local default = prefstanza.attr.default;
+ if default then
+ prefs[false] = default_attrs[default];
+ end
+
+ local always = prefstanza:get_child("always");
+ if always then
+ for rule in always:childtags("jid") do
+ local jid = rule:get_text();
+ prefs[jid] = true;
+ end
+ end
+
+ local never = prefstanza:get_child("never");
+ if never then
+ for rule in never:childtags("jid") do
+ local jid = rule:get_text();
+ prefs[jid] = false;
+ end
+ end
+
+ return prefs;
+end
+
+return {
+ tostanza = tostanza;
+ fromstanza = fromstanza;
+}
diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua
new file mode 100644
index 00000000..cf622e0f
--- /dev/null
+++ b/plugins/mod_mam/mod_mam.lua
@@ -0,0 +1,401 @@
+-- Prosody IM
+-- Copyright (C) 2008-2017 Matthew Wild
+-- Copyright (C) 2008-2017 Waqas Hussain
+-- Copyright (C) 2011-2017 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- XEP-0313: Message Archive Management for Prosody
+--
+
+local xmlns_mam = "urn:xmpp:mam:2";
+local xmlns_delay = "urn:xmpp:delay";
+local xmlns_forward = "urn:xmpp:forward:0";
+local xmlns_st_id = "urn:xmpp:sid:0";
+
+local um = require "core.usermanager";
+local st = require "util.stanza";
+local rsm = require "util.rsm";
+local get_prefs = module:require"mamprefs".get;
+local set_prefs = module:require"mamprefs".set;
+local prefs_to_stanza = module:require"mamprefsxml".tostanza;
+local prefs_from_stanza = module:require"mamprefsxml".fromstanza;
+local jid_bare = require "util.jid".bare;
+local jid_split = require "util.jid".split;
+local jid_prepped_split = require "util.jid".prepped_split;
+local dataform = require "util.dataforms".new;
+local host = module.host;
+
+local rm_load_roster = require "core.rostermanager".load_roster;
+
+local is_stanza = st.is_stanza;
+local tostring = tostring;
+local time_now = os.time;
+local m_min = math.min;
+local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse;
+local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
+local strip_tags = module:get_option_set("dont_archive_namespaces", { "http://jabber.org/protocol/chatstates" });
+
+local archive_store = module:get_option_string("archive_store", "archive");
+local archive = module:open_store(archive_store, "archive");
+
+if archive.name == "null" or not archive.find then
+ if not archive.find then
+ module:log("debug", "Attempt to open archive storage returned a valid driver but it does not seem to implement the storage API");
+ module:log("debug", "mod_%s does not support archiving", archive._provided_by or archive.name and "storage_"..archive.name.."(?)" or "<unknown>");
+ else
+ module:log("debug", "Attempt to open archive storage returned null driver");
+ end
+ module:log("debug", "See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information");
+ module:log("info", "Using in-memory fallback archive driver");
+ archive = module:require "fallback_archive";
+end
+
+local use_total = module:get_option_boolean("mam_include_total", true);
+
+local cleanup;
+
+local function schedule_cleanup(username)
+ if cleanup and not cleanup[username] then
+ table.insert(cleanup, username);
+ cleanup[username] = true;
+ end
+end
+
+-- Handle prefs.
+module:hook("iq/self/"..xmlns_mam..":prefs", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ local user = origin.username;
+ if stanza.attr.type == "set" then
+ local new_prefs = stanza:get_child("prefs", xmlns_mam);
+ local prefs = prefs_from_stanza(new_prefs);
+ local ok, err = set_prefs(user, prefs);
+ if not ok then
+ origin.send(st.error_reply(stanza, "cancel", "internal-server-error", "Error storing preferences: "..tostring(err)));
+ return true;
+ end
+ end
+ local prefs = prefs_to_stanza(get_prefs(user, true));
+ local reply = st.reply(stanza):add_child(prefs);
+ origin.send(reply);
+ return true;
+end);
+
+local query_form = dataform {
+ { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam; };
+ { name = "with"; type = "jid-single"; };
+ { name = "start"; type = "text-single" };
+ { name = "end"; type = "text-single"; };
+};
+
+-- Serve form
+module:hook("iq-get/self/"..xmlns_mam..":query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ get_prefs(origin.username, true);
+ origin.send(st.reply(stanza):query(xmlns_mam):add_child(query_form:form()));
+ return true;
+end);
+
+-- Handle archive queries
+module:hook("iq-set/self/"..xmlns_mam..":query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ local query = stanza.tags[1];
+ local qid = query.attr.queryid;
+
+ get_prefs(origin.username, true);
+ schedule_cleanup(origin.username);
+
+ -- Search query parameters
+ local qwith, qstart, qend;
+ local form = query:get_child("x", "jabber:x:data");
+ if form then
+ local err;
+ form, err = query_form:data(form);
+ if err then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err))));
+ return true;
+ end
+ qwith, qstart, qend = form["with"], form["start"], form["end"];
+ qwith = qwith and jid_bare(qwith); -- dataforms does jidprep
+ end
+
+ if qstart or qend then -- Validate timestamps
+ local vstart, vend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend));
+ if (qstart and not vstart) or (qend and not vend) then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp"))
+ return true;
+ end
+ qstart, qend = vstart, vend;
+ end
+
+ module:log("debug", "Archive query, id %s with %s from %s until %s",
+ tostring(qid), qwith or "anyone",
+ qstart and timestamp(qstart) or "the dawn of time",
+ qend and timestamp(qend) or "now");
+
+ -- RSM stuff
+ local qset = rsm.get(query);
+ local qmax = m_min(qset and qset.max or default_max_items, max_max_items);
+ local reverse = qset and qset.before or false;
+ local before, after = qset and qset.before, qset and qset.after;
+ if type(before) ~= "string" then before = nil; end
+
+ -- Load all the data!
+ local data, err = archive:find(origin.username, {
+ start = qstart; ["end"] = qend; -- Time range
+ with = qwith;
+ limit = qmax + 1;
+ before = before; after = after;
+ reverse = reverse;
+ total = use_total or qmax == 0;
+ });
+
+ if not data then
+ origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
+ return true;
+ end
+ local total = tonumber(err);
+
+ local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to };
+
+ local results = {};
+
+ -- Wrap it in stuff and deliver
+ local first, last;
+ local count = 0;
+ local complete = "true";
+ for id, item, when in data do
+ count = count + 1;
+ if count > qmax then
+ complete = nil;
+ break;
+ end
+ local fwd_st = st.message(msg_reply_attr)
+ :tag("result", { xmlns = xmlns_mam, queryid = qid, id = id })
+ :tag("forwarded", { xmlns = xmlns_forward })
+ :tag("delay", { xmlns = xmlns_delay, stamp = timestamp(when) }):up();
+
+ if not is_stanza(item) then
+ item = st.deserialize(item);
+ end
+ item.attr.xmlns = "jabber:client";
+ fwd_st:add_child(item);
+
+ if not first then first = id; end
+ last = id;
+
+ if reverse then
+ results[count] = fwd_st;
+ else
+ origin.send(fwd_st);
+ end
+ end
+
+ if reverse then
+ for i = #results, 1, -1 do
+ origin.send(results[i]);
+ end
+ first, last = last, first;
+ end
+
+ -- That's all folks!
+ module:log("debug", "Archive query %s completed", tostring(qid));
+
+ origin.send(st.reply(stanza)
+ :tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete })
+ :add_child(rsm.generate {
+ first = first, last = last, count = total }));
+ return true;
+end);
+
+local function has_in_roster(user, who)
+ local roster = rm_load_roster(user, host);
+ module:log("debug", "%s has %s in roster? %s", user, who, roster[who] and "yes" or "no");
+ return roster[who];
+end
+
+local function shall_store(user, who)
+ -- TODO Cache this?
+ if not um.user_exists(user, host) then
+ return false;
+ end
+ local prefs = get_prefs(user);
+ local rule = prefs[who];
+ module:log("debug", "%s's rule for %s is %s", user, who, tostring(rule));
+ if rule ~= nil then
+ return rule;
+ end
+ -- Below could be done by a metatable
+ local default = prefs[false];
+ module:log("debug", "%s's default rule is %s", user, tostring(default));
+ if default == "roster" then
+ return has_in_roster(user, who);
+ end
+ return default;
+end
+
+local function strip_stanza_id(stanza, user)
+ if stanza:get_child("stanza-id", xmlns_st_id) then
+ stanza = st.clone(stanza);
+ stanza:maptags(function (tag)
+ if tag.name == "stanza-id" and tag.attr.xmlns == xmlns_st_id then
+ local by_user, by_host, res = jid_prepped_split(tag.attr.by);
+ if not res and by_host == host and by_user == user then
+ return nil;
+ end
+ end
+ return tag;
+ end);
+ end
+ return stanza;
+end
+
+-- Handle messages
+local function message_handler(event, c2s)
+ local origin, stanza = event.origin, event.stanza;
+ local log = c2s and origin.log or module._log;
+ local orig_type = stanza.attr.type or "normal";
+ local orig_from = stanza.attr.from;
+ local orig_to = stanza.attr.to or orig_from;
+ -- Stanza without 'to' are treated as if it was to their own bare jid
+
+ -- Whos storage do we put it in?
+ local store_user = c2s and origin.username or jid_split(orig_to);
+ -- And who are they chatting with?
+ local with = jid_bare(c2s and orig_to or orig_from);
+
+ -- Filter out <stanza-id> that claim to be from us
+ event.stanza = strip_stanza_id(stanza, store_user);
+
+ -- We store chat messages or normal messages that have a body
+ if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body")) ) then
+ log("debug", "Not archiving stanza: %s (type)", stanza:top_tag());
+ return;
+ end
+
+ -- or if hints suggest we shouldn't
+ if not stanza:get_child("store", "urn:xmpp:hints") then -- No hint telling us we should store
+ if stanza:get_child("no-permanent-store", "urn:xmpp:hints")
+ or stanza:get_child("no-store", "urn:xmpp:hints") then -- Hint telling us we should NOT store
+ log("debug", "Not archiving stanza: %s (hint)", stanza:top_tag());
+ return;
+ end
+ end
+
+ local clone_for_storage;
+ if not strip_tags:empty() then
+ clone_for_storage = st.clone(stanza);
+ clone_for_storage:maptags(function (tag)
+ if strip_tags:contains(tag.attr.xmlns) then
+ return nil;
+ else
+ return tag;
+ end
+ end);
+ if #clone_for_storage.tags == 0 then
+ log("debug", "Not archiving stanza: %s (empty when stripped)", stanza:top_tag());
+ return;
+ end
+ else
+ clone_for_storage = stanza;
+ end
+
+ -- Check with the users preferences
+ if shall_store(store_user, with) then
+ log("debug", "Archiving stanza: %s", stanza:top_tag());
+
+ -- And stash it
+ local ok = archive:append(store_user, nil, clone_for_storage, time_now(), with);
+ if ok then
+ local clone_for_other_handlers = st.clone(stanza);
+ local id = ok;
+ clone_for_other_handlers:tag("stanza-id", { xmlns = xmlns_st_id, by = store_user.."@"..host, id = id }):up();
+ event.stanza = clone_for_other_handlers;
+ schedule_cleanup(store_user);
+ module:fire_event("archive-message-added", { origin = origin, stanza = clone_for_storage, for_user = store_user, id = id });
+ end
+ else
+ log("debug", "Not archiving stanza: %s (prefs)", stanza:top_tag());
+ end
+end
+
+local function c2s_message_handler(event)
+ return message_handler(event, true);
+end
+
+-- Filter out <stanza-id> before the message leaves the server to prevent privacy leak.
+local function strip_stanza_id_after_other_events(event)
+ event.stanza = strip_stanza_id(event.stanza, event.origin.username);
+end
+
+module:hook("pre-message/bare", strip_stanza_id_after_other_events, -1);
+module:hook("pre-message/full", strip_stanza_id_after_other_events, -1);
+
+local cleanup_after = module:get_option_string("archive_expires_after", "1w");
+local cleanup_interval = module:get_option_number("archive_cleanup_interval", 4 * 60 * 60);
+if cleanup_after ~= "never" then
+ local day = 86400;
+ local multipliers = { d = day, w = day * 7, m = 31 * day, y = 365.2425 * day };
+ local n, m = cleanup_after:lower():match("(%d+)%s*([dwmy]?)");
+ if not n then
+ module:log("error", "Could not parse archive_expires_after string %q", cleanup_after);
+ return false;
+ end
+
+ cleanup_after = tonumber(n) * ( multipliers[m] or 1 );
+
+ module:log("debug", "archive_expires_after = %d -- in seconds", cleanup_after);
+
+ if not archive.delete then
+ module:log("error", "archive_expires_after set but mod_%s does not support deleting", archive._provided_by);
+ return false;
+ end
+
+ -- Set of known users to do message expiry for
+ -- Populated either below or when new messages are added
+ cleanup = {};
+
+ -- Iterating over users is not supported by all authentication modules
+ -- Catch and ignore error if not supported
+ pcall(function ()
+ -- If this works, then we schedule cleanup for all known users on startup
+ for user in um.users(module.host) do
+ schedule_cleanup(user);
+ end
+ end);
+
+ -- At odd intervals, delete old messages for one user
+ module:add_timer(math.random(10, 60), function()
+ local user = table.remove(cleanup, 1);
+ if user then
+ module:log("debug", "Removing old messages for user %q", user);
+ local ok, err = archive:delete(user, { ["end"] = os.time() - cleanup_after; })
+ if not ok then
+ module:log("warn", "Could not expire archives for user %s: %s", user, err);
+ elseif type(ok) == "number" then
+ module:log("debug", "Removed %d messages", ok);
+ end
+ cleanup[user] = nil;
+ end
+ return math.random(cleanup_interval, cleanup_interval * 2);
+ end);
+else
+ module:log("debug", "Archive expiry disabled");
+ -- Don't ask the backend to count the potentially unbounded number of items,
+ -- it'll get slow.
+ use_total = module:get_option_boolean("mam_include_total", false);
+end
+
+-- Stanzas sent by local clients
+module:hook("pre-message/bare", c2s_message_handler, 0);
+module:hook("pre-message/full", c2s_message_handler, 0);
+-- Stanzas to local clients
+module:hook("message/bare", message_handler, 0);
+module:hook("message/full", message_handler, 0);
+
+module:hook("account-disco-info", function(event)
+ (event.reply or event.stanza):tag("feature", {var=xmlns_mam}):up();
+ (event.reply or event.stanza):tag("feature", {var=xmlns_st_id}):up();
+end);
+
diff --git a/plugins/mod_message.lua b/plugins/mod_message.lua
index e85da613..0d370ec1 100644
--- a/plugins/mod_message.lua
+++ b/plugins/mod_message.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -17,10 +17,10 @@ local user_exists = require "core.usermanager".user_exists;
local function process_to_bare(bare, origin, stanza)
local user = bare_sessions[bare];
-
+
local t = stanza.attr.type;
if t == "error" then
- -- discard
+ return true; -- discard
elseif t == "groupchat" then
origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
elseif t == "headline" then
@@ -48,11 +48,10 @@ local function process_to_bare(bare, origin, stanza)
local node, host = jid_split(bare);
local ok
if user_exists(node, host) then
- -- TODO apply the default privacy list
-
ok = module:fire_event('message/offline/handle', {
- origin = origin,
- stanza = stanza,
+ username = node;
+ origin = origin,
+ stanza = stanza,
});
end
@@ -66,20 +65,20 @@ end
module:hook("message/full", function(data)
-- message to full JID recieved
local origin, stanza = data.origin, data.stanza;
-
+
local session = full_sessions[stanza.attr.to];
if session and session.send(stanza) then
return true;
else -- resource not online
return process_to_bare(jid_bare(stanza.attr.to), origin, stanza);
end
-end);
+end, -1);
module:hook("message/bare", function(data)
-- message to bare JID recieved
local origin, stanza = data.origin, data.stanza;
return process_to_bare(stanza.attr.to or (origin.username..'@'..origin.host), origin, stanza);
-end);
+end, -1);
module:add_feature("msgoffline");
diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua
index 3dd6b816..13bc7e31 100644
--- a/plugins/mod_motd.lua
+++ b/plugins/mod_motd.lua
@@ -2,7 +2,7 @@
-- 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.
--
@@ -17,10 +17,9 @@ local st = require "util.stanza";
motd_text = motd_text:gsub("^%s*(.-)%s*$", "%1"):gsub("\n[ \t]+", "\n"); -- Strip indentation from the config
-module:hook("presence/bare", function (event)
+module:hook("presence/initial", function (event)
local session, stanza = event.origin, event.stanza;
- if session.username and not session.presence
- and not stanza.attr.type and not stanza.attr.to then
+ if not stanza.attr.type and not stanza.attr.to then
local motd_stanza =
st.message({ to = session.full_jid, from = motd_jid })
:tag("body"):text(motd_text);
diff --git a/plugins/mod_net_multiplex.lua b/plugins/mod_net_multiplex.lua
index a66ab31f..6db49391 100644
--- a/plugins/mod_net_multiplex.lua
+++ b/plugins/mod_net_multiplex.lua
@@ -19,7 +19,7 @@ module:hook("service-added", function (event) add_service(event.service); end);
module:hook("service-removed", function (event) available_services[event.service] = nil; end);
for service_name, services in pairs(portmanager.get_registered_services()) do
- for i, service in ipairs(services) do
+ for _, service in ipairs(services) do
add_service(service);
end
end
diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua
index 1ac62f94..487098d1 100644
--- a/plugins/mod_offline.lua
+++ b/plugins/mod_offline.lua
@@ -1,51 +1,43 @@
-- 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;
+local offline_messages = module:open_store("offline", "archive");
+
module:add_feature("msgoffline");
module:hook("message/offline/handle", function(event)
local origin, stanza = event.origin, event.stanza;
local to = stanza.attr.to;
- local node, host;
+ local node;
if to then
- node, host = jid_split(to)
+ node = jid_split(to)
else
- node, host = origin.username, origin.host;
+ node = origin.username;
end
-
- stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy();
- local result = datamanager.list_append(node, host, "offline", st.preserialize(stanza));
- stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
-
- return result;
-end);
+
+ return offline_messages:append(node, nil, stanza, os.time(), "");
+end, -1);
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");
+ local data = offline_messages:find(node);
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;
+ for _, stanza, when in data do
+ stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime(when)}):up(); -- XEP-0203
origin.send(stanza);
end
- datamanager.list_store(node, host, "offline", nil);
+ offline_messages:delete(node);
return true;
-end);
+end, -1);
diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua
index 22790869..1025be37 100644
--- a/plugins/mod_pep.lua
+++ b/plugins/mod_pep.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -46,11 +46,11 @@ local function subscription_presence(user_bare, recipient)
return is_contact_subscribed(username, host, recipient_bare);
end
-local function publish(session, node, id, item)
+module:hook("pep-publish-item", function (event)
+ local session, bare, node, id, item = event.session, event.user, event.node, event.id, event.item;
item.attr.xmlns = nil;
local disable = #item.tags ~= 1 or #item.tags[1] == 0;
if #item.tags == 0 then item.name = "retract"; end
- local bare = session.username..'@'..session.host;
local stanza = st.message({from=bare, type='headline'})
:tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'})
:tag('items', {node=node})
@@ -77,7 +77,8 @@ local function publish(session, node, id, item)
core_post_stanza(session, stanza);
end
end
-end
+end);
+
local function publish_all(user, recipient, session)
local d = data[user];
local notify = recipients[user] and recipients[user][recipient];
@@ -180,9 +181,16 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event)
local id = payload.attr.id or "1";
payload.attr.id = id;
session.send(st.reply(stanza));
- publish(session, node, id, st.clone(payload));
+ module:fire_event("pep-publish-item", {
+ node = node, user = jid_bare(session.full_jid), actor = session.jid,
+ id = id, session = session, item = st.clone(payload);
+ });
return true;
+ else
+ module:log("debug", "Payload is missing the <item>", node);
end
+ else
+ module:log("debug", "Unhandled payload: %s", payload and payload:top_tag() or "(no payload)");
end
elseif stanza.attr.type == 'get' then
local user = stanza.attr.to and jid_bare(stanza.attr.to) or session.username..'@'..session.host;
@@ -218,14 +226,17 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event)
end
elseif node then -- node doesn't exist
session.send(st.error_reply(stanza, 'cancel', 'item-not-found'));
+ module:log("debug", "Item '%s' not found", node)
return true;
else --invalid request
session.send(st.error_reply(stanza, 'modify', 'bad-request'));
+ module:log("debug", "Invalid request: %s", tostring(payload));
return true;
end
else --no presence subscription
session.send(st.error_reply(stanza, 'auth', 'not-authorized')
:tag('presence-subscription-required', {xmlns='http://jabber.org/protocol/pubsub#errors'}));
+ module:log("debug", "Unauthorized request: %s", tostring(payload));
return true;
end
end
@@ -271,23 +282,33 @@ module:hook("iq-result/bare/disco", function(event)
end);
module:hook("account-disco-info", function(event)
- local stanza = event.stanza;
- stanza:tag('identity', {category='pubsub', type='pep'}):up();
- stanza:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up();
+ local reply = event.reply;
+ reply:tag('identity', {category='pubsub', type='pep'}):up();
+ reply:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up();
end);
module:hook("account-disco-items", function(event)
- local stanza = event.stanza;
- local bare = stanza.attr.to;
+ local reply = event.reply;
+ local bare = reply.attr.to;
local user_data = data[bare];
if user_data then
for node, _ in pairs(user_data) do
- stanza:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes
+ reply:tag('item', {jid=bare, node=node}):up();
end
end
end);
+module:hook("account-disco-info-node", function (event)
+ local session, stanza, node = event.origin, event.stanza, event.node;
+ local user = stanza.attr.to;
+ local user_data = data[user];
+ if user_data and user_data[node] then
+ event.exists = true;
+ event.reply:tag('identity', {category='pubsub', type='leaf'}):up();
+ end
+end);
+
module:hook("resource-unbind", function (event)
local user_bare_jid = event.session.username.."@"..event.session.host;
if not bare_sessions[user_bare_jid] then -- User went offline
diff --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua
index 0bfcac66..1a503409 100644
--- a/plugins/mod_ping.lua
+++ b/plugins/mod_ping.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -11,14 +11,11 @@ local st = require "util.stanza";
module:add_feature("urn:xmpp:ping");
local function ping_handler(event)
- if event.stanza.attr.type == "get" then
- event.origin.send(st.reply(event.stanza));
- return true;
- end
+ return event.origin.send(st.reply(event.stanza));
end
-module:hook("iq/bare/urn:xmpp:ping:ping", ping_handler);
-module:hook("iq/host/urn:xmpp:ping:ping", ping_handler);
+module:hook("iq-get/bare/urn:xmpp:ping:ping", ping_handler);
+module:hook("iq-get/host/urn:xmpp:ping:ping", ping_handler);
-- Ad-hoc command
diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua
index b289fa44..fccc7a2b 100644
--- a/plugins/mod_posix.lua
+++ b/plugins/mod_posix.lua
@@ -1,24 +1,26 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-local want_pposix_version = "0.3.6";
+local want_pposix_version = "0.4.0";
local pposix = assert(require "util.pposix");
if pposix._VERSION ~= want_pposix_version then
- module:log("warn", "Unknown version (%s) of binary pposix module, expected %s. Perhaps you need to recompile?", tostring(pposix._VERSION), want_pposix_version);
+ module:log("warn", "Unknown version (%s) of binary pposix module, expected %s."
+ .. "Perhaps you need to recompile?", tostring(pposix._VERSION), want_pposix_version);
end
-local signal = select(2, pcall(require, "util.signal"));
-if type(signal) == "string" then
+local have_signal, signal = pcall(require, "util.signal");
+if not have_signal then
module:log("warn", "Couldn't load signal library, won't respond to SIGTERM");
end
+local format = require "util.format".format;
local lfs = require "lfs";
local stat = lfs.attributes;
@@ -26,38 +28,38 @@ local prosody = _G.prosody;
module:set_global(); -- we're a global module
-local umask = module:get_option("umask") or "027";
+local umask = module:get_option_string("umask", "027");
pposix.umask(umask);
-- Allow switching away from root, some people like strange ports.
module:hook("server-started", function ()
- local uid = module:get_option("setuid");
- local gid = module:get_option("setgid");
- if gid then
- local success, msg = pposix.setgid(gid);
- if success then
- module:log("debug", "Changed group to %s successfully.", gid);
- else
- module:log("error", "Failed to change group to %s. Error: %s", gid, msg);
- prosody.shutdown("Failed to change group to %s", gid);
- end
+ local uid = module:get_option("setuid");
+ local gid = module:get_option("setgid");
+ if gid then
+ local success, msg = pposix.setgid(gid);
+ if success then
+ module:log("debug", "Changed group to %s successfully.", gid);
+ else
+ module:log("error", "Failed to change group to %s. Error: %s", gid, msg);
+ prosody.shutdown("Failed to change group to %s", gid);
end
- if uid then
- local success, msg = pposix.setuid(uid);
- if success then
- module:log("debug", "Changed user to %s successfully.", uid);
- else
- module:log("error", "Failed to change user to %s. Error: %s", uid, msg);
- prosody.shutdown("Failed to change user to %s", uid);
- end
+ end
+ if uid then
+ local success, msg = pposix.setuid(uid);
+ if success then
+ module:log("debug", "Changed user to %s successfully.", uid);
+ else
+ module:log("error", "Failed to change user to %s. Error: %s", uid, msg);
+ prosody.shutdown("Failed to change user to %s", uid);
end
- end);
+ end
+end);
-- Don't even think about it!
if not prosody.start_time then -- server-starting
local suid = module:get_option("setuid");
if not suid or suid == 0 or suid == "root" then
- if pposix.getuid() == 0 and not module:get_option("run_as_root") then
+ if pposix.getuid() == 0 and not module:get_option_boolean("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");
@@ -80,7 +82,7 @@ local function write_pidfile()
if pidfile_handle then
remove_pidfile();
end
- pidfile = module:get_option_string("pidfile");
+ pidfile = module:get_option_path("pidfile", nil, "data");
if pidfile then
local err;
local mode = stat(pidfile) and "r+" or "w+";
@@ -112,31 +114,19 @@ local function write_pidfile()
end
local syslog_opened;
-function syslog_sink_maker(config)
+function syslog_sink_maker(config) -- luacheck: ignore 212/config
if not syslog_opened then
pposix.syslog_open("prosody", module:get_option_string("syslog_facility"));
syslog_opened = true;
end
- local syslog, format = pposix.syslog_log, string.format;
+ local syslog = pposix.syslog_log;
return function (name, level, message, ...)
- if ... then
- syslog(level, name, format(message, ...));
- else
- syslog(level, name, message);
- end
+ syslog(level, name, format(message, ...));
end;
end
require "core.loggingmanager".register_sink_type("syslog", syslog_sink_maker);
-local daemonize = module:get_option("daemonize");
-if daemonize == nil then
- local no_daemonize = module:get_option("no_daemonize"); --COMPAT w/ 0.5
- daemonize = not no_daemonize;
- if no_daemonize ~= nil then
- module:log("warn", "The 'no_daemonize' option is now replaced by 'daemonize'");
- module:log("warn", "Update your config from 'no_daemonize = %s' to 'daemonize = %s'", tostring(no_daemonize), tostring(daemonize));
- end
-end
+local daemonize = module:get_option("daemonize", prosody.installed);
local function remove_log_sinks()
local lm = require "core.loggingmanager";
@@ -170,7 +160,7 @@ end
module:hook("server-stopped", remove_pidfile);
-- Set signal handlers
-if signal.signal then
+if have_signal then
signal.signal("SIGTERM", function ()
module:log("warn", "Received SIGTERM");
prosody.unlock_globals();
@@ -183,7 +173,7 @@ if signal.signal then
prosody.reload_config();
prosody.reopen_logfiles();
end);
-
+
signal.signal("SIGINT", function ()
module:log("info", "Received SIGINT");
prosody.unlock_globals();
diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua
index 6df56fe0..0c243bc6 100644
--- a/plugins/mod_presence.lua
+++ b/plugins/mod_presence.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -10,7 +10,7 @@ local log = module._log;
local require = require;
local pairs = pairs;
-local t_concat, t_insert = table.concat, table.insert;
+local t_concat = table.concat;
local s_find = string.find;
local tonumber = tonumber;
@@ -27,49 +27,24 @@ local NULL = {};
local rostermanager = require "core.rostermanager";
local sessionmanager = require "core.sessionmanager";
-local function select_top_resources(user)
- local priority = 0;
- local recipients = {};
- for _, session in pairs(user.sessions) do -- find resource with greatest priority
- if session.presence then
- -- TODO check active privacy list for session
- local p = session.priority;
- if p > priority then
- priority = p;
- recipients = {session};
- elseif p == priority then
- t_insert(recipients, session);
- end
- end
- end
- return recipients;
-end
-local function recalc_resource_map(user)
- if user then
- user.top_resources = select_top_resources(user);
- if #user.top_resources == 0 then user.top_resources = nil; end
- end
-end
+local recalc_resource_map = require "util.presence".recalc_resource_map;
-local ignore_presence_priority = module:get_option("ignore_presence_priority");
+local ignore_presence_priority = module:get_option_boolean("ignore_presence_priority", false);
function handle_normal_presence(origin, stanza)
if ignore_presence_priority then
- local priority = stanza:child_with_name("priority");
+ local priority = stanza:get_child("priority");
if priority and priority[1] ~= "0" then
for i=#priority.tags,1,-1 do priority.tags[i] = nil; end
- for i=#priority,1,-1 do priority[i] = nil; end
+ for i=#priority,2,-1 do priority[i] = nil; end
priority[1] = "0";
end
end
- local priority = stanza:child_with_name("priority");
- if priority and #priority > 0 then
- priority = t_concat(priority);
- if s_find(priority, "^[+-]?[0-9]+$") then
- priority = tonumber(priority);
- if priority < -128 then priority = -128 end
- if priority > 127 then priority = 127 end
- else priority = 0; end
+ local priority = stanza:get_child_text("priority");
+ if priority and s_find(priority, "^[+-]?[0-9]+$") then
+ priority = tonumber(priority);
+ if priority < -128 then priority = -128 end
+ if priority > 127 then priority = 127 end
else priority = 0; end
if full_sessions[origin.full_jid] then -- if user is still connected
origin.send(stanza); -- reflect their presence back to them
@@ -90,6 +65,7 @@ function handle_normal_presence(origin, stanza)
end
end
if stanza.attr.type == nil and not origin.presence then -- initial presence
+ module:fire_event("presence/initial", { origin = origin, stanza = stanza } );
origin.presence = stanza; -- FIXME repeated later
local probe = st.presence({from = origin.full_jid, type = "probe"});
for jid, item in pairs(roster) do -- probe all contacts we are subscribed to
@@ -105,10 +81,8 @@ function handle_normal_presence(origin, stanza)
res.presence.attr.to = nil;
end
end
- if roster.pending then -- resend incoming subscription requests
- for jid in pairs(roster.pending) do
- origin.send(st.presence({type="subscribe", from=jid})); -- TODO add to attribute? Use original?
- end
+ for jid in pairs(roster[false].pending) do -- resend incoming subscription requests
+ origin.send(st.presence({type="subscribe", from=jid})); -- TODO add to attribute? Use original?
end
local request = st.presence({type="subscribe", from=origin.username.."@"..origin.host});
for jid, item in pairs(roster) do -- resend outgoing subscription requests
@@ -153,7 +127,7 @@ function send_presence_of_available_resources(user, host, jid, recipient_session
if h and h.type == "local" then
local u = h.sessions[user];
if u then
- for k, session in pairs(u.sessions) do
+ for _, session in pairs(u.sessions) do
local pres = session.presence;
if pres then
if stanza then pres = stanza; pres.attr.from = session.full_jid; end
@@ -230,7 +204,7 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b
local st_from, st_to = stanza.attr.from, stanza.attr.to;
stanza.attr.from, stanza.attr.to = from_bare, to_bare;
log("debug", "inbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare);
-
+
if stanza.attr.type == "probe" then
local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
if result then
@@ -315,7 +289,7 @@ module:hook("presence/bare", function(data)
if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID
return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
end
-
+
local user = bare_sessions[to];
if user then
for _, session in pairs(user.sessions) do
@@ -350,7 +324,7 @@ end);
module:hook("presence/host", function(data)
-- inbound presence to the host
local stanza = data.stanza;
-
+
local from_bare = jid_bare(stanza.attr.from);
local t = stanza.attr.type;
if t == "probe" then
@@ -383,3 +357,27 @@ module:hook("resource-unbind", function(event)
session.directed = nil;
end
end);
+
+module:hook("roster-item-removed", function (event)
+ local username = event.username;
+ local session = event.origin;
+ local roster = event.roster or session and session.roster;
+ local jid = event.jid;
+ local item = event.item;
+ local from_jid = session.full_jid or (username .. "@" .. module.host);
+
+ local subscription = item and item.subscription or "none";
+ local ask = item and item.ask;
+ local pending = roster and roster[false].pending[jid];
+
+ if subscription == "both" or subscription == "from" or pending then
+ core_post_stanza(session, st.presence({type="unsubscribed", from=from_jid, to=jid}));
+ end
+
+ if subscription == "both" or subscription == "to" or ask then
+ send_presence_of_available_resources(username, module.host, jid, session, st.presence({type="unavailable"}));
+ core_post_stanza(session, st.presence({type="unsubscribe", from=from_jid, to=jid}));
+ end
+
+end, -1);
+
diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua
index f95dfa50..b749b7c7 100644
--- a/plugins/mod_privacy.lua
+++ b/plugins/mod_privacy.lua
@@ -2,447 +2,12 @@
-- Copyright (C) 2009-2010 Matthew Wild
-- Copyright (C) 2009-2010 Waqas Hussain
-- Copyright (C) 2009 Thilo Cestonaro
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-module:add_feature("jabber:iq:privacy");
-
-local st = require "util.stanza";
-local bare_sessions, full_sessions = prosody.bare_sessions, prosody.full_sessions;
-local util_Jid = require "util.jid";
-local jid_bare = util_Jid.bare;
-local jid_split, jid_join = util_Jid.split, util_Jid.join;
-local load_roster = require "core.rostermanager".load_roster;
-local to_number = tonumber;
-
-local privacy_storage = module:open_store();
-
-function isListUsed(origin, name, privacy_lists)
- local user = bare_sessions[origin.username.."@"..origin.host];
- if user then
- for resource, session in pairs(user.sessions) do
- if resource ~= origin.resource then
- if session.activePrivacyList == name then
- return true;
- elseif session.activePrivacyList == nil and privacy_lists.default == name then
- return true;
- end
- end
- end
- end
-end
-
-function isAnotherSessionUsingDefaultList(origin)
- local user = bare_sessions[origin.username.."@"..origin.host];
- if user then
- for resource, session in pairs(user.sessions) do
- if resource ~= origin.resource and session.activePrivacyList == nil then
- return true;
- end
- end
- end
-end
-
-function declineList(privacy_lists, origin, stanza, which)
- if which == "default" then
- if isAnotherSessionUsingDefaultList(origin) then
- return { "cancel", "conflict", "Another session is online and using the default list."};
- end
- privacy_lists.default = nil;
- origin.send(st.reply(stanza));
- elseif which == "active" then
- origin.activePrivacyList = nil;
- origin.send(st.reply(stanza));
- else
- return {"modify", "bad-request", "Neither default nor active list specifed to decline."};
- end
- return true;
-end
-
-function activateList(privacy_lists, origin, stanza, which, name)
- local list = privacy_lists.lists[name];
-
- if which == "default" and list then
- if isAnotherSessionUsingDefaultList(origin) then
- return {"cancel", "conflict", "Another session is online and using the default list."};
- end
- privacy_lists.default = name;
- origin.send(st.reply(stanza));
- elseif which == "active" and list then
- origin.activePrivacyList = name;
- origin.send(st.reply(stanza));
- elseif not list then
- return {"cancel", "item-not-found", "No such list: "..name};
- else
- return {"modify", "bad-request", "No list chosen to be active or default."};
- end
- return true;
-end
-
-function deleteList(privacy_lists, origin, stanza, name)
- local list = privacy_lists.lists[name];
-
- if list then
- if isListUsed(origin, name, privacy_lists) then
- return {"cancel", "conflict", "Another session is online and using the list which should be deleted."};
- end
- if privacy_lists.default == name then
- privacy_lists.default = nil;
- end
- if origin.activePrivacyList == name then
- origin.activePrivacyList = nil;
- end
- privacy_lists.lists[name] = nil;
- origin.send(st.reply(stanza));
- return true;
- end
- return {"modify", "bad-request", "Not existing list specifed to be deleted."};
-end
-
-function createOrReplaceList (privacy_lists, origin, stanza, name, entries)
- local bare_jid = origin.username.."@"..origin.host;
-
- if privacy_lists.lists == nil then
- privacy_lists.lists = {};
- end
-
- local list = {};
- privacy_lists.lists[name] = list;
-
- local orderCheck = {};
- list.name = name;
- list.items = {};
-
- for _,item in ipairs(entries) do
- if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then
- return {"modify", "bad-request", "Order attribute not valid."};
- end
-
- if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then
- return {"modify", "bad-request", "Type attribute not valid."};
- end
-
- local tmp = {};
- orderCheck[item.attr.order] = true;
-
- tmp["type"] = item.attr.type;
- tmp["value"] = item.attr.value;
- tmp["action"] = item.attr.action;
- tmp["order"] = to_number(item.attr.order);
- tmp["presence-in"] = false;
- tmp["presence-out"] = false;
- tmp["message"] = false;
- tmp["iq"] = false;
-
- if #item.tags > 0 then
- for _,tag in ipairs(item.tags) do
- tmp[tag.name] = true;
- end
- end
-
- if tmp.type == "subscription" then
- if tmp.value ~= "both" and
- tmp.value ~= "to" and
- tmp.value ~= "from" and
- tmp.value ~= "none" then
- return {"cancel", "bad-request", "Subscription value must be both, to, from or none."};
- end
- end
-
- if tmp.action ~= "deny" and tmp.action ~= "allow" then
- return {"cancel", "bad-request", "Action must be either deny or allow."};
- end
- list.items[#list.items + 1] = tmp;
- end
-
- table.sort(list.items, function(a, b) return a.order < b.order; end);
-
- origin.send(st.reply(stanza));
- if bare_sessions[bare_jid] ~= nil then
- local iq = st.iq ( { type = "set", id="push1" } );
- iq:tag ("query", { xmlns = "jabber:iq:privacy" } );
- iq:tag ("list", { name = list.name } ):up();
- iq:up();
- for resource, session in pairs(bare_sessions[bare_jid].sessions) do
- iq.attr.to = bare_jid.."/"..resource
- session.send(iq);
- end
- else
- return {"cancel", "bad-request", "internal error."};
- end
- return true;
-end
-
-function getList(privacy_lists, origin, stanza, name)
- local reply = st.reply(stanza);
- reply:tag("query", {xmlns="jabber:iq:privacy"});
-
- if name == nil then
- if privacy_lists.lists then
- if origin.activePrivacyList then
- reply:tag("active", {name=origin.activePrivacyList}):up();
- end
- if privacy_lists.default then
- reply:tag("default", {name=privacy_lists.default}):up();
- end
- for name,list in pairs(privacy_lists.lists) do
- reply:tag("list", {name=name}):up();
- end
- end
- else
- local list = privacy_lists.lists[name];
- if list then
- reply = reply:tag("list", {name=list.name});
- for _,item in ipairs(list.items) do
- reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order});
- if item["message"] then reply:tag("message"):up(); end
- if item["iq"] then reply:tag("iq"):up(); end
- if item["presence-in"] then reply:tag("presence-in"):up(); end
- if item["presence-out"] then reply:tag("presence-out"):up(); end
- reply:up();
- end
- else
- return {"cancel", "item-not-found", "Unknown list specified."};
- end
- end
-
- origin.send(reply);
- return true;
-end
-
-module:hook("iq/bare/jabber:iq:privacy:query", function(data)
- local origin, stanza = data.origin, data.stanza;
-
- if stanza.attr.to == nil then -- only service requests to own bare JID
- local query = stanza.tags[1]; -- the query element
- local valid = false;
- local privacy_lists = privacy_storage:get(origin.username) or { lists = {} };
-
- if privacy_lists.lists[1] then -- Code to migrate from old privacy lists format, remove in 0.8
- module:log("info", "Upgrading format of stored privacy lists for %s@%s", origin.username, origin.host);
- local lists = privacy_lists.lists;
- for idx, list in ipairs(lists) do
- lists[list.name] = list;
- lists[idx] = nil;
- end
- end
-
- if stanza.attr.type == "set" then
- if #query.tags == 1 then -- the <query/> element MUST NOT include more than one child element
- for _,tag in ipairs(query.tags) do
- if tag.name == "active" or tag.name == "default" then
- if tag.attr.name == nil then -- Client declines the use of active / default list
- valid = declineList(privacy_lists, origin, stanza, tag.name);
- else -- Client requests change of active / default list
- valid = activateList(privacy_lists, origin, stanza, tag.name, tag.attr.name);
- end
- elseif tag.name == "list" and tag.attr.name then -- Client adds / edits a privacy list
- if #tag.tags == 0 then -- Client removes a privacy list
- valid = deleteList(privacy_lists, origin, stanza, tag.attr.name);
- else -- Client edits a privacy list
- valid = createOrReplaceList(privacy_lists, origin, stanza, tag.attr.name, tag.tags);
- end
- end
- end
- end
- elseif stanza.attr.type == "get" then
- local name = nil;
- local listsToRetrieve = 0;
- if #query.tags >= 1 then
- for _,tag in ipairs(query.tags) do
- if tag.name == "list" then -- Client requests a privacy list from server
- name = tag.attr.name;
- listsToRetrieve = listsToRetrieve + 1;
- end
- end
- end
- if listsToRetrieve == 0 or listsToRetrieve == 1 then
- valid = getList(privacy_lists, origin, stanza, name);
- end
- end
-
- if valid ~= true then
- valid = valid or { "cancel", "bad-request", "Couldn't understand request" };
- if valid[1] == nil then
- valid[1] = "cancel";
- end
- if valid[2] == nil then
- valid[2] = "bad-request";
- end
- origin.send(st.error_reply(stanza, valid[1], valid[2], valid[3]));
- else
- privacy_storage:set(origin.username, privacy_lists);
- end
- return true;
- end
-end);
-
-function checkIfNeedToBeBlocked(e, session)
- local origin, stanza = e.origin, e.stanza;
- local privacy_lists = privacy_storage:get(session.username) or {};
- local bare_jid = session.username.."@"..session.host;
- local to = stanza.attr.to or bare_jid;
- local from = stanza.attr.from;
-
- local is_to_user = bare_jid == jid_bare(to);
- local is_from_user = bare_jid == jid_bare(from);
-
- --module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
-
- if privacy_lists.lists == nil or
- not (session.activePrivacyList or privacy_lists.default)
- then
- return; -- Nothing to block, default is Allow all
- end
- if is_from_user and is_to_user then
- --module:log("debug", "Not blocking communications between user's resources");
- return; -- from one of a user's resource to another => HANDS OFF!
- end
-
- local listname = session.activePrivacyList;
- if listname == nil then
- listname = privacy_lists.default; -- no active list selected, use default list
- end
- local list = privacy_lists.lists[listname];
- if not list then -- should never happen
- module:log("warn", "given privacy list not found. name: %s for user %s", listname, bare_jid);
- return;
- end
- for _,item in ipairs(list.items) do
- local apply = false;
- local block = false;
- if (
- (stanza.name == "message" and item.message) or
- (stanza.name == "iq" and item.iq) or
- (stanza.name == "presence" and is_to_user and item["presence-in"]) or
- (stanza.name == "presence" and is_from_user and item["presence-out"]) or
- (item.message == false and item.iq == false and item["presence-in"] == false and item["presence-out"] == false)
- ) then
- apply = true;
- end
- if apply then
- local evilJid = {};
- apply = false;
- if is_to_user then
- --module:log("debug", "evil jid is (from): %s", from);
- evilJid.node, evilJid.host, evilJid.resource = jid_split(from);
- else
- --module:log("debug", "evil jid is (to): %s", to);
- evilJid.node, evilJid.host, evilJid.resource = jid_split(to);
- end
- if item.type == "jid" and
- (evilJid.node and evilJid.host and evilJid.resource and item.value == evilJid.node.."@"..evilJid.host.."/"..evilJid.resource) or
- (evilJid.node and evilJid.host and item.value == evilJid.node.."@"..evilJid.host) or
- (evilJid.host and evilJid.resource and item.value == evilJid.host.."/"..evilJid.resource) or
- (evilJid.host and item.value == evilJid.host) then
- apply = true;
- block = (item.action == "deny");
- elseif item.type == "group" then
- local roster = load_roster(session.username, session.host);
- local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
- if roster_entry then
- local groups = roster_entry.groups;
- for group in pairs(groups) do
- if group == item.value then
- apply = true;
- block = (item.action == "deny");
- break;
- end
- end
- end
- elseif item.type == "subscription" then -- we need a valid bare evil jid
- local roster = load_roster(session.username, session.host);
- local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
- if (not(roster_entry) and item.value == "none")
- or (roster_entry and roster_entry.subscription == item.value) then
- apply = true;
- block = (item.action == "deny");
- end
- elseif item.type == nil then
- apply = true;
- block = (item.action == "deny");
- end
- end
- if apply then
- if block then
- -- drop and not bounce groupchat messages, otherwise users will get kicked
- if stanza.attr.type == "groupchat" then
- return true;
- end
- module:log("debug", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
- if stanza.name == "message" then
- origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
- elseif stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then
- origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
- end
- return true; -- stanza blocked !
- else
- --module:log("debug", "stanza explicitly allowed!")
- return;
- end
- end
- end
-end
-
-function preCheckIncoming(e)
- local session;
- if e.stanza.attr.to ~= nil then
- local node, host, resource = jid_split(e.stanza.attr.to);
- if node == nil or host == nil then
- return;
- end
- if resource == nil then
- local prio = 0;
- if bare_sessions[node.."@"..host] ~= nil then
- for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do
- if session_.priority ~= nil and session_.priority >= prio then
- session = session_;
- prio = session_.priority;
- end
- end
- end
- else
- session = full_sessions[node.."@"..host.."/"..resource];
- end
- if session ~= nil then
- return checkIfNeedToBeBlocked(e, session);
- else
- --module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource));
- end
- end
-end
-
-function preCheckOutgoing(e)
- local session = e.origin;
- if e.stanza.attr.from == nil then
- e.stanza.attr.from = session.username .. "@" .. session.host;
- if session.resource ~= nil then
- e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource;
- end
- end
- if session.username then -- FIXME do properly
- return checkIfNeedToBeBlocked(e, session);
- end
-end
-
-module:hook("pre-message/full", preCheckOutgoing, 500);
-module:hook("pre-message/bare", preCheckOutgoing, 500);
-module:hook("pre-message/host", preCheckOutgoing, 500);
-module:hook("pre-iq/full", preCheckOutgoing, 500);
-module:hook("pre-iq/bare", preCheckOutgoing, 500);
-module:hook("pre-iq/host", preCheckOutgoing, 500);
-module:hook("pre-presence/full", preCheckOutgoing, 500);
-module:hook("pre-presence/bare", preCheckOutgoing, 500);
-module:hook("pre-presence/host", preCheckOutgoing, 500);
-module:hook("message/full", preCheckIncoming, 500);
-module:hook("message/bare", preCheckIncoming, 500);
-module:hook("message/host", preCheckIncoming, 500);
-module:hook("iq/full", preCheckIncoming, 500);
-module:hook("iq/bare", preCheckIncoming, 500);
-module:hook("iq/host", preCheckIncoming, 500);
-module:hook("presence/full", preCheckIncoming, 500);
-module:hook("presence/bare", preCheckIncoming, 500);
-module:hook("presence/host", preCheckIncoming, 500);
+-- COMPAT w/ pre 0.10
+module:log("error", "The mod_privacy plugin has been replaced by mod_blocklist. Please update your config. For more information see https://prosody.im/doc/modules/mod_privacy");
+module:depends("blocklist");
diff --git a/plugins/mod_private.lua b/plugins/mod_private.lua
index 365a997c..c01053d5 100644
--- a/plugins/mod_private.lua
+++ b/plugins/mod_private.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -15,38 +15,40 @@ module:add_feature("jabber:iq:private");
module:hook("iq/self/jabber:iq:private:query", function(event)
local origin, stanza = event.origin, event.stanza;
- local type = stanza.attr.type;
local query = stanza.tags[1];
- if #query.tags == 1 then
- local tag = query.tags[1];
- local key = tag.name..":"..tag.attr.xmlns;
- local data, err = private_storage:get(origin.username);
- if err then
- origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
+ if #query.tags ~= 1 then
+ origin.send(st.error_reply(stanza, "modify", "bad-format"));
+ return true;
+ end
+ local tag = query.tags[1];
+ local key = tag.name..":"..tag.attr.xmlns;
+ local data, err = private_storage:get(origin.username);
+ if err then
+ origin.send(st.error_reply(stanza, "wait", "internal-server-error", err));
+ return true;
+ end
+ if stanza.attr.type == "get" then
+ if data and data[key] then
+ origin.send(st.reply(stanza):query("jabber:iq:private"):add_child(st.deserialize(data[key])));
+ return true;
+ else
+ origin.send(st.reply(stanza):add_child(query));
return true;
end
- if stanza.attr.type == "get" then
- if data and data[key] then
- origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"}):add_child(st.deserialize(data[key])));
- else
- origin.send(st.reply(stanza):add_child(stanza.tags[1]));
- end
- else -- set
- if not data then data = {}; end;
- if #tag == 0 then
- data[key] = nil;
- else
- data[key] = st.preserialize(tag);
- end
- -- TODO delete datastore if empty
- if private_storage:set(origin.username, data) then
- origin.send(st.reply(stanza));
- else
- origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
- end
+ else -- type == set
+ if not data then data = {}; end;
+ if #tag == 0 then
+ data[key] = nil;
+ else
+ data[key] = st.preserialize(tag);
end
- else
- origin.send(st.error_reply(stanza, "modify", "bad-format"));
+ -- TODO delete datastore if empty
+ local ok, err = private_storage:set(origin.username, data);
+ if not ok then
+ origin.send(st.error_reply(stanza, "wait", "internal-server-error", err));
+ return true;
+ end
+ origin.send(st.reply(stanza));
+ return true;
end
- return true;
end);
diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua
index 1fa42bd8..cbbfad12 100644
--- a/plugins/mod_proxy65.lua
+++ b/plugins/mod_proxy65.lua
@@ -2,7 +2,7 @@
-- Copyright (C) 2008-2011 Matthew Wild
-- Copyright (C) 2008-2011 Waqas Hussain
-- Copyright (C) 2009 Thilo Cestonaro
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -30,7 +30,7 @@ function listener.onincoming(conn, data)
(conn == initiator and target or initiator):write(data);
return;
end -- FIXME server.link should be doing this?
-
+
if not session.greeting_done then
local nmethods = data:byte(2) or 0;
if data:byte(1) == 0x05 and nmethods > 0 and #data == 2 + nmethods then -- check if we have all the data
@@ -90,10 +90,10 @@ end
function module.add_host(module)
local host, name = module:get_host(), module:get_option_string("name", "SOCKS5 Bytestreams Service");
-
- local proxy_address = module:get_option("proxy65_address", host);
+
+ local proxy_address = module:get_option_string("proxy65_address", host);
local proxy_port = next(portmanager.get_active_services():search("proxy65", nil)[1] or {});
- local proxy_acl = module:get_option("proxy65_acl");
+ local proxy_acl = module:get_option_array("proxy65_acl");
-- COMPAT w/pre-0.9 where proxy65_port was specified in the components section of the config
local legacy_config = module:get_option_number("proxy65_port");
@@ -101,30 +101,13 @@ function module.add_host(module)
module:log("warn", "proxy65_port is deprecated, please put proxy65_ports = { %d } into the global section instead", legacy_config);
end
+ module:depends("disco");
module:add_identity("proxy", "bytestreams", name);
module:add_feature("http://jabber.org/protocol/bytestreams");
-
- module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function(event)
- local origin, stanza = event.origin, event.stanza;
- if not stanza.tags[1].attr.node then
- origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#info")
- :tag("identity", {category='proxy', type='bytestreams', name=name}):up()
- :tag("feature", {var="http://jabber.org/protocol/bytestreams"}) );
- return true;
- end
- end, -1);
-
- module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function(event)
- local origin, stanza = event.origin, event.stanza;
- if not stanza.tags[1].attr.node then
- origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#items"));
- return true;
- end
- end, -1);
-
+
module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event)
local origin, stanza = event.origin, event.stanza;
-
+
-- check ACL
while proxy_acl and #proxy_acl > 0 do -- using 'while' instead of 'if' so we can break out of it
local jid = stanza.attr.from;
@@ -137,22 +120,22 @@ function module.add_host(module)
origin.send(st.error_reply(stanza, "auth", "forbidden"));
return true;
end
-
+
local sid = stanza.tags[1].attr.sid;
origin.send(st.reply(stanza):tag("query", {xmlns="http://jabber.org/protocol/bytestreams", sid=sid})
:tag("streamhost", {jid=host, host=proxy_address, port=proxy_port}));
return true;
end);
-
+
module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event)
local origin, stanza = event.origin, event.stanza;
-
+
local query = stanza.tags[1];
local sid = query.attr.sid;
local from = stanza.attr.from;
local to = query:get_child_text("activate");
local prepped_to = jid_prep(to);
-
+
local info = "sid: "..tostring(sid)..", initiator: "..tostring(from)..", target: "..tostring(prepped_to or to);
if prepped_to and sid then
local sha = sha1(sid .. from .. prepped_to, true);
diff --git a/plugins/mod_pubsub.lua b/plugins/mod_pubsub.lua
deleted file mode 100644
index 04f2b615..00000000
--- a/plugins/mod_pubsub.lua
+++ /dev/null
@@ -1,463 +0,0 @@
-local pubsub = require "util.pubsub";
-local st = require "util.stanza";
-local jid_bare = require "util.jid".bare;
-local uuid_generate = require "util.uuid".generate;
-local usermanager = require "core.usermanager";
-
-local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
-local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
-local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
-local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
-
-local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
-local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
-local pubsub_disco_name = module:get_option("name");
-if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end
-
-local service;
-
-local handlers = {};
-
-function handle_pubsub_iq(event)
- local origin, stanza = event.origin, event.stanza;
- local pubsub = stanza.tags[1];
- local action = pubsub.tags[1];
- if not action then
- return origin.send(st.error_reply(stanza, "cancel", "bad-request"));
- end
- local handler = handlers[stanza.attr.type.."_"..action.name];
- if handler then
- handler(origin, stanza, action);
- return true;
- end
-end
-
-local pubsub_errors = {
- ["conflict"] = { "cancel", "conflict" };
- ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
- ["jid-required"] = { "modify", "bad-request", nil, "jid-required" };
- ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" };
- ["item-not-found"] = { "cancel", "item-not-found" };
- ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
- ["forbidden"] = { "auth", "forbidden" };
-};
-function pubsub_error_reply(stanza, error)
- local e = pubsub_errors[error];
- local reply = st.error_reply(stanza, unpack(e, 1, 3));
- if e[4] then
- reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up();
- end
- return reply;
-end
-
-function handlers.get_items(origin, stanza, items)
- local node = items.attr.node;
- local item = items:get_child("item");
- local id = item and item.attr.id;
-
- if not node then
- return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
- end
- local ok, results = service:get_items(node, stanza.attr.from, id);
- if not ok then
- return origin.send(pubsub_error_reply(stanza, results));
- end
-
- local data = st.stanza("items", { node = node });
- for _, entry in pairs(results) do
- data:add_child(entry);
- end
- local reply;
- if data then
- reply = st.reply(stanza)
- :tag("pubsub", { xmlns = xmlns_pubsub })
- :add_child(data);
- else
- reply = pubsub_error_reply(stanza, "item-not-found");
- end
- return origin.send(reply);
-end
-
-function handlers.get_subscriptions(origin, stanza, subscriptions)
- local node = subscriptions.attr.node;
- local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from);
- if not ok then
- return origin.send(pubsub_error_reply(stanza, ret));
- end
- local reply = st.reply(stanza)
- :tag("pubsub", { xmlns = xmlns_pubsub })
- :tag("subscriptions");
- for _, sub in ipairs(ret) do
- reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
- end
- return origin.send(reply);
-end
-
-function handlers.set_create(origin, stanza, create)
- local node = create.attr.node;
- local ok, ret, reply;
- if node then
- ok, ret = service:create(node, stanza.attr.from);
- if ok then
- reply = st.reply(stanza);
- else
- reply = pubsub_error_reply(stanza, ret);
- end
- else
- repeat
- node = uuid_generate();
- ok, ret = service:create(node, stanza.attr.from);
- until ok or ret ~= "conflict";
- if ok then
- reply = st.reply(stanza)
- :tag("pubsub", { xmlns = xmlns_pubsub })
- :tag("create", { node = node });
- else
- reply = pubsub_error_reply(stanza, ret);
- end
- end
- return origin.send(reply);
-end
-
-function handlers.set_delete(origin, stanza, delete)
- local node = delete.attr.node;
-
- local reply, notifier;
- if not node then
- return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
- end
- local ok, ret = service:delete(node, stanza.attr.from);
- if ok then
- reply = st.reply(stanza);
- else
- reply = pubsub_error_reply(stanza, ret);
- end
- return origin.send(reply);
-end
-
-function handlers.set_subscribe(origin, stanza, subscribe)
- local node, jid = subscribe.attr.node, subscribe.attr.jid;
- if not (node and jid) then
- return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
- end
- --[[
- local options_tag, options = stanza.tags[1]:get_child("options"), nil;
- if options_tag then
- options = options_form:data(options_tag.tags[1]);
- end
- --]]
- local options_tag, options; -- FIXME
- local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
- local reply;
- if ok then
- reply = st.reply(stanza)
- :tag("pubsub", { xmlns = xmlns_pubsub })
- :tag("subscription", {
- node = node,
- jid = jid,
- subscription = "subscribed"
- }):up();
- if options_tag then
- reply:add_child(options_tag);
- end
- else
- reply = pubsub_error_reply(stanza, ret);
- end
- origin.send(reply);
-end
-
-function handlers.set_unsubscribe(origin, stanza, unsubscribe)
- local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
- if not (node and jid) then
- return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
- end
- local ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
- local reply;
- if ok then
- reply = st.reply(stanza);
- else
- reply = pubsub_error_reply(stanza, ret);
- end
- return origin.send(reply);
-end
-
-function handlers.set_publish(origin, stanza, publish)
- local node = publish.attr.node;
- if not node then
- return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
- end
- local item = publish:get_child("item");
- local id = (item and item.attr.id);
- if not id then
- id = uuid_generate();
- if item then
- item.attr.id = id;
- end
- end
- local ok, ret = service:publish(node, stanza.attr.from, id, item);
- local reply;
- if ok then
- reply = st.reply(stanza)
- :tag("pubsub", { xmlns = xmlns_pubsub })
- :tag("publish", { node = node })
- :tag("item", { id = id });
- else
- reply = pubsub_error_reply(stanza, ret);
- end
- return origin.send(reply);
-end
-
-function handlers.set_retract(origin, stanza, retract)
- local node, notify = retract.attr.node, retract.attr.notify;
- notify = (notify == "1") or (notify == "true");
- local item = retract:get_child("item");
- local id = item and item.attr.id
- if not (node and id) then
- return origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required"));
- end
- local reply, notifier;
- if notify then
- notifier = st.stanza("retract", { id = id });
- end
- local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
- if ok then
- reply = st.reply(stanza);
- else
- reply = pubsub_error_reply(stanza, ret);
- end
- return origin.send(reply);
-end
-
-function handlers.set_purge(origin, stanza, purge)
- local node, notify = purge.attr.node, purge.attr.notify;
- notify = (notify == "1") or (notify == "true");
- local reply;
- if not node then
- return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
- end
- local ok, ret = service:purge(node, stanza.attr.from, notify);
- if ok then
- reply = st.reply(stanza);
- else
- reply = pubsub_error_reply(stanza, ret);
- end
- return origin.send(reply);
-end
-
-function simple_broadcast(kind, node, jids, item)
- if item then
- item = st.clone(item);
- item.attr.xmlns = nil; -- Clear the pubsub namespace
- end
- local message = st.message({ from = module.host, type = "headline" })
- :tag("event", { xmlns = xmlns_pubsub_event })
- :tag(kind, { node = node })
- :add_child(item);
- for jid in pairs(jids) do
- module:log("debug", "Sending notification to %s", jid);
- message.attr.to = jid;
- module:send(message);
- end
-end
-
-module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
-module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);
-
-local disco_info;
-
-local feature_map = {
- create = { "create-nodes", "instant-nodes", "item-ids" };
- retract = { "delete-items", "retract-items" };
- purge = { "purge-nodes" };
- publish = { "publish", autocreate_on_publish and "auto-create" };
- delete = { "delete-nodes" };
- get_items = { "retrieve-items" };
- add_subscription = { "subscribe" };
- get_subscriptions = { "retrieve-subscriptions" };
-};
-
-local function add_disco_features_from_service(disco, service)
- for method, features in pairs(feature_map) do
- if service[method] then
- for _, feature in ipairs(features) do
- if feature then
- disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up();
- end
- end
- end
- end
- for affiliation in pairs(service.config.capabilities) do
- if affiliation ~= "none" and affiliation ~= "owner" then
- disco:tag("feature", { var = xmlns_pubsub.."#"..affiliation.."-affiliation" }):up();
- end
- end
-end
-
-local function build_disco_info(service)
- local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" })
- :tag("identity", { category = "pubsub", type = "service", name = pubsub_disco_name }):up()
- :tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up();
- add_disco_features_from_service(disco_info, service);
- return disco_info;
-end
-
-module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event)
- local origin, stanza = event.origin, event.stanza;
- local node = stanza.tags[1].attr.node;
- if not node then
- return origin.send(st.reply(stanza):add_child(disco_info));
- else
- local ok, ret = service:get_nodes(stanza.attr.from);
- if ok and not ret[node] then
- ok, ret = false, "item-not-found";
- end
- if not ok then
- return origin.send(pubsub_error_reply(stanza, ret));
- end
- local reply = st.reply(stanza)
- :tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node })
- :tag("identity", { category = "pubsub", type = "leaf" });
- return origin.send(reply);
- end
-end);
-
-local function handle_disco_items_on_node(event)
- local stanza, origin = event.stanza, event.origin;
- local query = stanza.tags[1];
- local node = query.attr.node;
- local ok, ret = service:get_items(node, stanza.attr.from);
- if not ok then
- return origin.send(pubsub_error_reply(stanza, ret));
- end
-
- local reply = st.reply(stanza)
- :tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node });
-
- for id, item in pairs(ret) do
- reply:tag("item", { jid = module.host, name = id }):up();
- end
-
- return origin.send(reply);
-end
-
-
-module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event)
- if event.stanza.tags[1].attr.node then
- return handle_disco_items_on_node(event);
- end
- local ok, ret = service:get_nodes(event.stanza.attr.from);
- if not ok then
- event.origin.send(pubsub_error_reply(event.stanza, ret));
- else
- local reply = st.reply(event.stanza)
- :tag("query", { xmlns = "http://jabber.org/protocol/disco#items" });
- for node, node_obj in pairs(ret) do
- reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up();
- end
- event.origin.send(reply);
- end
- return true;
-end);
-
-local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
-local function get_affiliation(jid)
- local bare_jid = jid_bare(jid);
- if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then
- return admin_aff;
- end
-end
-
-function set_service(new_service)
- service = new_service;
- module.environment.service = service;
- disco_info = build_disco_info(service);
-end
-
-function module.save()
- return { service = service };
-end
-
-function module.restore(data)
- set_service(data.service);
-end
-
-set_service(pubsub.new({
- capabilities = {
- none = {
- create = false;
- publish = false;
- retract = false;
- get_nodes = true;
-
- subscribe = true;
- unsubscribe = true;
- get_subscription = true;
- get_subscriptions = true;
- get_items = true;
-
- subscribe_other = false;
- unsubscribe_other = false;
- get_subscription_other = false;
- get_subscriptions_other = false;
-
- be_subscribed = true;
- be_unsubscribed = true;
-
- set_affiliation = false;
- };
- publisher = {
- create = false;
- publish = true;
- retract = true;
- get_nodes = true;
-
- subscribe = true;
- unsubscribe = true;
- get_subscription = true;
- get_subscriptions = true;
- get_items = true;
-
- subscribe_other = false;
- unsubscribe_other = false;
- get_subscription_other = false;
- get_subscriptions_other = false;
-
- be_subscribed = true;
- be_unsubscribed = true;
-
- set_affiliation = false;
- };
- owner = {
- create = true;
- publish = true;
- retract = true;
- delete = true;
- get_nodes = true;
-
- subscribe = true;
- unsubscribe = true;
- get_subscription = true;
- get_subscriptions = true;
- get_items = true;
-
-
- subscribe_other = true;
- unsubscribe_other = true;
- get_subscription_other = true;
- get_subscriptions_other = true;
-
- be_subscribed = true;
- be_unsubscribed = true;
-
- set_affiliation = true;
- };
- };
-
- autocreate_on_publish = autocreate_on_publish;
- autocreate_on_subscribe = autocreate_on_subscribe;
-
- broadcaster = simple_broadcast;
- get_affiliation = get_affiliation;
-
- normalize_jid = jid_bare;
-}));
diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua
new file mode 100644
index 00000000..8e7bfc53
--- /dev/null
+++ b/plugins/mod_pubsub/mod_pubsub.lua
@@ -0,0 +1,233 @@
+local pubsub = require "util.pubsub";
+local st = require "util.stanza";
+local jid_bare = require "util.jid".bare;
+local usermanager = require "core.usermanager";
+
+local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
+local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
+local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
+
+local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
+local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
+local pubsub_disco_name = module:get_option_string("name", "Prosody PubSub Service");
+local expose_publisher = module:get_option_boolean("expose_publisher", false)
+
+local service;
+
+local lib_pubsub = module:require "pubsub";
+local handlers = lib_pubsub.handlers;
+local pubsub_error_reply = lib_pubsub.pubsub_error_reply;
+
+module:depends("disco");
+module:add_identity("pubsub", "service", pubsub_disco_name);
+module:add_feature("http://jabber.org/protocol/pubsub");
+
+function handle_pubsub_iq(event)
+ local origin, stanza = event.origin, event.stanza;
+ local pubsub = stanza.tags[1];
+ local action = pubsub.tags[1];
+ if not action then
+ origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+ return true;
+ end
+ local handler = handlers[stanza.attr.type.."_"..action.name];
+ if handler then
+ handler(origin, stanza, action, service);
+ return true;
+ end
+end
+
+function simple_broadcast(kind, node, jids, item, actor)
+ if item then
+ item = st.clone(item);
+ item.attr.xmlns = nil; -- Clear the pubsub namespace
+ if expose_publisher and actor then
+ item.attr.publisher = actor
+ end
+ end
+ local message = st.message({ from = module.host, type = "headline" })
+ :tag("event", { xmlns = xmlns_pubsub_event })
+ :tag(kind, { node = node })
+ :add_child(item);
+ for jid in pairs(jids) do
+ module:log("debug", "Sending notification to %s", jid);
+ message.attr.to = jid;
+ module:send(message);
+ end
+end
+
+module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
+module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);
+
+local feature_map = {
+ create = { "create-nodes", "instant-nodes", "item-ids" };
+ retract = { "delete-items", "retract-items" };
+ purge = { "purge-nodes" };
+ publish = { "publish", autocreate_on_publish and "auto-create" };
+ delete = { "delete-nodes" };
+ get_items = { "retrieve-items" };
+ add_subscription = { "subscribe" };
+ get_subscriptions = { "retrieve-subscriptions" };
+ set_configure = { "config-node" };
+ get_default = { "retrieve-default" };
+};
+
+local function add_disco_features_from_service(service)
+ for method, features in pairs(feature_map) do
+ if service[method] then
+ for _, feature in ipairs(features) do
+ if feature then
+ module:add_feature(xmlns_pubsub.."#"..feature);
+ end
+ end
+ end
+ end
+ for affiliation in pairs(service.config.capabilities) do
+ if affiliation ~= "none" and affiliation ~= "owner" then
+ module:add_feature(xmlns_pubsub.."#"..affiliation.."-affiliation");
+ end
+ end
+end
+
+module:hook("host-disco-info-node", function (event)
+ local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+ local ok, ret = service:get_nodes(stanza.attr.from);
+ if not ok or not ret[node] then
+ return;
+ end
+ event.exists = true;
+ reply:tag("identity", { category = "pubsub", type = "leaf" });
+end);
+
+module:hook("host-disco-items-node", function (event)
+ local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+ local ok, ret = service:get_items(node, stanza.attr.from);
+ if not ok then
+ return;
+ end
+
+ for _, id in ipairs(ret) do
+ reply:tag("item", { jid = module.host, name = id }):up();
+ end
+ event.exists = true;
+end);
+
+
+module:hook("host-disco-items", function (event)
+ local stanza, origin, reply = event.stanza, event.origin, event.reply;
+ local ok, ret = service:get_nodes(event.stanza.attr.from);
+ if not ok then
+ return;
+ end
+ for node, node_obj in pairs(ret) do
+ reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up();
+ end
+end);
+
+local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
+local function get_affiliation(jid)
+ local bare_jid = jid_bare(jid);
+ if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then
+ return admin_aff;
+ end
+end
+
+function set_service(new_service)
+ service = new_service;
+ module.environment.service = service;
+ add_disco_features_from_service(service);
+end
+
+function module.save()
+ return { service = service };
+end
+
+function module.restore(data)
+ set_service(data.service);
+end
+
+function module.load()
+ if module.reloading then return; end
+
+ set_service(pubsub.new({
+ capabilities = {
+ none = {
+ create = false;
+ publish = false;
+ retract = false;
+ get_nodes = true;
+
+ subscribe = true;
+ unsubscribe = true;
+ get_subscription = true;
+ get_subscriptions = true;
+ get_items = true;
+
+ subscribe_other = false;
+ unsubscribe_other = false;
+ get_subscription_other = false;
+ get_subscriptions_other = false;
+
+ be_subscribed = true;
+ be_unsubscribed = true;
+
+ set_affiliation = false;
+ };
+ publisher = {
+ create = false;
+ publish = true;
+ retract = true;
+ get_nodes = true;
+
+ subscribe = true;
+ unsubscribe = true;
+ get_subscription = true;
+ get_subscriptions = true;
+ get_items = true;
+
+ subscribe_other = false;
+ unsubscribe_other = false;
+ get_subscription_other = false;
+ get_subscriptions_other = false;
+
+ be_subscribed = true;
+ be_unsubscribed = true;
+
+ set_affiliation = false;
+ };
+ owner = {
+ create = true;
+ publish = true;
+ retract = true;
+ delete = true;
+ get_nodes = true;
+ configure = true;
+
+ subscribe = true;
+ unsubscribe = true;
+ get_subscription = true;
+ get_subscriptions = true;
+ get_items = true;
+
+
+ subscribe_other = true;
+ unsubscribe_other = true;
+ get_subscription_other = true;
+ get_subscriptions_other = true;
+
+ be_subscribed = true;
+ be_unsubscribed = true;
+
+ set_affiliation = true;
+ };
+ };
+
+ autocreate_on_publish = autocreate_on_publish;
+ autocreate_on_subscribe = autocreate_on_subscribe;
+
+ broadcaster = simple_broadcast;
+ get_affiliation = get_affiliation;
+
+ normalize_jid = jid_bare;
+ }));
+end
diff --git a/plugins/mod_pubsub/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua
new file mode 100644
index 00000000..1497c21c
--- /dev/null
+++ b/plugins/mod_pubsub/pubsub.lib.lua
@@ -0,0 +1,317 @@
+local st = require "util.stanza";
+local uuid_generate = require "util.uuid".generate;
+local dataform = require"util.dataforms".new;
+
+local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
+local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
+local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
+
+local _M = {};
+
+local handlers = {};
+_M.handlers = handlers;
+
+local pubsub_errors = {
+ ["conflict"] = { "cancel", "conflict" };
+ ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
+ ["jid-required"] = { "modify", "bad-request", nil, "jid-required" };
+ ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" };
+ ["item-not-found"] = { "cancel", "item-not-found" };
+ ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
+ ["forbidden"] = { "auth", "forbidden" };
+ ["not-allowed"] = { "cancel", "not-allowed" };
+};
+local function pubsub_error_reply(stanza, error)
+ local e = pubsub_errors[error];
+ local reply = st.error_reply(stanza, unpack(e, 1, 3));
+ if e[4] then
+ reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up();
+ end
+ return reply;
+end
+_M.pubsub_error_reply = pubsub_error_reply;
+
+local node_config_form = require"util.dataforms".new {
+ {
+ type = "hidden";
+ name = "FORM_TYPE";
+ value = "http://jabber.org/protocol/pubsub#node_config";
+ };
+ {
+ type = "text-single";
+ name = "pubsub#max_items";
+ label = "Max # of items to persist";
+ };
+};
+
+function handlers.get_items(origin, stanza, items, service)
+ local node = items.attr.node;
+ local item = items:get_child("item");
+ local id = item and item.attr.id;
+
+ if not node then
+ origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+ return true;
+ end
+ local ok, results = service:get_items(node, stanza.attr.from, id);
+ if not ok then
+ origin.send(pubsub_error_reply(stanza, results));
+ return true;
+ end
+
+ local data = st.stanza("items", { node = node });
+ for _, id in ipairs(results) do
+ data:add_child(results[id]);
+ end
+ local reply;
+ if data then
+ reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub })
+ :add_child(data);
+ else
+ reply = pubsub_error_reply(stanza, "item-not-found");
+ end
+ origin.send(reply);
+ return true;
+end
+
+function handlers.get_subscriptions(origin, stanza, subscriptions, service)
+ local node = subscriptions.attr.node;
+ local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from);
+ if not ok then
+ origin.send(pubsub_error_reply(stanza, ret));
+ return true;
+ end
+ local reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub })
+ :tag("subscriptions");
+ for _, sub in ipairs(ret) do
+ reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
+ end
+ origin.send(reply);
+ return true;
+end
+
+function handlers.set_create(origin, stanza, create, service)
+ local node = create.attr.node;
+ local ok, ret, reply;
+ if node then
+ ok, ret = service:create(node, stanza.attr.from);
+ if ok then
+ reply = st.reply(stanza);
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ else
+ repeat
+ node = uuid_generate();
+ ok, ret = service:create(node, stanza.attr.from);
+ until ok or ret ~= "conflict";
+ if ok then
+ reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub })
+ :tag("create", { node = node });
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ end
+ origin.send(reply);
+ return true;
+end
+
+function handlers.set_delete(origin, stanza, delete, service)
+ local node = delete.attr.node;
+
+ local reply, notifier;
+ if not node then
+ origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+ return true;
+ end
+ local ok, ret = service:delete(node, stanza.attr.from);
+ if ok then
+ reply = st.reply(stanza);
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ origin.send(reply);
+ return true;
+end
+
+function handlers.set_subscribe(origin, stanza, subscribe, service)
+ local node, jid = subscribe.attr.node, subscribe.attr.jid;
+ if not (node and jid) then
+ origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
+ return true;
+ end
+ --[[
+ local options_tag, options = stanza.tags[1]:get_child("options"), nil;
+ if options_tag then
+ options = options_form:data(options_tag.tags[1]);
+ end
+ --]]
+ local options_tag, options; -- FIXME
+ local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
+ local reply;
+ if ok then
+ reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub })
+ :tag("subscription", {
+ node = node,
+ jid = jid,
+ subscription = "subscribed"
+ }):up();
+ if options_tag then
+ reply:add_child(options_tag);
+ end
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ origin.send(reply);
+end
+
+function handlers.set_unsubscribe(origin, stanza, unsubscribe, service)
+ local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
+ if not (node and jid) then
+ origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
+ return true;
+ end
+ local ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
+ local reply;
+ if ok then
+ reply = st.reply(stanza);
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ origin.send(reply);
+ return true;
+end
+
+function handlers.set_publish(origin, stanza, publish, service)
+ local node = publish.attr.node;
+ if not node then
+ origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+ return true;
+ end
+ local item = publish:get_child("item");
+ local id = (item and item.attr.id);
+ if not id then
+ id = uuid_generate();
+ if item then
+ item.attr.id = id;
+ end
+ end
+ local ok, ret = service:publish(node, stanza.attr.from, id, item);
+ local reply;
+ if ok then
+ reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub })
+ :tag("publish", { node = node })
+ :tag("item", { id = id });
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ origin.send(reply);
+ return true;
+end
+
+function handlers.set_retract(origin, stanza, retract, service)
+ local node, notify = retract.attr.node, retract.attr.notify;
+ notify = (notify == "1") or (notify == "true");
+ local item = retract:get_child("item");
+ local id = item and item.attr.id
+ if not (node and id) then
+ origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required"));
+ return true;
+ end
+ local reply, notifier;
+ if notify then
+ notifier = st.stanza("retract", { id = id });
+ end
+ local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
+ if ok then
+ reply = st.reply(stanza);
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ origin.send(reply);
+ return true;
+end
+
+function handlers.set_purge(origin, stanza, purge, service)
+ local node, notify = purge.attr.node, purge.attr.notify;
+ notify = (notify == "1") or (notify == "true");
+ local reply;
+ if not node then
+ origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+ return true;
+ end
+ local ok, ret = service:purge(node, stanza.attr.from, notify);
+ if ok then
+ reply = st.reply(stanza);
+ else
+ reply = pubsub_error_reply(stanza, ret);
+ end
+ origin.send(reply);
+ return true;
+end
+
+function handlers.get_configure(origin, stanza, config, service)
+ local node = config.attr.node;
+ if not node then
+ origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+ return true;
+ end
+
+ if not service:may(node, stanza.attr.from, "configure") then
+ origin.send(pubsub_error_reply(stanza, "forbidden"));
+ return true;
+ end
+
+ local node_obj = service.nodes[node];
+ if not node_obj then
+ origin.send(pubsub_error_reply(stanza, "item-not-found"));
+ return true;
+ end
+
+ local reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub_owner })
+ :tag("configure", { node = node })
+ :add_child(node_config_form:form(node_obj.config));
+ origin.send(reply);
+ return true;
+end
+
+function handlers.set_configure(origin, stanza, config, service)
+ local node = config.attr.node;
+ if not node then
+ origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+ return true;
+ end
+ if not service:may(node, stanza.attr.from, "configure") then
+ origin.send(pubsub_error_reply(stanza, "forbidden"));
+ return true;
+ end
+ local new_config, err = node_config_form:data(config.tags[1]);
+ if not new_config then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", err));
+ return true;
+ end
+ local ok, err = service:set_node_config(node, stanza.attr.from, new_config);
+ if not ok then
+ origin.send(pubsub_error_reply(stanza, err));
+ return true;
+ end
+ origin.send(st.reply(stanza));
+ return true;
+end
+
+function handlers.get_default(origin, stanza, default, service)
+ local reply = st.reply(stanza)
+ :tag("pubsub", { xmlns = xmlns_pubsub_owner })
+ :tag("default")
+ :add_child(node_config_form:form(service.node_defaults));
+ origin.send(reply);
+ return true;
+end
+
+return _M;
diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua
index 63d0b077..b39ce090 100644
--- a/plugins/mod_register.lua
+++ b/plugins/mod_register.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -13,9 +13,10 @@ local usermanager_user_exists = require "core.usermanager".user_exists;
local usermanager_create_user = require "core.usermanager".create_user;
local usermanager_set_password = require "core.usermanager".set_password;
local usermanager_delete_user = require "core.usermanager".delete_user;
-local os_time = os.time;
local nodeprep = require "util.encodings".stringprep.nodeprep;
local jid_bare = require "util.jid".bare;
+local create_throttle = require "util.throttle".create;
+local new_cache = require "util.cache".new;
local compat = module:get_option_boolean("registration_compat", true);
local allow_registration = module:get_option_boolean("allow_registration", false);
@@ -41,30 +42,37 @@ local field_map = {
date = { name = "date", type = "text-single", label = "Birth date" };
};
+local title = module:get_option_string("registration_title",
+ "Creating a new account");
+local instructions = module:get_option_string("registration_instructions",
+ "Choose a username and password for use with this service.");
+
local registration_form = dataform_new{
- title = "Creating a new account";
- instructions = "Choose a username and password for use with this service.";
+ title = title;
+ instructions = instructions;
field_map.username;
field_map.password;
};
local registration_query = st.stanza("query", {xmlns = "jabber:iq:register"})
- :tag("instructions"):text("Choose a username and password for use with this service."):up()
+ :tag("instructions"):text(instructions):up()
:tag("username"):up()
:tag("password"):up();
for _, field in ipairs(additional_fields) do
if type(field) == "table" then
registration_form[#registration_form + 1] = field;
- else
+ elseif field_map[field] or field_map[field:sub(1, -2)] then
if field:match("%+$") then
- field = field:sub(1, #field - 1);
+ field = field:sub(1, -2);
field_map[field].required = true;
end
registration_form[#registration_form + 1] = field_map[field];
registration_query:tag(field):up();
+ else
+ module:log("error", "Unknown field %q", field);
end
end
registration_query:add_child(registration_form:form());
@@ -73,7 +81,7 @@ module:add_feature("jabber:iq:register");
local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up();
module:hook("stream-features", function(event)
- local session, features = event.origin, event.features;
+ local session, features = event.origin, event.features;
-- Advertise registration to unauthorized clients only.
if not(allow_registration) or session.type ~= "c2s_unauthed" or (require_encryption and not session.secure) then
@@ -83,8 +91,10 @@ module:hook("stream-features", function(event)
features:add_child(register_stream_feature);
end);
+-- Password change and account deletion handler
local function handle_registration_stanza(event)
local session, stanza = event.origin, event.stanza;
+ local log = session.log or module._log;
local query = stanza.tags[1];
if stanza.attr.type == "get" then
@@ -98,29 +108,30 @@ local function handle_registration_stanza(event)
if query.tags[1] and query.tags[1].name == "remove" then
local username, host = session.username, session.host;
+ -- This one weird trick sends a reply to this stanza before the user is deleted
local old_session_close = session.close;
- session.close = function(session, ...)
- session.send(st.reply(stanza));
- return old_session_close(session, ...);
+ session.close = function(self, ...)
+ self.send(st.reply(stanza));
+ return old_session_close(self, ...);
end
-
+
local ok, err = usermanager_delete_user(username, host);
-
+
if not ok then
- module:log("debug", "Removing user account %s@%s failed: %s", username, host, err);
+ log("debug", "Removing user account %s@%s failed: %s", username, host, err);
session.close = old_session_close;
session.send(st.error_reply(stanza, "cancel", "service-unavailable", err));
return true;
end
-
- module:log("info", "User removed their account: %s@%s", username, host);
+
+ log("info", "User removed their account: %s@%s", username, host);
module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session });
else
local username = nodeprep(query:get_child_text("username"));
local password = query:get_child_text("password");
if username and password then
if username == session.username then
- if usermanager_set_password(username, password, session.host) then
+ if usermanager_set_password(username, password, session.host, session.resource) then
session.send(st.reply(stanza));
else
-- TODO unable to write file, file may be locked, etc, what's the correct error?
@@ -170,19 +181,40 @@ local function parse_response(query)
end
end
-local recent_ips = {};
-local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations");
-local whitelist_only = module:get_option("whitelist_registration_only");
-local whitelisted_ips = module:get_option("registration_whitelist") or { "127.0.0.1" };
-local blacklisted_ips = module:get_option("registration_blacklist") or {};
+local min_seconds_between_registrations = module:get_option_number("min_seconds_between_registrations");
+local whitelist_only = module:get_option_boolean("whitelist_registration_only");
+local whitelisted_ips = module:get_option_set("registration_whitelist", { "127.0.0.1", "::1" })._items;
+local blacklisted_ips = module:get_option_set("registration_blacklist", {})._items;
+
+local throttle_max = module:get_option_number("registration_throttle_max", min_seconds_between_registrations and 1);
+local throttle_period = module:get_option_number("registration_throttle_period", min_seconds_between_registrations);
+local throttle_cache_size = module:get_option_number("registration_throttle_cache_size", 100);
+local blacklist_overflow = module:get_option_boolean("blacklist_on_registration_throttle_overload", false);
-for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end
-for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end
+local throttle_cache = new_cache(throttle_cache_size, blacklist_overflow and function (ip, throttle)
+ if not throttle:peek() then
+ module:log("info", "Adding ip %s to registration blacklist", ip);
+ blacklisted_ips[ip] = true;
+ end
+end or nil);
+
+local function check_throttle(ip)
+ if not throttle_max then return true end
+ local throttle = throttle_cache:get(ip);
+ if not throttle then
+ throttle = create_throttle(throttle_max, throttle_period);
+ end
+ throttle_cache:set(ip, throttle);
+ return throttle:poll(1);
+end
+-- In-band registration
module:hook("stanza/iq/jabber:iq:register:query", function(event)
local session, stanza = event.origin, event.stanza;
+ local log = session.log or module._log;
if not(allow_registration) or session.type ~= "c2s_unauthed" then
+ log("debug", "Attempted registration when disabled or already authenticated");
session.send(st.error_reply(stanza, "cancel", "service-unavailable"));
elseif require_encryption and not session.secure then
session.send(st.error_reply(stanza, "modify", "policy-violation", "Encryption is required"));
@@ -198,57 +230,59 @@ module:hook("stanza/iq/jabber:iq:register:query", function(event)
else
local data, errors = parse_response(query);
if errors then
+ log("debug", "Error parsing registration form:");
+ for field, err in pairs(errors) do
+ log("debug", "Field %q: %s", field, err);
+ end
session.send(st.error_reply(stanza, "modify", "not-acceptable"));
else
-- Check that the user is not blacklisted or registering too often
if not session.ip then
- module:log("debug", "User's IP not known; can't apply blacklist/whitelist");
+ log("debug", "User's IP not known; can't apply blacklist/whitelist");
elseif blacklisted_ips[session.ip] or (whitelist_only and not whitelisted_ips[session.ip]) then
session.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not allowed to register an account."));
return true;
- elseif min_seconds_between_registrations and not whitelisted_ips[session.ip] then
- if not recent_ips[session.ip] then
- recent_ips[session.ip] = { time = os_time(), count = 1 };
- else
- local ip = recent_ips[session.ip];
- ip.count = ip.count + 1;
-
- if os_time() - ip.time < min_seconds_between_registrations then
- ip.time = os_time();
- session.send(st.error_reply(stanza, "wait", "not-acceptable"));
- return true;
- end
- ip.time = os_time();
+ elseif throttle_max and not whitelisted_ips[session.ip] then
+ if not check_throttle(session.ip) then
+ log("debug", "Registrations over limit for ip %s", session.ip or "?");
+ session.send(st.error_reply(stanza, "wait", "not-acceptable"));
+ return true;
end
end
local username, password = nodeprep(data.username), data.password;
data.username, data.password = nil, nil;
local host = module.host;
if not username or username == "" then
+ log("debug", "The requested username is invalid.");
session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is invalid."));
return true;
end
- local user = { username = username , host = host, allowed = true }
+ local user = { username = username , host = host, additional = data, allowed = true }
module:fire_event("user-registering", user);
if not user.allowed then
+ log("debug", "Registration disallowed by module");
session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is forbidden."));
elseif usermanager_user_exists(username, host) then
+ log("debug", "Attempt to register with existing username");
session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists."));
else
-- TODO unable to write file, file may be locked, etc, what's the correct error?
local error_reply = st.error_reply(stanza, "wait", "internal-server-error", "Failed to write data to disk.");
if usermanager_create_user(username, password, host) then
- if next(data) and not account_details:set(username, data) then
+ data.registered = os.time();
+ if not account_details:set(username, data) then
+ log("debug", "Could not store extra details");
usermanager_delete_user(username, host);
session.send(error_reply);
return true;
end
session.send(st.reply(stanza)); -- user created!
- module:log("info", "User account created: %s@%s", username, host);
+ log("info", "User account created: %s@%s", username, host);
module:fire_event("user-registered", {
username = username, host = host, source = "mod_register",
session = session });
else
+ log("debug", "Could not create user");
session.send(error_reply);
end
end
diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua
index d530bb45..24c50678 100644
--- a/plugins/mod_roster.lua
+++ b/plugins/mod_roster.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -19,7 +19,6 @@ local rm_load_roster = require "core.rostermanager".load_roster;
local rm_remove_from_roster = require "core.rostermanager".remove_from_roster;
local rm_add_to_roster = require "core.rostermanager".add_to_roster;
local rm_roster_push = require "core.rostermanager".roster_push;
-local core_post_stanza = prosody.core_post_stanza;
module:add_feature("jabber:iq:roster");
@@ -36,15 +35,15 @@ module:hook("iq/self/jabber:iq:roster:query", function(event)
if stanza.attr.type == "get" then
local roster = st.reply(stanza);
-
+
local client_ver = tonumber(stanza.tags[1].attr.ver);
local server_ver = tonumber(session.roster[false].version or 1);
-
+
if not (client_ver and server_ver) or client_ver ~= server_ver then
roster:query("jabber:iq:roster");
-- Client does not support versioning, or has stale roster
for jid, item in pairs(session.roster) do
- if jid ~= "pending" and jid then
+ if jid then
roster:tag("item", {
jid = jid,
subscription = item.subscription,
@@ -64,9 +63,7 @@ module:hook("iq/self/jabber:iq:roster:query", function(event)
else -- stanza.attr.type == "set"
local query = stanza.tags[1];
if #query.tags == 1 and query.tags[1].name == "item"
- and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid
- -- Protection against overwriting roster.pending, until we move it
- and query.tags[1].attr.jid ~= "pending" then
+ and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid then
local item = query.tags[1];
local from_node, from_host = jid_split(stanza.attr.from);
local jid = jid_prep(item.attr.jid);
@@ -77,13 +74,9 @@ module:hook("iq/self/jabber:iq:roster:query", function(event)
local roster = session.roster;
local r_item = roster[jid];
if r_item then
- local to_bare = node and (node.."@"..host) or host; -- bare JID
- if r_item.subscription == "both" or r_item.subscription == "from" or (roster.pending and roster.pending[jid]) then
- core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=to_bare}));
- end
- if r_item.subscription == "both" or r_item.subscription == "to" or r_item.ask then
- core_post_stanza(session, st.presence({type="unsubscribe", from=session.full_jid, to=to_bare}));
- end
+ module:fire_event("roster-item-removed", {
+ username = node, jid = jid, item = r_item, origin = session, roster = roster,
+ });
local success, err_type, err_cond, err_msg = rm_remove_from_roster(session, jid);
if success then
session.send(st.reply(stanza));
@@ -140,16 +133,19 @@ end);
module:hook_global("user-deleted", function(event)
local username, host = event.username, event.host;
+ local origin = event.origin or prosody.hosts[host];
if host ~= module.host then return end
- local bare = username .. "@" .. host;
local roster = rm_load_roster(username, host);
for jid, item in pairs(roster) do
- if jid and jid ~= "pending" then
- if item.subscription == "both" or item.subscription == "from" or (roster.pending and roster.pending[jid]) then
- module:send(st.presence({type="unsubscribed", from=bare, to=jid}));
- end
- if item.subscription == "both" or item.subscription == "to" or item.ask then
- module:send(st.presence({type="unsubscribe", from=bare, to=jid}));
+ if jid then
+ module:fire_event("roster-item-removed", {
+ username = username, jid = jid, item = item, roster = roster, origin = origin,
+ });
+ else
+ for pending_jid in pairs(item.pending) do
+ module:fire_event("roster-item-removed", {
+ username = username, jid = pending_jid, roster = roster, origin = origin,
+ });
end
end
end
diff --git a/plugins/mod_s2s/mod_s2s.lua b/plugins/mod_s2s/mod_s2s.lua
index 10b81a17..aa7fe8b6 100644
--- a/plugins/mod_s2s/mod_s2s.lua
+++ b/plugins/mod_s2s/mod_s2s.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -15,7 +15,6 @@ local core_process_stanza = prosody.core_process_stanza;
local tostring, type = tostring, type;
local t_insert = table.insert;
local xpcall, traceback = xpcall, debug.traceback;
-local NULL = {};
local add_task = require "util.timer".add_task;
local st = require "util.stanza";
@@ -26,7 +25,6 @@ local s2s_new_incoming = require "core.s2smanager".new_incoming;
local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
local s2s_destroy_session = require "core.s2smanager".destroy_session;
local uuid_gen = require "util.uuid".generate;
-local cert_verify_identity = require "util.x509".verify_identity;
local fire_global_event = prosody.events.fire_event;
local s2sout = module:require("s2sout");
@@ -39,10 +37,20 @@ local secure_domains, insecure_domains =
module:get_option_set("s2s_secure_domains", {})._items, module:get_option_set("s2s_insecure_domains", {})._items;
local require_encryption = module:get_option_boolean("s2s_require_encryption", false);
+local measure_connections = module:measure("connections", "amount");
+
local sessions = module:shared("sessions");
local log = module._log;
+module:hook("stats-update", function ()
+ local count = 0;
+ for _ in pairs(sessions) do
+ count = count + 1;
+ end
+ measure_connections(count);
+end);
+
--- Handle stanzas to remote domains
local bouncy_stanzas = { message = true, presence = true, iq = true };
@@ -135,6 +143,12 @@ function route_to_new_session(event)
return true;
end
+local function keepalive(event)
+ return event.session.sends2s(' ');
+end
+
+module:hook("s2s-read-timeout", keepalive, -1);
+
function module.add_host(module)
if module:get_option_boolean("disallow_s2s", false) then
module:log("warn", "The 'disallow_s2s' config option is deprecated, please see http://prosody.im/doc/s2s#disabling");
@@ -143,15 +157,29 @@ function module.add_host(module)
module:hook("route/remote", route_to_existing_session, -1);
module:hook("route/remote", route_to_new_session, -10);
module:hook("s2s-authenticated", make_authenticated, -1);
+ module:hook("s2s-read-timeout", keepalive, -1);
+ module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
+ if session.type == "s2sout" then
+ -- Stream is authenticated and we are seem to be done with feature negotiation,
+ -- so the stream is ready for stanzas. RFC 6120 Section 4.3
+ mark_connected(session);
+ return true;
+ elseif not session.dialback_verifying then
+ session.log("warn", "No SASL EXTERNAL offer and Dialback doesn't seem to be enabled, giving up");
+ session:close();
+ return false;
+ end
+ end, -1);
end
-- Stream is authorised, and ready for normal stanzas
function mark_connected(session)
+
local sendq = session.sendq;
-
+
local from, to = session.from_host, session.to_host;
-
- session.log("info", "%s s2s connection %s->%s complete", session.direction, from, to);
+
+ session.log("info", "%s s2s connection %s->%s complete", session.direction:gsub("^.", string.upper), from, to);
local event_data = { session = session };
if session.type == "s2sout" then
@@ -166,7 +194,7 @@ function mark_connected(session)
fire_global_event("s2sin-established", event_data);
hosts[to].events.fire_event("s2sin-established", event_data);
end
-
+
if session.direction == "outgoing" then
if sendq then
session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host);
@@ -177,7 +205,8 @@ function mark_connected(session)
end
session.sendq = nil;
end
-
+
+ session.resolver = nil;
session.ip_hosts = nil;
session.srv_hosts = nil;
end
@@ -212,14 +241,17 @@ function make_authenticated(event)
return false;
end
session.log("debug", "connection %s->%s is now authenticated for %s", session.from_host, session.to_host, host);
-
- mark_connected(session);
-
+
+ if (session.type == "s2sout" and session.external_auth ~= "succeeded") or session.type == "s2sin" then
+ -- Stream either used dialback for authentication or is an incoming stream.
+ mark_connected(session);
+ end
+
return true;
end
--- Helper to check that a session peer's certificate is valid
-local function check_cert_status(session)
+function check_cert_status(session)
local host = session.direction == "outgoing" and session.to_host or session.from_host
local conn = session.conn:socket()
local cert
@@ -227,39 +259,6 @@ local function check_cert_status(session)
cert = conn:getpeercertificate()
end
- if cert then
- local chain_valid, errors;
- if conn.getpeerverification then
- chain_valid, errors = conn:getpeerverification();
- elseif conn.getpeerchainvalid then -- COMPAT mw/luasec-hg
- chain_valid, errors = conn:getpeerchainvalid();
- errors = (not chain_valid) and { { errors } } or nil;
- else
- chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } };
- end
- -- Is there any interest in printing out all/the number of errors here?
- if not chain_valid then
- (session.log or log)("debug", "certificate chain validation result: invalid");
- for depth, t in pairs(errors or NULL) do
- (session.log or log)("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", "))
- end
- session.cert_chain_status = "invalid";
- else
- (session.log or log)("debug", "certificate chain validation result: valid");
- session.cert_chain_status = "valid";
-
- -- We'll go ahead and verify the asserted identity if the
- -- connecting server specified one.
- if host then
- if cert_verify_identity(host, "xmpp-server", cert) then
- session.cert_identity_status = "valid"
- else
- session.cert_identity_status = "invalid"
- end
- (session.log or log)("debug", "certificate identity validation result: %s", session.cert_identity_status);
- end
- end
- end
return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert });
end
@@ -271,23 +270,26 @@ local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
function stream_callbacks.streamopened(session, attr)
session.version = tonumber(attr.version) or 0;
-
+
-- TODO: Rename session.secure to session.encrypted
if session.secure == false then
session.secure = true;
+ session.encrypted = true;
- -- Check if TLS compression is used
local sock = session.conn:socket();
if sock.info then
- session.compressed = sock:info"compression";
- elseif sock.compression then
- session.compressed = sock:compression(); --COMPAT mw/luasec-hg
+ local info = sock:info();
+ (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
+ session.compressed = info.compression;
+ else
+ (session.log or log)("info", "Stream encrypted");
+ session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg
end
end
if session.direction == "incoming" then
-- Send a reply stream header
-
+
-- Validate to/from
local to, from = nameprep(attr.to), nameprep(attr.from);
if not to and attr.to then -- COMPAT: Some servers do not reliably set 'to' (especially on stream restarts)
@@ -298,7 +300,7 @@ function stream_callbacks.streamopened(session, attr)
session:close({ condition = "improper-addressing", text = "Invalid 'from' address" });
return;
end
-
+
-- Set session.[from/to]_host if they have not been set already and if
-- this session isn't already authenticated
if session.type == "s2sin_unauthed" and from and not session.from_host then
@@ -313,10 +315,10 @@ function stream_callbacks.streamopened(session, attr)
session:close({ condition = "improper-addressing", text = "New stream 'to' attribute does not match original" });
return;
end
-
+
-- For convenience we'll put the sanitised values into these variables
to, from = session.to_host, session.from_host;
-
+
session.streamid = uuid_gen();
(session.log or log)("debug", "Incoming s2s received %s", st.stanza("stream:stream", attr):top_tag());
if to then
@@ -352,15 +354,21 @@ function stream_callbacks.streamopened(session, attr)
session.notopen = nil;
if session.version >= 1.0 then
local features = st.stanza("stream:features");
-
+
if to then
hosts[to].events.fire_event("s2s-stream-features", { origin = session, features = features });
else
(session.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", from or session.ip or "unknown host");
+ fire_global_event("s2s-stream-features-legacy", { origin = session, features = features });
+ end
+
+ if ( session.type == "s2sin" or session.type == "s2sout" ) or features.tags[1] then
+ log("debug", "Sending stream features: %s", tostring(features));
+ session.sends2s(features);
+ else
+ (session.log or log)("warn", "No stream features to offer, giving up");
+ session:close({ condition = "undefined-condition", text = "No stream features to offer" });
end
-
- log("debug", "Sending stream features: %s", tostring(features));
- session.sends2s(features);
end
elseif session.direction == "outgoing" then
session.notopen = nil;
@@ -390,7 +398,7 @@ function stream_callbacks.streamopened(session, attr)
end
end
session.send_buffer = nil;
-
+
-- If server is pre-1.0, don't wait for features, just do dialback
if session.version < 1.0 then
if not session.dialback_verifying then
@@ -434,9 +442,6 @@ end
local function handleerr(err) log("error", "Traceback[s2s]: %s", traceback(tostring(err), 2)); end
function stream_callbacks.handlestanza(session, stanza)
- if stanza.attr.xmlns == "jabber:client" then --COMPAT: Prosody pre-0.6.2 may send jabber:client
- stanza.attr.xmlns = nil;
- end
stanza = session.filter("stanzas/in", stanza);
if stanza then
return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
@@ -481,10 +486,10 @@ local function session_close(session, reason, remote_reason)
session.sends2s("</stream:stream>");
function session.sends2s() return false; end
-
+
local reason = remote_reason or (reason and (reason.text or reason.condition)) or reason;
- session.log("info", "%s s2s stream %s->%s closed: %s", session.direction, session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed");
-
+ session.log("info", "%s s2s stream %s->%s closed: %s", session.direction:gsub("^.", string.upper), session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed");
+
-- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
local conn = session.conn;
if reason == nil and not session.notopen and session.type == "s2sin" then
@@ -502,47 +507,58 @@ local function session_close(session, reason, remote_reason)
end
end
-function session_open_stream(session, from, to)
- local attr = {
- ["xmlns:stream"] = 'http://etherx.jabber.org/streams',
- xmlns = 'jabber:server',
- version = session.version and (session.version > 0 and "1.0" or nil),
- ["xml:lang"] = 'en',
- id = session.streamid,
- from = from or "", to = to or "",
- }
+function session_stream_attrs(session, from, to, attr)
if not from or (hosts[from] and hosts[from].modules.dialback) then
attr["xmlns:db"] = 'jabber:server:dialback';
end
-
- session.sends2s("<?xml version='1.0'?>");
- session.sends2s(st.stanza("stream:stream", attr):top_tag());
- return true;
+ if not from then
+ attr.from = '';
+ end
+ if not to then
+ attr.to = '';
+ end
end
-- Session initialization logic shared by incoming and outgoing
local function initialize_session(session)
local stream = new_xmpp_stream(session, stream_callbacks);
+ local log = session.log or log;
session.stream = stream;
-
+
session.notopen = true;
-
+
function session.reset_stream()
session.notopen = true;
session.streamid = nil;
session.stream:reset();
end
- session.open_stream = session_open_stream;
-
- local filter = session.filter;
+ session.stream_attrs = session_stream_attrs;
+
+ local filter = initialize_filters(session);
+ local conn = session.conn;
+ local w = conn.write;
+
+ function session.sends2s(t)
+ log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^[^>]*>?"));
+ if t.name then
+ t = filter("stanzas/out", t);
+ end
+ if t then
+ t = filter("bytes/out", tostring(t));
+ if t then
+ return w(conn, t);
+ end
+ end
+ end
+
function session.data(data)
data = filter("bytes/in", data);
if data then
local ok, err = stream:feed(data);
if ok then return; end
- (session.log or log)("warn", "Received invalid XML: %s", data);
- (session.log or log)("warn", "Problem was: %s", err);
+ log("warn", "Received invalid XML: %s", data);
+ log("warn", "Problem was: %s", err);
session:close("not-well-formed");
end
end
@@ -554,6 +570,8 @@ local function initialize_session(session)
return handlestanza(session, stanza);
end
+ module:fire_event("s2s-created", { session = session });
+
add_task(connect_timeout, function ()
if session.type == "s2sin" or session.type == "s2sout" then
return; -- Ok, we're connected
@@ -574,26 +592,11 @@ function listener.onconnect(conn)
session = s2s_new_incoming(conn);
sessions[conn] = session;
session.log("debug", "Incoming s2s connection");
-
- local filter = initialize_filters(session);
- local w = conn.write;
- session.sends2s = function (t)
- log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)"));
- if t.name then
- t = filter("stanzas/out", t);
- end
- if t then
- t = filter("bytes/out", tostring(t));
- if t then
- return w(conn, t);
- end
- end
- end
-
initialize_session(session);
else -- Outgoing session connected
session:open_stream(session.from_host, session.to_host);
end
+ session.ip = conn:ip();
end
function listener.onincoming(conn, data)
@@ -602,7 +605,7 @@ function listener.onincoming(conn, data)
session.data(data);
end
end
-
+
function listener.onstatus(conn, status)
if status == "ssl-handshake-complete" then
local session = sessions[conn];
@@ -620,7 +623,6 @@ function listener.ondisconnect(conn, err)
if err and session.direction == "outgoing" and session.notopen then
(session.log or log)("debug", "s2s connection attempt failed: %s", err);
if s2sout.attempt_connection(session, err) then
- (session.log or log)("debug", "...so we're going to try another target");
return; -- Session lives for now
end
end
@@ -629,8 +631,15 @@ function listener.ondisconnect(conn, err)
end
end
+function listener.onreadtimeout(conn)
+ local session = sessions[conn];
+ if session then
+ local host = session.host or session.to_host;
+ return (hosts[host] or prosody).events.fire_event("s2s-read-timeout", { session = session });
+ end
+end
+
function listener.register_outgoing(conn, session)
- session.direction = "outgoing";
sessions[conn] = session;
initialize_session(session);
end
@@ -648,7 +657,7 @@ function check_auth_policy(event)
elseif must_secure and insecure_domains[host] then
must_secure = false;
end
-
+
if must_secure and (session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid") then
module:log("warn", "Forbidding insecure connection to/from %s", host or session.ip or "(unknown host)");
if session.direction == "incoming" then
diff --git a/plugins/mod_s2s/s2sout.lib.lua b/plugins/mod_s2s/s2sout.lib.lua
index dc122af7..cd8553e1 100644
--- a/plugins/mod_s2s/s2sout.lib.lua
+++ b/plugins/mod_s2s/s2sout.lib.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -22,6 +22,8 @@ local local_addresses = require "util.net".local_addresses;
local s2s_destroy_session = require "core.s2smanager".destroy_session;
+local default_mode = module:get_option("network_default_read_size", 4096);
+
local log = module._log;
local sources = {};
@@ -46,14 +48,16 @@ end
function s2sout.initiate_connection(host_session)
initialize_filters(host_session);
host_session.version = 1;
-
+
+ host_session.resolver = adns.resolver();
+
-- Kick the connection attempting machine into life
if not s2sout.attempt_connection(host_session) then
-- Intentionally not returning here, the
-- session is needed, connected or not
s2s_destroy_session(host_session);
end
-
+
if not host_session.sends2s then
-- A sends2s which buffers data (until the stream is opened)
-- note that data in this buffer will be sent before the stream is authed
@@ -74,22 +78,21 @@ end
function s2sout.attempt_connection(host_session, err)
local to_host = host_session.to_host;
local connect_host, connect_port = to_host and idna_to_ascii(to_host), 5269;
-
+
if not connect_host then
return false;
end
-
+
if not err then -- This is our first attempt
log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host);
host_session.connecting = true;
- local handle;
- handle = adns.lookup(function (answer)
- handle = nil;
+ host_session.resolver:lookup(function (answer)
+ local srv_hosts = { answer = answer };
+ host_session.srv_hosts = srv_hosts;
+ host_session.srv_choice = 0;
host_session.connecting = nil;
if answer and #answer > 0 then
log("debug", "%s has SRV records, handling...", to_host);
- local srv_hosts = { answer = answer };
- host_session.srv_hosts = srv_hosts;
for _, record in ipairs(answer) do
t_insert(srv_hosts, record.srv);
end
@@ -99,7 +102,7 @@ function s2sout.attempt_connection(host_session, err)
return;
end
t_sort(srv_hosts, compare_srv_priorities);
-
+
local srv_choice = srv_hosts[1];
host_session.srv_choice = 1;
if srv_choice then
@@ -118,7 +121,7 @@ function s2sout.attempt_connection(host_session, err)
end
end
end, "_xmpp-server._tcp."..connect_host..".", "SRV");
-
+
return true; -- Attempt in progress
elseif host_session.ip_hosts then
return s2sout.try_connect(host_session, connect_host, connect_port, err);
@@ -128,11 +131,11 @@ function s2sout.attempt_connection(host_session, err)
connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
host_session.log("info", "Connection failed (%s). Attempt #%d: This time to %s:%d", tostring(err), host_session.srv_choice, connect_host, connect_port);
else
- host_session.log("info", "Out of connection options, can't connect to %s", tostring(host_session.to_host));
+ host_session.log("info", "Failed in all attempts to connect to %s", tostring(host_session.to_host));
-- We're out of options
return false;
end
-
+
if not (connect_host and connect_port) then
-- Likely we couldn't resolve DNS
log("warn", "Hmm, we're without a host (%s) and port (%s) to connect to for %s, giving up :(", tostring(connect_host), tostring(connect_port), tostring(to_host));
@@ -165,7 +168,7 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err)
local have_other_result = not(has_ipv4) or not(has_ipv6) or false;
if has_ipv4 then
- handle4 = adns.lookup(function (reply, err)
+ handle4 = host_session.resolver:lookup(function (reply, err)
handle4 = nil;
if reply and reply[#reply] and reply[#reply].a then
@@ -173,6 +176,8 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err)
log("debug", "DNS reply for %s gives us %s", connect_host, ip.a);
IPs[#IPs+1] = new_ip(ip.a, "IPv4");
end
+ elseif err then
+ log("debug", "Error in DNS lookup: %s", err);
end
if have_other_result then
@@ -201,7 +206,7 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err)
end
if has_ipv6 then
- handle6 = adns.lookup(function (reply, err)
+ handle6 = host_session.resolver:lookup(function (reply, err)
handle6 = nil;
if reply and reply[#reply] and reply[#reply].aaaa then
@@ -209,6 +214,8 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err)
log("debug", "DNS reply for %s gives us %s", connect_host, ip.aaaa);
IPs[#IPs+1] = new_ip(ip.aaaa, "IPv6");
end
+ elseif err then
+ log("debug", "Error in DNS lookup: %s", err);
end
if have_other_result then
@@ -252,11 +259,12 @@ function s2sout.try_connect(host_session, connect_host, connect_port, err)
end
function s2sout.make_connect(host_session, connect_host, connect_port)
- (host_session.log or log)("info", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port);
+ (host_session.log or log)("debug", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port);
-- Reset secure flag in case this is another
-- connection attempt after a failed STARTTLS
host_session.secure = nil;
+ host_session.encrypted = nil;
local conn, handler;
local proto = connect_host.proto;
@@ -267,7 +275,7 @@ function s2sout.make_connect(host_session, connect_host, connect_port)
else
handler = "Unsupported protocol: "..tostring(proto);
end
-
+
if not conn then
log("warn", "Failed to create outgoing connection, system error: %s", handler);
return false, handler;
@@ -279,29 +287,14 @@ function s2sout.make_connect(host_session, connect_host, connect_port)
log("warn", "s2s connect() to %s (%s:%d) failed: %s", host_session.to_host, connect_host.addr, connect_port, err);
return false, err;
end
-
- conn = wrapclient(conn, connect_host.addr, connect_port, s2s_listener, "*a");
+
+ conn = wrapclient(conn, connect_host.addr, connect_port, s2s_listener, default_mode);
host_session.conn = conn;
-
- local filter = initialize_filters(host_session);
- local w, log = conn.write, host_session.log;
- host_session.sends2s = function (t)
- log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
- if t.name then
- t = filter("stanzas/out", t);
- end
- if t then
- t = filter("bytes/out", tostring(t));
- if t then
- return w(conn, tostring(t));
- end
- end
- end
-
+
-- Register this outgoing connection so that xmppserver_listener knows about it
-- otherwise it will assume it is a new incoming connection
s2s_listener.register_outgoing(conn, host_session);
-
+
log("debug", "Connection attempt in progress...");
return true;
end
diff --git a/plugins/mod_s2s_auth_certs.lua b/plugins/mod_s2s_auth_certs.lua
new file mode 100644
index 00000000..dd0eb3cb
--- /dev/null
+++ b/plugins/mod_s2s_auth_certs.lua
@@ -0,0 +1,49 @@
+module:set_global();
+
+local cert_verify_identity = require "util.x509".verify_identity;
+local NULL = {};
+local log = module._log;
+
+module:hook("s2s-check-certificate", function(event)
+ local session, host, cert = event.session, event.host, event.cert;
+ local conn = session.conn:socket();
+ local log = session.log or log;
+
+ if not cert then
+ log("warn", "No certificate provided by %s", host or "unknown host");
+ return;
+ end
+
+ local chain_valid, errors;
+ if conn.getpeerverification then
+ chain_valid, errors = conn:getpeerverification();
+ elseif conn.getpeerchainvalid then -- COMPAT mw/luasec-hg
+ chain_valid, errors = conn:getpeerchainvalid();
+ errors = (not chain_valid) and { { errors } } or nil;
+ else
+ chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } };
+ end
+ -- Is there any interest in printing out all/the number of errors here?
+ if not chain_valid then
+ log("debug", "certificate chain validation result: invalid");
+ for depth, t in pairs(errors or NULL) do
+ log("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", "))
+ end
+ session.cert_chain_status = "invalid";
+ else
+ log("debug", "certificate chain validation result: valid");
+ session.cert_chain_status = "valid";
+
+ -- We'll go ahead and verify the asserted identity if the
+ -- connecting server specified one.
+ if host then
+ if cert_verify_identity(host, "xmpp-server", cert) then
+ session.cert_identity_status = "valid"
+ else
+ session.cert_identity_status = "invalid"
+ end
+ log("debug", "certificate identity validation result: %s", session.cert_identity_status);
+ end
+ end
+end, 509);
+
diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua
index a23d1f53..fba84ef8 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -1,11 +1,11 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-
+-- luacheck: ignore 431/log
local st = require "util.stanza";
@@ -13,13 +13,13 @@ local sm_bind_resource = require "core.sessionmanager".bind_resource;
local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
local base64 = require "util.encodings".base64;
-local cert_verify_identity = require "util.x509".verify_identity;
-
local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler;
local tostring = tostring;
-local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
-local allow_unencrypted_plain_auth = module:get_option("allow_unencrypted_plain_auth")
+local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", false));
+local allow_unencrypted_plain_auth = module:get_option_boolean("allow_unencrypted_plain_auth", false)
+local insecure_mechanisms = module:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth and {} or {"PLAIN", "LOGIN"});
+local disabled_mechanisms = module:get_option_set("disable_sasl_mechanisms", { "DIGEST-MD5" });
local log = module._log;
@@ -28,15 +28,15 @@ local xmlns_bind ='urn:ietf:params:xml:ns:xmpp-bind';
local function build_reply(status, ret, err_msg)
local reply = st.stanza(status, {xmlns = xmlns_sasl});
- if status == "challenge" then
- --log("debug", "CHALLENGE: %s", ret or "");
- reply:text(base64.encode(ret or ""));
- elseif status == "failure" then
+ if status == "failure" then
reply:tag(ret):up();
if err_msg then reply:tag("text"):text(err_msg); end
- elseif status == "success" then
- --log("debug", "SUCCESS: %s", ret or "");
- reply:text(base64.encode(ret or ""));
+ elseif status == "challenge" or status == "success" then
+ if ret == "" then
+ reply:text("=")
+ elseif ret then
+ reply:text(base64.encode(ret));
+ end
else
module:log("error", "Unknown sasl status: %s", status);
end
@@ -82,7 +82,7 @@ local function sasl_process_cdata(session, stanza)
return true;
end
-module:hook_stanza(xmlns_sasl, "success", function (session, stanza)
+module:hook_tag(xmlns_sasl, "success", function (session)
if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
module:log("debug", "SASL EXTERNAL with %s succeeded", session.to_host);
session.external_auth = "succeeded"
@@ -93,7 +93,7 @@ module:hook_stanza(xmlns_sasl, "success", function (session, stanza)
return true;
end)
-module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
+module:hook_tag(xmlns_sasl, "failure", function (session, stanza)
if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
local text = stanza:get_child_text("text");
@@ -110,13 +110,16 @@ module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
module:log("info", "SASL EXTERNAL with %s failed: %s", session.to_host, condition);
session.external_auth = "failed"
+ session.external_auth_failure_reason = condition;
end, 500)
-module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
- -- TODO: Dialback wasn't loaded. Do something useful.
+module:hook_tag(xmlns_sasl, "failure", function (session, stanza) -- luacheck: ignore 212/stanza
+ session.log("debug", "No fallback from SASL EXTERNAL failure, giving up");
+ session:close(nil, session.external_auth_failure_reason);
+ return true;
end, 90)
-module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
+module:hook_tag("http://etherx.jabber.org/streams", "features", function (session, stanza)
if session.type ~= "s2sout_unauthed" or not session.secure then return; end
local mechanisms = stanza:get_child("mechanisms", xmlns_sasl)
@@ -135,71 +138,52 @@ module:hook_stanza("http://etherx.jabber.org/streams", "features", function (ses
end, 150);
local function s2s_external_auth(session, stanza)
+ if session.external_auth ~= "offered" then return end -- Unexpected request
+
local mechanism = stanza.attr.mechanism;
- if not session.secure then
- if mechanism == "EXTERNAL" then
- session.sends2s(build_reply("failure", "encryption-required"))
- else
- session.sends2s(build_reply("failure", "invalid-mechanism"))
- end
+ if mechanism ~= "EXTERNAL" then
+ session.sends2s(build_reply("failure", "invalid-mechanism"));
return true;
end
- if mechanism ~= "EXTERNAL" or session.cert_chain_status ~= "valid" then
- session.sends2s(build_reply("failure", "invalid-mechanism"))
+ if not session.secure then
+ session.sends2s(build_reply("failure", "encryption-required"));
return true;
end
- local text = stanza[1]
+ local text = stanza[1];
if not text then
- session.sends2s(build_reply("failure", "malformed-request"))
- return true
+ session.sends2s(build_reply("failure", "malformed-request"));
+ return true;
end
- -- Either the value is "=" and we've already verified the external
- -- cert identity, or the value is a string and either matches the
- -- from_host (
-
- text = base64.decode(text)
+ text = base64.decode(text);
if not text then
- session.sends2s(build_reply("failure", "incorrect-encoding"))
+ session.sends2s(build_reply("failure", "incorrect-encoding"));
return true;
end
- if session.cert_identity_status == "valid" then
- if text ~= "" and text ~= session.from_host then
- session.sends2s(build_reply("failure", "invalid-authzid"))
- return true
- end
- else
- if text == "" then
- session.sends2s(build_reply("failure", "invalid-authzid"))
- return true
- end
-
- local cert = session.conn:socket():getpeercertificate()
- if (cert_verify_identity(text, "xmpp-server", cert)) then
- session.cert_identity_status = "valid"
- else
- session.cert_identity_status = "invalid"
- session.sends2s(build_reply("failure", "invalid-authzid"))
- return true
- end
+ -- The text value is either "" or equals session.from_host
+ if not ( text == "" or text == session.from_host ) then
+ session.sends2s(build_reply("failure", "invalid-authzid"));
+ return true;
end
- session.external_auth = "succeeded"
-
- if not session.from_host then
- session.from_host = text;
+ -- We've already verified the external cert identity before offering EXTERNAL
+ if session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid" then
+ session.sends2s(build_reply("failure", "not-authorized"));
+ session:close();
+ return true;
end
- session.sends2s(build_reply("success"))
- local domain = text ~= "" and text or session.from_host;
- module:log("info", "Accepting SASL EXTERNAL identity from %s", domain);
- module:fire_event("s2s-authenticated", { session = session, host = domain });
+ -- Success!
+ session.external_auth = "succeeded";
+ session.sends2s(build_reply("success"));
+ module:log("info", "Accepting SASL EXTERNAL identity from %s", session.from_host);
+ module:fire_event("s2s-authenticated", { session = session, host = session.from_host });
session:reset_stream();
- return true
+ return true;
end
module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
@@ -217,9 +201,12 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
session.sasl_handler = usermanager_get_sasl_handler(module.host, session);
end
local mechanism = stanza.attr.mechanism;
- if not session.secure and (secure_auth_only or (mechanism == "PLAIN" and not allow_unencrypted_plain_auth)) then
+ if not session.secure and (secure_auth_only or insecure_mechanisms:contains(mechanism)) then
session.send(build_reply("failure", "encryption-required"));
return true;
+ elseif disabled_mechanisms:contains(mechanism) then
+ session.send(build_reply("failure", "invalid-mechanism"));
+ return true;
end
local valid_mechanism = session.sasl_handler:select(mechanism);
if not valid_mechanism then
@@ -243,23 +230,55 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event)
return true;
end);
+local function tls_unique(self)
+ return self.userdata["tls-unique"]:getpeerfinished();
+end
+
local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' };
local bind_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-bind' };
local xmpp_session_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-session' };
module:hook("stream-features", function(event)
local origin, features = event.origin, event.features;
+ local log = origin.log or log;
if not origin.username then
if secure_auth_only and not origin.secure then
+ log("debug", "Not offering authentication on insecure connection");
return;
end
- origin.sasl_handler = usermanager_get_sasl_handler(module.host, origin);
+ local sasl_handler = usermanager_get_sasl_handler(module.host, origin)
+ origin.sasl_handler = sasl_handler;
+ if origin.encrypted then
+ -- check wether LuaSec has the nifty binding to the function needed for tls-unique
+ -- FIXME: would be nice to have this check only once and not for every socket
+ if sasl_handler.add_cb_handler then
+ local socket = origin.conn:socket();
+ if socket.getpeerfinished then
+ sasl_handler:add_cb_handler("tls-unique", tls_unique);
+ end
+ sasl_handler["userdata"] = {
+ ["tls-unique"] = socket;
+ };
+ end
+ end
local mechanisms = st.stanza("mechanisms", mechanisms_attr);
- for mechanism in pairs(origin.sasl_handler:mechanisms()) do
- if mechanism ~= "PLAIN" or origin.secure or allow_unencrypted_plain_auth then
+ local sasl_mechanisms = sasl_handler:mechanisms()
+ for mechanism in pairs(sasl_mechanisms) do
+ if disabled_mechanisms:contains(mechanism) then
+ log("debug", "Not offering disabled mechanism %s", mechanism);
+ elseif not origin.secure and insecure_mechanisms:contains(mechanism) then
+ log("debug", "Not offering mechanism %s on insecure connection", mechanism);
+ else
+ log("debug", "Offering mechanism %s", mechanism);
mechanisms:tag("mechanism"):text(mechanism):up();
end
end
- if mechanisms[1] then features:add_child(mechanisms); end
+ if mechanisms[1] then
+ features:add_child(mechanisms);
+ elseif not next(sasl_mechanisms) then
+ log("warn", "No available SASL mechanisms, verify that the configured authentication module is working");
+ else
+ log("warn", "All available authentication mechanisms are either disabled or not suitable for an insecure connection");
+ end
else
features:tag("bind", bind_attr):tag("required"):up():up();
features:tag("session", xmpp_session_attr):tag("optional"):up():up();
@@ -269,10 +288,10 @@ end);
module:hook("s2s-stream-features", function(event)
local origin, features = event.origin, event.features;
if origin.secure and origin.type == "s2sin_unauthed" then
- -- Offer EXTERNAL if chain is valid and either we didn't validate
- -- the identity or it passed.
- if origin.cert_chain_status == "valid" and origin.cert_identity_status ~= "invalid" then --TODO: Configurable
- module:log("debug", "Offering SASL EXTERNAL")
+ -- Offer EXTERNAL only if both chain and identity is valid.
+ if origin.cert_chain_status == "valid" and origin.cert_identity_status == "valid" then
+ module:log("debug", "Offering SASL EXTERNAL");
+ origin.external_auth = "offered"
features:tag("mechanisms", { xmlns = xmlns_sasl })
:tag("mechanism"):text("EXTERNAL")
:up():up();
@@ -280,12 +299,12 @@ module:hook("s2s-stream-features", function(event)
end
end);
-module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event)
+module:hook("stanza/iq/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event)
local origin, stanza = event.origin, event.stanza;
local resource;
if stanza.attr.type == "set" then
local bind = stanza.tags[1];
- resource = bind:child_with_name("resource");
+ resource = bind:get_child("resource");
resource = resource and #resource.tags == 0 and resource[1] or nil;
end
local success, err_type, err, err_msg = sm_bind_resource(origin, resource);
diff --git a/plugins/mod_server_contact_info.lua b/plugins/mod_server_contact_info.lua
new file mode 100644
index 00000000..7ee8a08f
--- /dev/null
+++ b/plugins/mod_server_contact_info.lua
@@ -0,0 +1,49 @@
+-- XEP-0157: Contact Addresses for XMPP Services for Prosody
+--
+-- Copyright (C) 2011-2016 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+--
+
+local t_insert = table.insert;
+local array = require "util.array";
+local df_new = require "util.dataforms".new;
+
+-- Source: http://xmpp.org/registrar/formtypes.html#http:--jabber.org-network-serverinfo
+local valid_types = {
+ abuse = true;
+ admin = true;
+ feedback = true;
+ sales = true;
+ security = true;
+ support = true;
+}
+
+local contact_config = module:get_option("contact_info");
+if not contact_config or not next(contact_config) then -- we'll use admins from the config as default
+ local admins = module:get_option_inherited_set("admins", {});
+ if admins:empty() then
+ module:log("error", "No contact_info or admins set in config");
+ return -- Nothing to attach, so we'll just skip it.
+ end
+ module:log("info", "No contact_info in config, using admins as fallback");
+ contact_config = {
+ admin = array.collect( admins / function(admin) return "xmpp:" .. admin; end);
+ };
+end
+
+local form_layout = {
+ { value = "http://jabber.org/network/serverinfo"; type = "hidden"; name = "FORM_TYPE"; };
+};
+
+local form_values = {};
+
+for t in pairs(valid_types) do
+ local addresses = contact_config[t];
+ if addresses then
+ t_insert(form_layout, { name = t .. "-addresses", type = "list-multi" });
+ form_values[t .. "-addresses"] = addresses;
+ end
+end
+
+module:add_extension(df_new(form_layout):form(form_values, "result"));
diff --git a/plugins/mod_stanza_debug.lua b/plugins/mod_stanza_debug.lua
new file mode 100644
index 00000000..6dedb6f7
--- /dev/null
+++ b/plugins/mod_stanza_debug.lua
@@ -0,0 +1,29 @@
+module:set_global();
+
+local tostring = tostring;
+local filters = require "util.filters";
+
+local function log_send(t, session)
+ if t and t ~= "" and t ~= " " then
+ session.log("debug", "SEND: %s", tostring(t));
+ end
+ return t;
+end
+
+local function log_recv(t, session)
+ if t and t ~= "" and t ~= " " then
+ session.log("debug", "RECV: %s", tostring(t));
+ end
+ return t;
+end
+
+local function init_raw_logging(session)
+ filters.add_filter(session, "stanzas/in", log_recv, -10000);
+ filters.add_filter(session, "stanzas/out", log_send, 10000);
+end
+
+filters.add_filter_hook(init_raw_logging);
+
+function module.unload()
+ filters.remove_filter_hook(init_raw_logging);
+end
diff --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua
index 972ecbee..76052575 100644
--- a/plugins/mod_storage_internal.lua
+++ b/plugins/mod_storage_internal.lua
@@ -1,31 +1,166 @@
local datamanager = require "core.storagemanager".olddm;
+local array = require "util.array";
+local datetime = require "util.datetime";
+local st = require "util.stanza";
+local now = require "util.time".now;
+local id = require "util.id".medium;
local host = module.host;
local driver = {};
-local driver_mt = { __index = driver };
function driver:open(store, typ)
- return setmetatable({ store = store, type = typ }, driver_mt);
+ local mt = self[typ or "keyval"]
+ if not mt then
+ return nil, "unsupported-store";
+ end
+ return setmetatable({ store = store, type = typ }, mt);
end
-function driver:get(user)
+
+function driver:stores(username) -- luacheck: ignore 212/self
+ return datamanager.stores(username, host);
+end
+
+function driver:purge(user) -- luacheck: ignore 212/self
+ return datamanager.purge(user, host);
+end
+
+local keyval = { };
+driver.keyval = { __index = keyval };
+
+function keyval:get(user)
return datamanager.load(user, host, self.store);
end
-function driver:set(user, data)
+function keyval:set(user, data)
return datamanager.store(user, host, self.store, data);
end
-function driver:stores(username)
- return datamanager.stores(username, host);
+function keyval:users()
+ return datamanager.users(host, self.store, self.type);
end
-function driver:users()
- return datamanager.users(host, self.store, self.type);
+local archive = {};
+driver.archive = { __index = archive };
+
+function archive:append(username, key, value, when, with)
+ key = key or id();
+ when = when or now();
+ if not st.is_stanza(value) then
+ return nil, "unsupported-datatype";
+ end
+ value = st.preserialize(st.clone(value));
+ value.key = key;
+ value.when = when;
+ value.with = with;
+ value.attr.stamp = datetime.datetime(when);
+ value.attr.stamp_legacy = datetime.legacy(when);
+ local ok, err = datamanager.list_append(username, host, self.store, value);
+ if not ok then return ok, err; end
+ return key;
end
-function driver:purge(user)
- return datamanager.purge(user, host);
+function archive:find(username, query)
+ local items, err = datamanager.list_load(username, host, self.store);
+ if not items then
+ if err then
+ return items, err;
+ else
+ return function () end, 0;
+ end
+ end
+ local count = #items;
+ local i = 0;
+ if query then
+ items = array(items);
+ if query.key then
+ items:filter(function (item)
+ return item.key == query.key;
+ end);
+ end
+ if query.with then
+ items:filter(function (item)
+ return item.with == query.with;
+ end);
+ end
+ if query.start then
+ items:filter(function (item)
+ return item.when >= query.start;
+ end);
+ end
+ if query["end"] then
+ items:filter(function (item)
+ return item.when <= query["end"];
+ end);
+ end
+ count = #items;
+ if query.reverse then
+ items:reverse();
+ if query.before then
+ for j = 1, count do
+ if (items[j].key or tostring(j)) == query.before then
+ i = j;
+ break;
+ end
+ end
+ end
+ elseif query.after then
+ for j = 1, count do
+ if (items[j].key or tostring(j)) == query.after then
+ i = j;
+ break;
+ end
+ end
+ end
+ if query.limit and #items - i > query.limit then
+ items[i+query.limit+1] = nil;
+ end
+ end
+ return function ()
+ i = i + 1;
+ local item = items[i];
+ if not item then return; end
+ local key = item.key or tostring(i);
+ local when = item.when or datetime.parse(item.attr.stamp);
+ local with = item.with;
+ item.key, item.when, item.with = nil, nil, nil;
+ item.attr.stamp = nil;
+ item.attr.stamp_legacy = nil;
+ item = st.deserialize(item);
+ return key, item, when, with;
+ end, count;
+end
+
+function archive:dates(username)
+ local items, err = datamanager.list_load(username, host, self.store);
+ if not items then return items, err; end
+ return array(items):pluck("when"):map(datetime.date):unique();
+end
+
+function archive:delete(username, query)
+ if not query or next(query) == nil then
+ return datamanager.list_store(username, host, self.store, nil);
+ end
+ for k in pairs(query) do
+ if k ~= "end" then return nil, "unsupported-query-field"; end
+ end
+ local items, err = datamanager.list_load(username, host, self.store);
+ if not items then
+ if err then
+ return items, err;
+ end
+ -- Store is empty
+ return 0;
+ end
+ items = array(items);
+ local count_before = #items;
+ items:filter(function (item)
+ return item.when > query["end"];
+ end);
+ local count = count_before - #items;
+ local ok, err = datamanager.list_store(username, host, self.store, items);
+ if not ok then return ok, err; end
+ return count;
end
module:provides("storage", driver);
diff --git a/plugins/mod_storage_none.lua b/plugins/mod_storage_none.lua
index 8f2d2f56..e05a0fb7 100644
--- a/plugins/mod_storage_none.lua
+++ b/plugins/mod_storage_none.lua
@@ -1,8 +1,13 @@
+-- luacheck: ignore 212
+
local driver = {};
local driver_mt = { __index = driver };
-function driver:open(store)
- return setmetatable({ store = store }, driver_mt);
+function driver:open(store, typ)
+ if typ and typ ~= "keyval" and typ ~= "archive" then
+ return nil, "unsupported-store";
+ end
+ return setmetatable({ store = store, type = typ }, driver_mt);
end
function driver:get(user)
return {};
@@ -20,4 +25,16 @@ function driver:purge(user)
return true;
end
+function driver:append()
+ return nil, "Storage disabled";
+end
+
+function driver:find()
+ return function () end, 0;
+end
+
+function driver:delete()
+ return true;
+end
+
module:provides("storage", driver);
diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua
index eed3fec9..13c961f8 100644
--- a/plugins/mod_storage_sql.lua
+++ b/plugins/mod_storage_sql.lua
@@ -1,187 +1,39 @@
---[[
-
-DB Tables:
- Prosody - key-value, map
- | host | user | store | key | type | value |
- ProsodyArchive - list
- | host | user | store | key | time | stanzatype | jsonvalue |
-
-Mapping:
- Roster - Prosody
- | host | user | "roster" | "contactjid" | type | value |
- | host | user | "roster" | NULL | "json" | roster[false] data |
- Account - Prosody
- | host | user | "accounts" | "username" | type | value |
-
- Offline - ProsodyArchive
- | host | user | "offline" | "contactjid" | time | "message" | json|XML |
-
-]]
-
-local type = type;
-local tostring = tostring;
-local tonumber = tonumber;
-local pairs = pairs;
-local next = next;
-local setmetatable = setmetatable;
-local xpcall = xpcall;
-local json = require "util.json";
-local build_url = require"socket.url".build;
-
-local DBI;
-local connection;
-local host,user,store = module.host;
-local params = module:get_option("sql");
-
-local dburi;
-local connections = module:shared "/*/sql/connection-cache";
-
-local function db2uri(params)
- return build_url{
- scheme = params.driver,
- user = params.username,
- password = params.password,
- host = params.host,
- port = params.port,
- path = params.database,
- };
-end
+-- luacheck: ignore 212/self
+local json = require "util.json";
+local sql = require "util.sql";
+local xml_parse = require "util.xml".parse;
+local uuid = require "util.uuid";
+local resolve_relative_path = require "util.paths".resolve_relative_path;
-local resolve_relative_path = require "core.configmanager".resolve_relative_path;
+local is_stanza = require"util.stanza".is_stanza;
+local t_concat = table.concat;
-local function test_connection()
- if not connection then return nil; end
- if connection:ping() then
- return true;
- else
- module:log("debug", "Database connection closed");
- connection = nil;
- connections[dburi] = nil;
- end
-end
-local function connect()
- if not test_connection() then
- prosody.unlock_globals();
- local dbh, err = DBI.Connect(
- params.driver, params.database,
- params.username, params.password,
- params.host, params.port
- );
- prosody.lock_globals();
- if not dbh then
- module:log("debug", "Database connection failed: %s", tostring(err));
- return nil, err;
+local noop = function() end
+local unpack = unpack
+local function iterator(result)
+ return function(result_)
+ local row = result_();
+ if row ~= nil then
+ return unpack(row);
end
- module:log("debug", "Successfully connected to database");
- dbh:autocommit(false); -- don't commit automatically
- connection = dbh;
-
- connections[dburi] = dbh;
- end
- return connection;
+ end, result, nil;
end
-local function create_table()
- if not module:get_option("sql_manage_tables", true) then
- return;
- end
- local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);";
- if params.driver == "PostgreSQL" then
- create_sql = create_sql:gsub("`", "\"");
- elseif params.driver == "MySQL" then
- create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT");
- end
-
- local stmt, err = connection:prepare(create_sql);
- if stmt then
- local ok = stmt:execute();
- local commit_ok = connection:commit();
- if ok and commit_ok then
- module:log("info", "Initialized new %s database with prosody table", params.driver);
- local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)";
- if params.driver == "PostgreSQL" then
- index_sql = index_sql:gsub("`", "\"");
- elseif params.driver == "MySQL" then
- index_sql = index_sql:gsub("`([,)])", "`(20)%1");
- end
- local stmt, err = connection:prepare(index_sql);
- local ok, commit_ok, commit_err;
- if stmt then
- ok, err = stmt:execute();
- commit_ok, commit_err = connection:commit();
- end
- if not(ok and commit_ok) then
- module:log("warn", "Failed to create index (%s), lookups may not be optimised", err or commit_err);
- end
- elseif params.driver == "MySQL" then -- COMPAT: Upgrade tables from 0.8.0
- -- Failed to create, but check existing MySQL table here
- local stmt = connection:prepare("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'");
- local ok = stmt:execute();
- local commit_ok = connection:commit();
- if ok and commit_ok then
- if stmt:rowcount() > 0 then
- module:log("info", "Upgrading database schema...");
- local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT");
- local ok, err = stmt:execute();
- local commit_ok = connection:commit();
- if ok and commit_ok then
- module:log("info", "Database table automatically upgraded");
- else
- module:log("error", "Failed to upgrade database schema (%s), please see "
- .."http://prosody.im/doc/mysql for help",
- err or "unknown error");
- end
- end
- repeat until not stmt:fetch();
- end
- end
- elseif params.driver ~= "SQLite3" then -- SQLite normally fails to prepare for existing table
- module:log("warn", "Prosody was not able to automatically check/create the database table (%s), "
- .."see http://prosody.im/doc/modules/mod_storage_sql#table_management for help.",
- err or "unknown error");
- end
-end
-
-do -- process options to get a db connection
- local ok;
- prosody.unlock_globals();
- ok, DBI = pcall(require, "DBI");
- if not ok then
- package.loaded["DBI"] = {};
- module:log("error", "Failed to load the LuaDBI library for accessing SQL databases: %s", DBI);
- module:log("error", "More information on installing LuaDBI can be found at http://prosody.im/doc/depends#luadbi");
- end
- prosody.lock_globals();
- if not ok or not DBI.Connect then
- return; -- Halt loading of this module
- end
+local default_params = { driver = "SQLite3" };
- params = params or { driver = "SQLite3" };
-
- if params.driver == "SQLite3" then
- params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
- end
-
- assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");
-
- dburi = db2uri(params);
- connection = connections[dburi];
-
- assert(connect());
-
- -- Automatically create table, ignore failure (table probably already exists)
- create_table();
-end
+local engine;
local function serialize(value)
local t = type(value);
if t == "string" or t == "boolean" or t == "number" then
return t, tostring(value);
+ elseif is_stanza(value) then
+ return "xml", tostring(value);
elseif t == "table" then
- local value,err = json.encode(value);
- if value then return "json", value; end
+ local encoded,err = json.encode(value);
+ if encoded then return "json", encoded; end
return nil, err;
end
return nil, "Unhandled value type: "..t;
@@ -194,55 +46,26 @@ local function deserialize(t, value)
elseif t == "number" then return tonumber(value);
elseif t == "json" then
return json.decode(value);
+ elseif t == "xml" then
+ return xml_parse(value);
end
end
-local function dosql(sql, ...)
- if params.driver == "PostgreSQL" then
- sql = sql:gsub("`", "\"");
- end
- -- do prepared statement stuff
- local stmt, err = connection:prepare(sql);
- if not stmt and not test_connection() then error("connection failed"); end
- if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
- -- run query
- local ok, err = stmt:execute(...);
- if not ok and not test_connection() then error("connection failed"); end
- if not ok then return nil, err; end
-
- return stmt;
-end
-local function getsql(sql, ...)
- return dosql(sql, host or "", user or "", store or "", ...);
-end
-local function setsql(sql, ...)
- local stmt, err = getsql(sql, ...);
- if not stmt then return stmt, err; end
- return stmt:affected();
-end
-local function transact(...)
- -- ...
-end
-local function rollback(...)
- if connection then connection:rollback(); end -- FIXME check for rollback error?
- return ...;
-end
-local function commit(...)
- local success,err = connection:commit();
- if not success then return nil, "SQL commit failed: "..tostring(err); end
- return ...;
-end
+local host = module.host;
+local user, store;
local function keyval_store_get()
- local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
- if not stmt then return rollback(nil, err); end
-
local haveany;
local result = {};
- for row in stmt:rows(true) do
+ local select_sql = [[
+ SELECT "key","type","value"
+ FROM "prosody"
+ WHERE "host"=? AND "user"=? AND "store"=?;
+ ]]
+ for row in engine:select(select_sql, host, user or "", store) do
haveany = true;
- local k = row.key;
- local v = deserialize(row.type, row.value);
+ local k = row[1];
+ local v = deserialize(row[2], row[3]);
if k and v then
if k ~= "" then result[k] = v; elseif type(v) == "table" then
for a,b in pairs(v) do
@@ -251,164 +74,531 @@ local function keyval_store_get()
end
end
end
- return commit(haveany and result or nil);
+ if haveany then
+ return result;
+ end
end
local function keyval_store_set(data)
- local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
- if not affected then return rollback(affected, err); end
-
+ local delete_sql = [[
+ DELETE FROM "prosody"
+ WHERE "host"=? AND "user"=? AND "store"=?
+ ]];
+ engine:delete(delete_sql, host, user or "", store);
+
+ local insert_sql = [[
+ INSERT INTO "prosody"
+ ("host","user","store","key","type","value")
+ VALUES (?,?,?,?,?,?);
+ ]]
if data and next(data) ~= nil then
local extradata = {};
for key, value in pairs(data) do
if type(key) == "string" and key ~= "" then
- local t, value = serialize(value);
- if not t then return rollback(t, value); end
- local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value);
- if not ok then return rollback(ok, err); end
+ local t, encoded_value = assert(serialize(value));
+ engine:insert(insert_sql, host, user or "", store, key, t, encoded_value);
else
extradata[key] = value;
end
end
if next(extradata) ~= nil then
- local t, extradata = serialize(extradata);
- if not t then return rollback(t, extradata); end
- local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", "", t, extradata);
- if not ok then return rollback(ok, err); end
+ local t, encoded_extradata = assert(serialize(extradata));
+ engine:insert(insert_sql, host, user or "", store, "", t, encoded_extradata);
end
end
- return commit(true);
+ return true;
end
+--- Key/value store API (default store type)
+
local keyval_store = {};
keyval_store.__index = keyval_store;
function keyval_store:get(username)
- user,store = username,self.store;
- if not connection and not connect() then return nil, "Unable to connect to database"; end
- local success, ret, err = xpcall(keyval_store_get, debug.traceback);
- if not connection and connect() then
- success, ret, err = xpcall(keyval_store_get, debug.traceback);
+ user, store = username, self.store;
+ local ok, result = engine:transaction(keyval_store_get);
+ if not ok then
+ module:log("error", "Unable to read from database %s store for %s: %s", store, username or "<host>", result);
+ return nil, result;
end
- if success then return ret, err; else return rollback(nil, ret); end
+ return result;
end
function keyval_store:set(username, data)
user,store = username,self.store;
- if not connection and not connect() then return nil, "Unable to connect to database"; end
- local success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback);
- if not connection and connect() then
- success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback);
- end
- if success then return ret, err; else return rollback(nil, ret); end
+ return engine:transaction(function()
+ return keyval_store_set(data);
+ end);
end
function keyval_store:users()
- local stmt, err = dosql("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store);
- if not stmt then
- return rollback(nil, err);
- end
- local next = stmt:rows();
- return commit(function()
- local row = next();
- return row and row[1];
+ local ok, result = engine:transaction(function()
+ local select_sql = [[
+ SELECT DISTINCT "user"
+ FROM "prosody"
+ WHERE "host"=? AND "store"=?;
+ ]];
+ return engine:select(select_sql, host, self.store);
end);
+ if not ok then return ok, result end
+ return iterator(result);
end
-local function map_store_get(key)
- local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
- if not stmt then return rollback(nil, err); end
-
- local haveany;
- local result = {};
- for row in stmt:rows(true) do
- haveany = true;
- local k = row.key;
- local v = deserialize(row.type, row.value);
- if k and v then
- if k ~= "" then result[k] = v; elseif type(v) == "table" then
- for a,b in pairs(v) do
- result[a] = b;
+--- Archive store API
+
+-- luacheck: ignore 512 431/user 431/store
+local map_store = {};
+map_store.__index = map_store;
+map_store.remove = {};
+function map_store:get(username, key)
+ local ok, result = engine:transaction(function()
+ local query = [[
+ SELECT "type", "value"
+ FROM "prosody"
+ WHERE "host"=? AND "user"=? AND "store"=? AND "key"=?
+ LIMIT 1
+ ]];
+ local data;
+ if type(key) == "string" and key ~= "" then
+ for row in engine:select(query, host, username or "", self.store, key) do
+ data = deserialize(row[1], row[2]);
+ end
+ return data;
+ else
+ for row in engine:select(query, host, username or "", self.store, "") do
+ data = deserialize(row[1], row[2]);
+ end
+ return data and data[key] or nil;
+ end
+ end);
+ if not ok then return nil, result; end
+ return result;
+end
+function map_store:set(username, key, data)
+ if data == nil then data = self.remove; end
+ return self:set_keys(username, { [key] = data });
+end
+function map_store:set_keys(username, keydatas)
+ local ok, result = engine:transaction(function()
+ local delete_sql = [[
+ DELETE FROM "prosody"
+ WHERE "host"=? AND "user"=? AND "store"=? AND "key"=?;
+ ]];
+ local insert_sql = [[
+ INSERT INTO "prosody"
+ ("host","user","store","key","type","value")
+ VALUES (?,?,?,?,?,?);
+ ]];
+ local select_extradata_sql = [[
+ SELECT "type", "value"
+ FROM "prosody"
+ WHERE "host"=? AND "user"=? AND "store"=? AND "key"=?
+ LIMIT 1;
+ ]];
+ for key, data in pairs(keydatas) do
+ if type(key) == "string" and key ~= "" then
+ engine:delete(delete_sql,
+ host, username or "", self.store, key);
+ if data ~= self.remove then
+ local t, value = assert(serialize(data));
+ engine:insert(insert_sql, host, username or "", self.store, key, t, value);
+ end
+ else
+ local extradata = {};
+ for row in engine:select(select_extradata_sql, host, username or "", self.store, "") do
+ extradata = deserialize(row[1], row[2]);
end
+ engine:delete(delete_sql, host, username or "", self.store, "");
+ extradata[key] = data;
+ local t, value = assert(serialize(extradata));
+ engine:insert(insert_sql, host, username or "", self.store, "", t, value);
end
end
- end
- return commit(haveany and result[key] or nil);
+ return true;
+ end);
+ if not ok then return nil, result; end
+ return result;
end
-local function map_store_set(key, data)
- local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
- if not affected then return rollback(affected, err); end
-
- if data and next(data) ~= nil then
- if type(key) == "string" and key ~= "" then
- local t, value = serialize(data);
- if not t then return rollback(t, value); end
- local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value);
- if not ok then return rollback(ok, err); end
+
+local archive_store = {}
+archive_store.caps = {
+ total = true;
+};
+archive_store.__index = archive_store
+function archive_store:append(username, key, value, when, with)
+ local user,store = username,self.store;
+ when = when or os.time();
+ with = with or "";
+ local ok, ret = engine:transaction(function()
+ local delete_sql = [[
+ DELETE FROM "prosodyarchive"
+ WHERE "host"=? AND "user"=? AND "store"=? AND "key"=?;
+ ]];
+ local insert_sql = [[
+ INSERT INTO "prosodyarchive"
+ ("host", "user", "store", "when", "with", "key", "type", "value")
+ VALUES (?,?,?,?,?,?,?,?);
+ ]];
+ if key then
+ engine:delete(delete_sql, host, user or "", store, key);
else
- -- TODO non-string keys
+ key = uuid.generate();
end
- end
- return commit(true);
+ local t, encoded_value = assert(serialize(value));
+ engine:insert(insert_sql, host, user or "", store, when, with, key, t, encoded_value);
+ return key;
+ end);
+ if not ok then return ok, ret; end
+ return ret; -- the key
end
-local map_store = {};
-map_store.__index = map_store;
-function map_store:get(username, key)
- user,store = username,self.store;
- local success, ret, err = xpcall(function() return map_store_get(key); end, debug.traceback);
- if success then return ret, err; else return rollback(nil, ret); end
+-- Helpers for building the WHERE clause
+local function archive_where(query, args, where)
+ -- Time range, inclusive
+ if query.start then
+ args[#args+1] = query.start
+ where[#where+1] = "\"when\" >= ?"
+ end
+
+ if query["end"] then
+ args[#args+1] = query["end"];
+ if query.start then
+ where[#where] = "\"when\" BETWEEN ? AND ?" -- is this inclusive?
+ else
+ where[#where+1] = "\"when\" <= ?"
+ end
+ end
+
+ -- Related name
+ if query.with then
+ where[#where+1] = "\"with\" = ?";
+ args[#args+1] = query.with
+ end
+
+ -- Unique id
+ if query.key then
+ where[#where+1] = "\"key\" = ?";
+ args[#args+1] = query.key
+ end
end
-function map_store:set(username, key, data)
- user,store = username,self.store;
- local success, ret, err = xpcall(function() return map_store_set(key, data); end, debug.traceback);
- if success then return ret, err; else return rollback(nil, ret); end
+local function archive_where_id_range(query, args, where)
+ local args_len = #args
+ -- Before or after specific item, exclusive
+ if query.after then -- keys better be unique!
+ where[#where+1] = [[
+ "sort_id" > COALESCE(
+ (
+ SELECT "sort_id"
+ FROM "prosodyarchive"
+ WHERE "key" = ? AND "host" = ? AND "user" = ? AND "store" = ?
+ LIMIT 1
+ ), 0)
+ ]];
+ args[args_len+1], args[args_len+2], args[args_len+3], args[args_len+4] = query.after, args[1], args[2], args[3];
+ args_len = args_len + 4
+ end
+ if query.before then
+ where[#where+1] = [[
+ "sort_id" < COALESCE(
+ (
+ SELECT "sort_id"
+ FROM "prosodyarchive"
+ WHERE "key" = ? AND "host" = ? AND "user" = ? AND "store" = ?
+ LIMIT 1
+ ),
+ (
+ SELECT MAX("sort_id")+1
+ FROM "prosodyarchive"
+ )
+ )
+ ]]
+ args[args_len+1], args[args_len+2], args[args_len+3], args[args_len+4] = query.before, args[1], args[2], args[3];
+ end
end
-local list_store = {};
-list_store.__index = list_store;
-function list_store:scan(username, from, to, jid, typ)
- user,store = username,self.store;
-
- local cols = {"from", "to", "jid", "typ"};
- local vals = { from , to , jid , typ };
- local stmt, err;
- local query = "SELECT * FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=?";
-
- query = query.." ORDER BY time";
- --local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
-
- return nil, "not-implemented"
+function archive_store:find(username, query)
+ query = query or {};
+ local user,store = username,self.store;
+ local total;
+ local ok, result = engine:transaction(function()
+ local sql_query = [[
+ SELECT "key", "type", "value", "when", "with"
+ FROM "prosodyarchive"
+ WHERE %s
+ ORDER BY "sort_id" %s%s;
+ ]];
+ local args = { host, user or "", store, };
+ local where = { "\"host\" = ?", "\"user\" = ?", "\"store\" = ?", };
+
+ archive_where(query, args, where);
+
+ -- Total matching
+ if query.total then
+ local stats = engine:select("SELECT COUNT(*) FROM \"prosodyarchive\" WHERE "
+ .. t_concat(where, " AND "), unpack(args));
+ if stats then
+ for row in stats do
+ total = row[1];
+ end
+ end
+ if query.limit == 0 then -- Skip the real query
+ return noop, total;
+ end
+ end
+
+ archive_where_id_range(query, args, where);
+
+ if query.limit then
+ args[#args+1] = query.limit;
+ end
+
+ sql_query = sql_query:format(t_concat(where, " AND "), query.reverse
+ and "DESC" or "ASC", query.limit and " LIMIT ?" or "");
+ return engine:select(sql_query, unpack(args));
+ end);
+ if not ok then return ok, result end
+ return function()
+ local row = result();
+ if row ~= nil then
+ return row[1], deserialize(row[2], row[3]), row[4], row[5];
+ end
+ end, total;
end
+function archive_store:delete(username, query)
+ query = query or {};
+ local user,store = username,self.store;
+ local ok, stmt = engine:transaction(function()
+ local sql_query = "DELETE FROM \"prosodyarchive\" WHERE %s;";
+ local args = { host, user or "", store, };
+ local where = { "\"host\" = ?", "\"user\" = ?", "\"store\" = ?", };
+ if user == true then
+ table.remove(args, 2);
+ table.remove(where, 2);
+ end
+ archive_where(query, args, where);
+ archive_where_id_range(query, args, where);
+ sql_query = sql_query:format(t_concat(where, " AND "));
+ return engine:delete(sql_query, unpack(args));
+ end);
+ return ok and stmt:affected(), stmt;
+end
+
+local stores = {
+ keyval = keyval_store;
+ map = map_store;
+ archive = archive_store;
+};
+
+--- Implement storage driver API
+
+-- FIXME: Some of these operations need to operate on the archive store(s) too
+
local driver = {};
function driver:open(store, typ)
- if not typ then -- default key-value store
- return setmetatable({ store = store }, keyval_store);
+ local store_mt = stores[typ or "keyval"];
+ if store_mt then
+ return setmetatable({ store = store }, store_mt);
end
return nil, "unsupported-store";
end
function driver:stores(username)
- local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" ..
+ local query = "SELECT DISTINCT \"store\" FROM \"prosody\" WHERE \"host\"=? AND \"user\"" ..
(username == true and "!=?" or "=?");
if username == true or not username then
username = "";
end
- local stmt, err = dosql(sql, host, username);
- if not stmt then
- return rollback(nil, err);
- end
- local next = stmt:rows();
- return commit(function()
- local row = next();
- return row and row[1];
+ local ok, result = engine:transaction(function()
+ return engine:select(query, host, username);
end);
+ if not ok then return ok, result end
+ return iterator(result);
end
function driver:purge(username)
- local stmt, err = dosql("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username);
- if not stmt then return rollback(stmt, err); end
- local changed, err = stmt:affected();
- if not changed then return rollback(changed, err); end
- return commit(true, changed);
+ return engine:transaction(function()
+ engine:delete("DELETE FROM \"prosody\" WHERE \"host\"=? AND \"user\"=?", host, username);
+ engine:delete("DELETE FROM \"prosodyarchive\" WHERE \"host\"=? AND \"user\"=?", host, username);
+ end);
end
-module:provides("storage", driver);
+--- Initialization
+
+
+local function create_table(engine, name) -- luacheck: ignore 431/engine
+ local Table, Column, Index = sql.Table, sql.Column, sql.Index;
+
+ local ProsodyTable = Table {
+ name= name or "prosody";
+ Column { name="host", type="TEXT", nullable=false };
+ Column { name="user", type="TEXT", nullable=false };
+ Column { name="store", type="TEXT", nullable=false };
+ Column { name="key", type="TEXT", nullable=false };
+ Column { name="type", type="TEXT", nullable=false };
+ Column { name="value", type="MEDIUMTEXT", nullable=false };
+ Index { name="prosody_index", "host", "user", "store", "key" };
+ };
+ engine:transaction(function()
+ ProsodyTable:create(engine);
+ end);
+
+ local ProsodyArchiveTable = Table {
+ name="prosodyarchive";
+ Column { name="sort_id", type="INTEGER", primary_key=true, auto_increment=true };
+ Column { name="host", type="TEXT", nullable=false };
+ Column { name="user", type="TEXT", nullable=false };
+ Column { name="store", type="TEXT", nullable=false };
+ Column { name="key", type="TEXT", nullable=false }; -- item id
+ Column { name="when", type="INTEGER", nullable=false }; -- timestamp
+ Column { name="with", type="TEXT", nullable=false }; -- related id
+ Column { name="type", type="TEXT", nullable=false };
+ Column { name="value", type="MEDIUMTEXT", nullable=false };
+ Index { name="prosodyarchive_index", unique = true, "host", "user", "store", "key" };
+ Index { name="prosodyarchive_with_when", "host", "user", "store", "with", "when" };
+ Index { name="prosodyarchive_when", "host", "user", "store", "when" };
+ };
+ engine:transaction(function()
+ ProsodyArchiveTable:create(engine);
+ end);
+end
+
+local function upgrade_table(engine, params, apply_changes) -- luacheck: ignore 431/engine
+ local changes = false;
+ if params.driver == "MySQL" then
+ local success,err = engine:transaction(function()
+ local result = engine:execute("SHOW COLUMNS FROM \"prosody\" WHERE \"Field\"='value' and \"Type\"='text'");
+ if result:rowcount() > 0 then
+ changes = true;
+ if apply_changes then
+ module:log("info", "Upgrading database schema...");
+ engine:execute("ALTER TABLE \"prosody\" MODIFY COLUMN \"value\" MEDIUMTEXT");
+ module:log("info", "Database table automatically upgraded");
+ end
+ end
+ return true;
+ end);
+ if not success then
+ module:log("error", "Failed to check/upgrade database schema (%s), please see "
+ .."http://prosody.im/doc/mysql for help",
+ err or "unknown error");
+ return false;
+ end
+
+ -- COMPAT w/pre-0.10: Upgrade table to UTF-8 if not already
+ local check_encoding_query = [[
+ SELECT "COLUMN_NAME","COLUMN_TYPE","TABLE_NAME"
+ FROM "information_schema"."columns"
+ WHERE "TABLE_NAME" LIKE 'prosody%%'
+ AND "TABLE_SCHEMA" = ?
+ AND ( "CHARACTER_SET_NAME"!=? OR "COLLATION_NAME"!=?);
+ ]];
+ -- FIXME Is it ok to ignore the return values from this?
+ engine:transaction(function()
+ local result = assert(engine:execute(check_encoding_query, params.database, engine.charset, engine.charset.."_bin"));
+ local n_bad_columns = result:rowcount();
+ if n_bad_columns > 0 then
+ changes = true;
+ if apply_changes then
+ module:log("warn", "Found %d columns in prosody table requiring encoding change, updating now...", n_bad_columns);
+ local fix_column_query1 = "ALTER TABLE \"%s\" CHANGE \"%s\" \"%s\" BLOB;";
+ local fix_column_query2 = "ALTER TABLE \"%s\" CHANGE \"%s\" \"%s\" %s CHARACTER SET '%s' COLLATE '%s_bin';";
+ for row in result:rows() do
+ local column_name, column_type, table_name = unpack(row);
+ module:log("debug", "Fixing column %s in table %s", column_name, table_name);
+ engine:execute(fix_column_query1:format(table_name, column_name, column_name));
+ engine:execute(fix_column_query2:format(table_name, column_name, column_name, column_type, engine.charset, engine.charset));
+ end
+ module:log("info", "Database encoding upgrade complete!");
+ end
+ end
+ end);
+ success,err = engine:transaction(function()
+ return engine:execute(check_encoding_query, params.database,
+ engine.charset, engine.charset.."_bin");
+ end);
+ if not success then
+ module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error");
+ return false;
+ end
+ end
+ return changes;
+end
+
+local function normalize_database(driver, database) -- luacheck: ignore 431/driver
+ if driver == "SQLite3" and database ~= ":memory:" then
+ return resolve_relative_path(prosody.paths.data or ".", database or "prosody.sqlite");
+ end
+ return database;
+end
+
+local function normalize_params(params)
+ return {
+ driver = assert(params.driver,
+ "Configuration error: Both the SQL driver and the database need to be specified");
+ database = assert(normalize_database(params.driver, params.database),
+ "Configuration error: Both the SQL driver and the database need to be specified");
+ username = params.username;
+ password = params.password;
+ host = params.host;
+ port = params.port;
+ };
+end
+
+function module.load()
+ if prosody.prosodyctl then return; end
+ local engines = module:shared("/*/sql/connections");
+ local params = normalize_params(module:get_option("sql", default_params));
+ engine = engines[sql.db2uri(params)];
+ if not engine then
+ module:log("debug", "Creating new engine");
+ engine = sql:create_engine(params, function (engine) -- luacheck: ignore 431/engine
+ if module:get_option("sql_manage_tables", true) then
+ -- Automatically create table, ignore failure (table probably already exists)
+ -- FIXME: we should check in information_schema, etc.
+ create_table(engine);
+ -- Check whether the table needs upgrading
+ if upgrade_table(engine, params, false) then
+ module:log("error", "Old database format detected. Please run: prosodyctl mod_%s upgrade", module.name);
+ return false, "database upgrade needed";
+ end
+ end
+ end);
+ engines[sql.db2uri(params)] = engine;
+ end
+
+ module:provides("storage", driver);
+end
+
+function module.command(arg)
+ local config = require "core.configmanager";
+ local prosodyctl = require "util.prosodyctl";
+ local command = table.remove(arg, 1);
+ if command == "upgrade" then
+ -- We need to find every unique dburi in the config
+ local uris = {};
+ for host in pairs(prosody.hosts) do -- luacheck: ignore 431/host
+ local params = normalize_params(config.get(host, "sql") or default_params);
+ uris[sql.db2uri(params)] = params;
+ end
+ print("We will check and upgrade the following databases:\n");
+ for _, params in pairs(uris) do
+ print("", "["..params.driver.."] "..params.database..(params.host and " on "..params.host or ""));
+ end
+ print("");
+ print("Ensure you have working backups of the above databases before continuing! ");
+ if not prosodyctl.show_yesno("Continue with the database upgrade? [yN]") then
+ print("Ok, no upgrade. But you do have backups, don't you? ...don't you?? :-)");
+ return;
+ end
+ -- Upgrade each one
+ for _, params in pairs(uris) do
+ print("Checking "..params.database.."...");
+ engine = sql:create_engine(params);
+ upgrade_table(engine, params, true);
+ end
+ print("All done!");
+ elseif command then
+ print("Unknown command: "..command);
+ else
+ print("Available commands:");
+ print("","upgrade - Perform database upgrade");
+ end
+end
diff --git a/plugins/mod_storage_sql1.lua b/plugins/mod_storage_sql1.lua
new file mode 100644
index 00000000..a5bb5bfa
--- /dev/null
+++ b/plugins/mod_storage_sql1.lua
@@ -0,0 +1,414 @@
+
+--[[
+
+DB Tables:
+ Prosody - key-value, map
+ | host | user | store | key | type | value |
+ ProsodyArchive - list
+ | host | user | store | key | time | stanzatype | jsonvalue |
+
+Mapping:
+ Roster - Prosody
+ | host | user | "roster" | "contactjid" | type | value |
+ | host | user | "roster" | NULL | "json" | roster[false] data |
+ Account - Prosody
+ | host | user | "accounts" | "username" | type | value |
+
+ Offline - ProsodyArchive
+ | host | user | "offline" | "contactjid" | time | "message" | json|XML |
+
+]]
+
+local type = type;
+local tostring = tostring;
+local tonumber = tonumber;
+local pairs = pairs;
+local next = next;
+local setmetatable = setmetatable;
+local xpcall = xpcall;
+local json = require "util.json";
+local build_url = require"socket.url".build;
+
+local DBI;
+local connection;
+local host,user,store = module.host;
+local params = module:get_option("sql");
+
+local dburi;
+local connections = module:shared "/*/sql/connection-cache";
+
+local function db2uri(params)
+ return build_url{
+ scheme = params.driver,
+ user = params.username,
+ password = params.password,
+ host = params.host,
+ port = params.port,
+ path = params.database,
+ };
+end
+
+
+local resolve_relative_path = require "util.paths".resolve_relative_path;
+
+local function test_connection()
+ if not connection then return nil; end
+ if connection:ping() then
+ return true;
+ else
+ module:log("debug", "Database connection closed");
+ connection = nil;
+ connections[dburi] = nil;
+ end
+end
+local function connect()
+ if not test_connection() then
+ prosody.unlock_globals();
+ local dbh, err = DBI.Connect(
+ params.driver, params.database,
+ params.username, params.password,
+ params.host, params.port
+ );
+ prosody.lock_globals();
+ if not dbh then
+ module:log("debug", "Database connection failed: %s", tostring(err));
+ return nil, err;
+ end
+ module:log("debug", "Successfully connected to database");
+ dbh:autocommit(false); -- don't commit automatically
+ connection = dbh;
+
+ connections[dburi] = dbh;
+ end
+ return connection;
+end
+
+local function create_table()
+ if not module:get_option("sql_manage_tables", true) then
+ return;
+ end
+ local create_sql = "CREATE TABLE `prosody` (`host` TEXT, `user` TEXT, `store` TEXT, `key` TEXT, `type` TEXT, `value` TEXT);";
+ if params.driver == "PostgreSQL" then
+ create_sql = create_sql:gsub("`", "\"");
+ elseif params.driver == "MySQL" then
+ create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT");
+ end
+
+ local stmt, err = connection:prepare(create_sql);
+ if stmt then
+ local ok = stmt:execute();
+ local commit_ok = connection:commit();
+ if ok and commit_ok then
+ module:log("info", "Initialized new %s database with prosody table", params.driver);
+ local index_sql = "CREATE INDEX `prosody_index` ON `prosody` (`host`, `user`, `store`, `key`)";
+ if params.driver == "PostgreSQL" then
+ index_sql = index_sql:gsub("`", "\"");
+ elseif params.driver == "MySQL" then
+ index_sql = index_sql:gsub("`([,)])", "`(20)%1");
+ end
+ local stmt, err = connection:prepare(index_sql);
+ local ok, commit_ok, commit_err;
+ if stmt then
+ ok, err = stmt:execute();
+ commit_ok, commit_err = connection:commit();
+ end
+ if not(ok and commit_ok) then
+ module:log("warn", "Failed to create index (%s), lookups may not be optimised", err or commit_err);
+ end
+ elseif params.driver == "MySQL" then -- COMPAT: Upgrade tables from 0.8.0
+ -- Failed to create, but check existing MySQL table here
+ local stmt = connection:prepare("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'");
+ local ok = stmt:execute();
+ local commit_ok = connection:commit();
+ if ok and commit_ok then
+ if stmt:rowcount() > 0 then
+ module:log("info", "Upgrading database schema...");
+ local stmt = connection:prepare("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT");
+ local ok, err = stmt:execute();
+ local commit_ok = connection:commit();
+ if ok and commit_ok then
+ module:log("info", "Database table automatically upgraded");
+ else
+ module:log("error", "Failed to upgrade database schema (%s), please see "
+ .."http://prosody.im/doc/mysql for help",
+ err or "unknown error");
+ end
+ end
+ repeat until not stmt:fetch();
+ end
+ end
+ elseif params.driver ~= "SQLite3" then -- SQLite normally fails to prepare for existing table
+ module:log("warn", "Prosody was not able to automatically check/create the database table (%s), "
+ .."see http://prosody.im/doc/modules/mod_storage_sql#table_management for help.",
+ err or "unknown error");
+ end
+end
+
+do -- process options to get a db connection
+ local ok;
+ prosody.unlock_globals();
+ ok, DBI = pcall(require, "DBI");
+ if not ok then
+ package.loaded["DBI"] = {};
+ module:log("error", "Failed to load the LuaDBI library for accessing SQL databases: %s", DBI);
+ module:log("error", "More information on installing LuaDBI can be found at http://prosody.im/doc/depends#luadbi");
+ end
+ prosody.lock_globals();
+ if not ok or not DBI.Connect then
+ return; -- Halt loading of this module
+ end
+
+ params = params or { driver = "SQLite3" };
+
+ if params.driver == "SQLite3" then
+ params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
+ end
+
+ assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");
+
+ dburi = db2uri(params);
+ connection = connections[dburi];
+
+ assert(connect());
+
+ -- Automatically create table, ignore failure (table probably already exists)
+ create_table();
+end
+
+local function serialize(value)
+ local t = type(value);
+ if t == "string" or t == "boolean" or t == "number" then
+ return t, tostring(value);
+ elseif t == "table" then
+ local value,err = json.encode(value);
+ if value then return "json", value; end
+ return nil, err;
+ end
+ return nil, "Unhandled value type: "..t;
+end
+local function deserialize(t, value)
+ if t == "string" then return value;
+ elseif t == "boolean" then
+ if value == "true" then return true;
+ elseif value == "false" then return false; end
+ elseif t == "number" then return tonumber(value);
+ elseif t == "json" then
+ return json.decode(value);
+ end
+end
+
+local function dosql(sql, ...)
+ if params.driver == "PostgreSQL" then
+ sql = sql:gsub("`", "\"");
+ end
+ -- do prepared statement stuff
+ local stmt, err = connection:prepare(sql);
+ if not stmt and not test_connection() then error("connection failed"); end
+ if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
+ -- run query
+ local ok, err = stmt:execute(...);
+ if not ok and not test_connection() then error("connection failed"); end
+ if not ok then return nil, err; end
+
+ return stmt;
+end
+local function getsql(sql, ...)
+ return dosql(sql, host or "", user or "", store or "", ...);
+end
+local function setsql(sql, ...)
+ local stmt, err = getsql(sql, ...);
+ if not stmt then return stmt, err; end
+ return stmt:affected();
+end
+local function transact(...)
+ -- ...
+end
+local function rollback(...)
+ if connection then connection:rollback(); end -- FIXME check for rollback error?
+ return ...;
+end
+local function commit(...)
+ local success,err = connection:commit();
+ if not success then return nil, "SQL commit failed: "..tostring(err); end
+ return ...;
+end
+
+local function keyval_store_get()
+ local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
+ if not stmt then return rollback(nil, err); end
+
+ local haveany;
+ local result = {};
+ for row in stmt:rows(true) do
+ haveany = true;
+ local k = row.key;
+ local v = deserialize(row.type, row.value);
+ if k and v then
+ if k ~= "" then result[k] = v; elseif type(v) == "table" then
+ for a,b in pairs(v) do
+ result[a] = b;
+ end
+ end
+ end
+ end
+ return commit(haveany and result or nil);
+end
+local function keyval_store_set(data)
+ local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
+ if not affected then return rollback(affected, err); end
+
+ if data and next(data) ~= nil then
+ local extradata = {};
+ for key, value in pairs(data) do
+ if type(key) == "string" and key ~= "" then
+ local t, value = serialize(value);
+ if not t then return rollback(t, value); end
+ local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value);
+ if not ok then return rollback(ok, err); end
+ else
+ extradata[key] = value;
+ end
+ end
+ if next(extradata) ~= nil then
+ local t, extradata = serialize(extradata);
+ if not t then return rollback(t, extradata); end
+ local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", "", t, extradata);
+ if not ok then return rollback(ok, err); end
+ end
+ end
+ return commit(true);
+end
+
+local keyval_store = {};
+keyval_store.__index = keyval_store;
+function keyval_store:get(username)
+ user,store = username,self.store;
+ if not connection and not connect() then return nil, "Unable to connect to database"; end
+ local success, ret, err = xpcall(keyval_store_get, debug.traceback);
+ if not connection and connect() then
+ success, ret, err = xpcall(keyval_store_get, debug.traceback);
+ end
+ if success then return ret, err; else return rollback(nil, ret); end
+end
+function keyval_store:set(username, data)
+ user,store = username,self.store;
+ if not connection and not connect() then return nil, "Unable to connect to database"; end
+ local success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback);
+ if not connection and connect() then
+ success, ret, err = xpcall(function() return keyval_store_set(data); end, debug.traceback);
+ end
+ if success then return ret, err; else return rollback(nil, ret); end
+end
+function keyval_store:users()
+ local stmt, err = dosql("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store);
+ if not stmt then
+ return rollback(nil, err);
+ end
+ local next = stmt:rows();
+ return commit(function()
+ local row = next();
+ return row and row[1];
+ end);
+end
+
+local function map_store_get(key)
+ local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
+ if not stmt then return rollback(nil, err); end
+
+ local haveany;
+ local result = {};
+ for row in stmt:rows(true) do
+ haveany = true;
+ local k = row.key;
+ local v = deserialize(row.type, row.value);
+ if k and v then
+ if k ~= "" then result[k] = v; elseif type(v) == "table" then
+ for a,b in pairs(v) do
+ result[a] = b;
+ end
+ end
+ end
+ end
+ return commit(haveany and result[key] or nil);
+end
+local function map_store_set(key, data)
+ local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
+ if not affected then return rollback(affected, err); end
+
+ if data and next(data) ~= nil then
+ if type(key) == "string" and key ~= "" then
+ local t, value = serialize(data);
+ if not t then return rollback(t, value); end
+ local ok, err = setsql("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", key, t, value);
+ if not ok then return rollback(ok, err); end
+ else
+ -- TODO non-string keys
+ end
+ end
+ return commit(true);
+end
+
+local map_store = {};
+map_store.__index = map_store;
+function map_store:get(username, key)
+ user,store = username,self.store;
+ local success, ret, err = xpcall(function() return map_store_get(key); end, debug.traceback);
+ if success then return ret, err; else return rollback(nil, ret); end
+end
+function map_store:set(username, key, data)
+ user,store = username,self.store;
+ local success, ret, err = xpcall(function() return map_store_set(key, data); end, debug.traceback);
+ if success then return ret, err; else return rollback(nil, ret); end
+end
+
+local list_store = {};
+list_store.__index = list_store;
+function list_store:scan(username, from, to, jid, typ)
+ user,store = username,self.store;
+
+ local cols = {"from", "to", "jid", "typ"};
+ local vals = { from , to , jid , typ };
+ local stmt, err;
+ local query = "SELECT * FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=?";
+
+ query = query.." ORDER BY time";
+ --local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
+
+ return nil, "not-implemented"
+end
+
+local driver = {};
+
+function driver:open(store, typ)
+ if typ and typ ~= "keyval" then
+ return nil, "unsupported-store";
+ end
+ return setmetatable({ store = store }, keyval_store);
+end
+
+function driver:stores(username)
+ local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" ..
+ (username == true and "!=?" or "=?");
+ if username == true or not username then
+ username = "";
+ end
+ local stmt, err = dosql(sql, host, username);
+ if not stmt then
+ return rollback(nil, err);
+ end
+ local next = stmt:rows();
+ return commit(function()
+ local row = next();
+ return row and row[1];
+ end);
+end
+
+function driver:purge(username)
+ local stmt, err = dosql("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username);
+ if not stmt then return rollback(stmt, err); end
+ local changed, err = stmt:affected();
+ if not changed then return rollback(changed, err); end
+ return commit(true, changed);
+end
+
+module:provides("storage", driver);
diff --git a/plugins/storage/mod_xep0227.lua b/plugins/mod_storage_xep0227.lua
index 5d07a2ea..ef227ca3 100644
--- a/plugins/storage/mod_xep0227.lua
+++ b/plugins/mod_storage_xep0227.lua
@@ -7,28 +7,31 @@ local t_remove = table.remove;
local os_remove = os.remove;
local io_open = io.open;
+local paths = require"util.paths";
local st = require "util.stanza";
local parse_xml_real = require "util.xml".parse;
local function getXml(user, host)
local jid = user.."@"..host;
- local path = "data/"..jid..".xml";
+ local path = paths.join(prosody.paths.data, jid..".xml");
local f = io_open(path);
if not f then return; end
local s = f:read("*a");
+ f:close();
return parse_xml_real(s);
end
local function setXml(user, host, xml)
local jid = user.."@"..host;
- local path = "data/"..jid..".xml";
+ local path = paths.join(prosody.paths.data, jid..".xml");
+ local f, err = io_open(path, "w");
+ if not f then return f, err; end
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
+ f:close();
return os_remove(path);
end
end
@@ -44,7 +47,7 @@ local function getUserElement(xml)
end
end
local function createOuterXml(user, host)
- return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'})
+ return st.stanza("server-data", {xmlns='urn:xmpp:pie:0'})
:tag("host", {jid=host})
:tag("user", {name = user});
end
@@ -63,19 +66,36 @@ end
local handlers = {};
+-- In order to support mod_auth_internal_hashed
+local extended = "http://prosody.im/protocol/extended-xep0227\1";
+
handlers.accounts = {
get = function(self, user)
- local user = getUserElement(getXml(user, self.host));
+ user = getUserElement(getXml(user, self.host));
if user and user.attr.password then
return { password = user.attr.password };
+ elseif user then
+ local data = {};
+ for k, v in pairs(user.attr) do
+ if k:sub(1, #extended) == extended then
+ data[k:sub(#extended+1)] = v;
+ end
+ end
+ return data;
end
end;
set = function(self, user, data)
- if data and data.password then
+ if data 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;
+ for k, v in pairs(data) do
+ if k == "password" then
+ usere.attr.password = v;
+ else
+ usere.attr[extended..k] = v;
+ end
+ end
return setXml(user, self.host, xml);
else
return setXml(user, self.host, nil);
@@ -84,7 +104,7 @@ handlers.accounts = {
};
handlers.vcard = {
get = function(self, user)
- local user = getUserElement(getXml(user, self.host));
+ user = getUserElement(getXml(user, self.host));
if user then
local vcard = user:get_child("vCard", 'vcard-temp');
if vcard then
@@ -113,7 +133,7 @@ handlers.vcard = {
};
handlers.private = {
get = function(self, user)
- local user = getUserElement(getXml(user, self.host));
+ user = getUserElement(getXml(user, self.host));
if user then
local private = user:get_child("query", "jabber:iq:private");
if private then
@@ -147,15 +167,10 @@ handlers.private = {
-----------------------------
local driver = {};
-function driver:open(host, datastore, typ)
- local instance = setmetatable({}, self);
- instance.host = host;
- instance.datastore = datastore;
+function driver:open(datastore, typ)
local handler = handlers[datastore];
- if not handler then return nil; end
- for key,val in pairs(handler) do
- instance[key] = val;
- end
+ if not handler then return nil, "unsupported-datastore"; end
+ local instance = setmetatable({ host = module.host; datastore = datastore; }, { __index = handler });
if instance.init then instance:init(); end
return instance;
end
diff --git a/plugins/mod_time.lua b/plugins/mod_time.lua
index cb69ebe7..ae7da916 100644
--- a/plugins/mod_time.lua
+++ b/plugins/mod_time.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua
index 2741b8d4..029ddd1d 100644
--- a/plugins/mod_tls.lua
+++ b/plugins/mod_tls.lua
@@ -1,16 +1,16 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-local config = require "core.configmanager";
local create_context = require "core.certmanager".create_context;
+local rawgetopt = require"core.configmanager".rawget;
local st = require "util.stanza";
-local c2s_require_encryption = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
+local c2s_require_encryption = module:get_option("c2s_require_encryption", module:get_option("require_encryption"));
local s2s_require_encryption = module:get_option("s2s_require_encryption");
local allow_s2s_tls = module:get_option("s2s_allow_encryption") ~= false;
local s2s_secure_auth = module:get_option("s2s_secure_auth");
@@ -22,6 +22,7 @@ end
local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls';
local starttls_attr = { xmlns = xmlns_starttls };
+local starttls_initiate= st.stanza("starttls", starttls_attr);
local starttls_proceed = st.stanza("proceed", starttls_attr);
local starttls_failure = st.stanza("failure", starttls_attr);
local c2s_feature = st.stanza("starttls", starttls_attr);
@@ -29,20 +30,67 @@ local s2s_feature = st.stanza("starttls", starttls_attr);
if c2s_require_encryption then c2s_feature:tag("required"):up(); end
if s2s_require_encryption then s2s_feature:tag("required"):up(); end
-local global_ssl_ctx = prosody.global_ssl_ctx;
-
local hosts = prosody.hosts;
local host = hosts[module.host];
+local ssl_ctx_c2s, ssl_ctx_s2sout, ssl_ctx_s2sin;
+local ssl_cfg_c2s, ssl_cfg_s2sout, ssl_cfg_s2sin;
+
+function module.load()
+ local NULL, err = {};
+ local modhost = module.host;
+ local parent = modhost:match("%.(.*)$");
+
+ local parent_ssl = rawgetopt(parent, "ssl") or NULL;
+ local host_ssl = rawgetopt(modhost, "ssl") or parent_ssl;
+
+ local global_c2s = rawgetopt("*", "c2s_ssl") or NULL;
+ local parent_c2s = rawgetopt(parent, "c2s_ssl") or NULL;
+ local host_c2s = rawgetopt(modhost, "c2s_ssl") or parent_c2s;
+
+ local global_s2s = rawgetopt("*", "s2s_ssl") or NULL;
+ local parent_s2s = rawgetopt(parent, "s2s_ssl") or NULL;
+ local host_s2s = rawgetopt(modhost, "s2s_ssl") or parent_s2s;
+
+ ssl_ctx_c2s, err, ssl_cfg_c2s = create_context(host.host, "server", host_c2s, host_ssl, global_c2s); -- for incoming client connections
+ if not ssl_ctx_c2s then module:log("error", "Error creating context for c2s: %s", err); end
+
+ ssl_ctx_s2sout, err, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s); -- for outgoing server connections
+ if not ssl_ctx_s2sout then module:log("error", "Error creating contexts for s2sout: %s", err); end
+
+ ssl_ctx_s2sin, err, ssl_cfg_s2sin = create_context(host.host, "server", host_s2s, host_ssl, global_s2s); -- for incoming server connections
+ if not ssl_ctx_s2sin then module:log("error", "Error creating contexts for s2sin: %s", err); end
+end
+
+module:hook_global("config-reloaded", module.load);
+
local function can_do_tls(session)
+ if not session.conn.starttls then
+ if not session.secure then
+ session.log("debug", "Underlying connection does not support STARTTLS");
+ end
+ return false;
+ elseif session.ssl_ctx ~= nil then
+ return session.ssl_ctx;
+ end
if session.type == "c2s_unauthed" then
- return session.conn.starttls and host.ssl_ctx_in;
+ session.ssl_ctx = ssl_ctx_c2s;
+ session.ssl_cfg = ssl_cfg_c2s;
elseif session.type == "s2sin_unauthed" and allow_s2s_tls then
- return session.conn.starttls and host.ssl_ctx_in;
+ session.ssl_ctx = ssl_ctx_s2sin;
+ session.ssl_cfg = ssl_cfg_s2sin;
elseif session.direction == "outgoing" and allow_s2s_tls then
- return session.conn.starttls and host.ssl_ctx;
+ session.ssl_ctx = ssl_ctx_s2sout;
+ session.ssl_cfg = ssl_cfg_s2sout;
+ else
+ session.log("debug", "Unknown session type, don't know which TLS context to use");
+ return false;
+ end
+ if not session.ssl_ctx then
+ session.log("debug", "Should be able to do TLS but no context available");
+ return false;
end
- return false;
+ return session.ssl_ctx;
end
-- Hook <starttls/>
@@ -51,9 +99,7 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event)
if can_do_tls(origin) then
(origin.sends2s or origin.send)(starttls_proceed);
origin:reset_stream();
- local host = origin.to_host or origin.host;
- local ssl_ctx = host and hosts[host].ssl_ctx_in or global_ssl_ctx;
- origin.conn:starttls(ssl_ctx);
+ origin.conn:starttls(origin.ssl_ctx);
origin.log("debug", "TLS negotiation started for %s...", origin.type);
origin.secure = false;
else
@@ -79,42 +125,21 @@ module:hook("s2s-stream-features", function(event)
end);
-- For s2sout connections, start TLS if we can
-module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
+module:hook_tag("http://etherx.jabber.org/streams", "features", function (session, stanza)
module:log("debug", "Received features element");
- if can_do_tls(session) and stanza:child_with_ns(xmlns_starttls) then
+ if can_do_tls(session) and stanza:get_child("starttls", xmlns_starttls) then
module:log("debug", "%s is offering TLS, taking up the offer...", session.to_host);
- session.sends2s("<starttls xmlns='"..xmlns_starttls.."'/>");
+ session.sends2s(starttls_initiate);
return true;
end
end, 500);
-module:hook_stanza(xmlns_starttls, "proceed", function (session, stanza)
- module:log("debug", "Proceeding with TLS on s2sout...");
- session:reset_stream();
- local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx;
- session.conn:starttls(ssl_ctx);
- session.secure = false;
- return true;
-end);
-
-local function assert_log(ret, err)
- if not ret then
- module:log("error", "Unable to initialize TLS: %s", err);
- end
- return ret;
-end
-
-function module.load()
- local ssl_config = config.rawget(module.host, "ssl");
- if not ssl_config then
- local base_host = module.host:match("%.(.*)");
- ssl_config = config.get(base_host, "ssl");
+module:hook_tag(xmlns_starttls, "proceed", function (session, stanza) -- luacheck: ignore 212/stanza
+ if session.type == "s2sout_unauthed" and can_do_tls(session) then
+ module:log("debug", "Proceeding with TLS on s2sout...");
+ session:reset_stream();
+ session.conn:starttls(session.ssl_ctx);
+ session.secure = false;
+ return true;
end
- host.ssl_ctx = assert_log(create_context(host.host, "client", ssl_config)); -- for outgoing connections
- host.ssl_ctx_in = assert_log(create_context(host.host, "server", ssl_config)); -- for incoming connections
-end
-
-function module.unload()
- host.ssl_ctx = nil;
- host.ssl_ctx_in = nil;
-end
+end);
diff --git a/plugins/mod_unknown.lua b/plugins/mod_unknown.lua
new file mode 100644
index 00000000..4d20b8ad
--- /dev/null
+++ b/plugins/mod_unknown.lua
@@ -0,0 +1,4 @@
+-- Unknown platform stub
+module:set_global();
+
+-- TODO Do things that make sense if we don't know about the platform
diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua
index 3f275b2f..2e369b16 100644
--- a/plugins/mod_uptime.lua
+++ b/plugins/mod_uptime.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
diff --git a/plugins/mod_vcard.lua b/plugins/mod_vcard.lua
index 26b30e3a..72f92ef7 100644
--- a/plugins/mod_vcard.lua
+++ b/plugins/mod_vcard.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
diff --git a/plugins/mod_version.lua b/plugins/mod_version.lua
index d35103b6..7f045415 100644
--- a/plugins/mod_version.lua
+++ b/plugins/mod_version.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -16,11 +16,11 @@ local query = st.stanza("query", {xmlns = "jabber:iq:version"})
:tag("name"):text("Prosody"):up()
:tag("version"):text(prosody.version):up();
-if not module:get_option("hide_os_type") then
+if not module:get_option_boolean("hide_os_type") then
if os.getenv("WINDIR") then
version = "Windows";
else
- local os_version_command = module:get_option("os_version_command");
+ local os_version_command = module:get_option_string("os_version_command");
local ok, pposix = pcall(require, "util.pposix");
if not os_version_command and (ok and pposix and pposix.uname) then
version = pposix.uname().sysname;
diff --git a/plugins/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua
index 0e9d2fca..82666b09 100644
--- a/plugins/mod_watchregistrations.lua
+++ b/plugins/mod_watchregistrations.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -11,13 +11,14 @@ local host = module:get_host();
local jid_prep = require "util.jid".prep;
local registration_watchers = module:get_option_set("registration_watchers", module:get_option("admins", {})) / jid_prep;
-local registration_notification = module:get_option("registration_notification", "User $username just registered on $host from $ip");
+local registration_from = module:get_option_string("registration_from", host);
+local registration_notification = module:get_option_string("registration_notification", "User $username just registered on $host from $ip");
local st = require "util.stanza";
module:hook("user-registered", function (user)
module:log("debug", "Notifying of new registration");
- local message = st.message{ type = "chat", from = host }
+ local message = st.message{ type = "chat", from = registration_from }
:tag("body")
:text(registration_notification:gsub("%$(%w+)", function (v)
return user[v] or user.session and user.session[v] or nil;
diff --git a/plugins/mod_websocket.lua b/plugins/mod_websocket.lua
new file mode 100644
index 00000000..edc104df
--- /dev/null
+++ b/plugins/mod_websocket.lua
@@ -0,0 +1,343 @@
+-- Prosody IM
+-- Copyright (C) 2012-2014 Florian Zeitz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- luacheck: ignore 431/log
+
+module:set_global();
+
+local add_task = require "util.timer".add_task;
+local add_filter = require "util.filters".add_filter;
+local sha1 = require "util.hashes".sha1;
+local base64 = require "util.encodings".base64.encode;
+local st = require "util.stanza";
+local parse_xml = require "util.xml".parse;
+local contains_token = require "util.http".contains_token;
+local portmanager = require "core.portmanager";
+local sm_destroy_session = require"core.sessionmanager".destroy_session;
+local log = module._log;
+
+local websocket_frames = require"net.websocket.frames";
+local parse_frame = websocket_frames.parse;
+local build_frame = websocket_frames.build;
+local build_close = websocket_frames.build_close;
+local parse_close = websocket_frames.parse_close;
+
+local t_concat = table.concat;
+
+local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5);
+local consider_websocket_secure = module:get_option_boolean("consider_websocket_secure");
+local cross_domain = module:get_option_set("cross_domain_websocket", {});
+if cross_domain:contains("*") or cross_domain:contains(true) then
+ cross_domain = true;
+end
+
+local function check_origin(origin)
+ if cross_domain == true then
+ return true;
+ end
+ return cross_domain:contains(origin);
+end
+
+local xmlns_framing = "urn:ietf:params:xml:ns:xmpp-framing";
+local xmlns_streams = "http://etherx.jabber.org/streams";
+local xmlns_client = "jabber:client";
+local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
+
+module:depends("c2s")
+local sessions = module:shared("c2s/sessions");
+local c2s_listener = portmanager.get_service("c2s").listener;
+
+--- Session methods
+local function session_open_stream(session, from, to)
+ local attr = {
+ xmlns = xmlns_framing,
+ ["xml:lang"] = "en",
+ version = "1.0",
+ id = session.streamid or "",
+ from = from or session.host, to = to,
+ };
+ if session.stream_attrs then
+ session:stream_attrs(from, to, attr)
+ end
+ session.send(st.stanza("open", attr));
+end
+
+local function session_close(session, reason)
+ local log = session.log or log;
+ if session.conn then
+ if session.notopen then
+ session:open_stream();
+ end
+ if reason then -- nil == no err, initiated by us, false == initiated by client
+ local stream_error = st.stanza("stream:error");
+ if type(reason) == "string" then -- assume stream error
+ stream_error:tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' });
+ elseif type(reason) == "table" then
+ if reason.condition then
+ stream_error:tag(reason.condition, stream_xmlns_attr):up();
+ if reason.text then
+ stream_error:tag("text", stream_xmlns_attr):text(reason.text):up();
+ end
+ if reason.extra then
+ stream_error:add_child(reason.extra);
+ end
+ elseif reason.name then -- a stanza
+ stream_error = reason;
+ end
+ end
+ log("debug", "Disconnecting client, <stream:error> is: %s", tostring(stream_error));
+ session.send(stream_error);
+ end
+
+ session.send(st.stanza("close", { xmlns = xmlns_framing }));
+ function session.send() return false; end
+
+ local reason = (reason and (reason.name or reason.text or reason.condition)) or reason;
+ session.log("debug", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed");
+
+ -- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
+ local conn = session.conn;
+ if reason == nil and not session.notopen and session.type == "c2s" then
+ -- Grace time to process data from authenticated cleanly-closed stream
+ add_task(stream_close_timeout, function ()
+ if not session.destroyed then
+ session.log("warn", "Failed to receive a stream close response, closing connection anyway...");
+ sm_destroy_session(session, reason);
+ conn:write(build_close(1000, "Stream closed"));
+ conn:close();
+ end
+ end);
+ else
+ sm_destroy_session(session, reason);
+ conn:write(build_close(1000, "Stream closed"));
+ conn:close();
+ end
+ end
+end
+
+
+--- Filters
+local function filter_open_close(data)
+ if not data:find(xmlns_framing, 1, true) then return data; end
+
+ local oc = parse_xml(data);
+ if not oc then return data; end
+ if oc.attr.xmlns ~= xmlns_framing then return data; end
+ if oc.name == "close" then return "</stream:stream>"; end
+ if oc.name == "open" then
+ oc.name = "stream:stream";
+ oc.attr.xmlns = nil;
+ oc.attr["xmlns:stream"] = xmlns_streams;
+ return oc:top_tag();
+ end
+
+ return data;
+end
+function handle_request(event)
+ local request, response = event.request, event.response;
+ local conn = response.conn;
+
+ conn.starttls = false; -- Prevent mod_tls from believing starttls can be done
+
+ if not request.headers.sec_websocket_key then
+ response.headers.content_type = "text/html";
+ return [[<!DOCTYPE html><html><head><title>Websocket</title></head><body>
+ <p>It works! Now point your WebSocket client to this URL to connect to Prosody.</p>
+ </body></html>]];
+ end
+
+ local wants_xmpp = contains_token(request.headers.sec_websocket_protocol or "", "xmpp");
+
+ if not wants_xmpp then
+ module:log("debug", "Client didn't want to talk XMPP, list of protocols was %s", request.headers.sec_websocket_protocol or "(empty)");
+ return 501;
+ end
+
+ if not check_origin(request.headers.origin or "") then
+ module:log("debug", "Origin %s is not allowed by 'cross_domain_websocket'", request.headers.origin or "(missing header)");
+ return 403;
+ end
+
+ local function websocket_close(code, message)
+ conn:write(build_close(code, message));
+ conn:close();
+ end
+
+ local dataBuffer;
+ local function handle_frame(frame)
+ local opcode = frame.opcode;
+ local length = frame.length;
+ module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
+
+ -- Error cases
+ if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
+ websocket_close(1002, "Reserved bits not zero");
+ return false;
+ end
+
+ if opcode == 0x8 then -- close frame
+ if length == 1 then
+ websocket_close(1002, "Close frame with payload, but too short for status code");
+ return false;
+ elseif length >= 2 then
+ local status_code = parse_close(frame.data)
+ if status_code < 1000 then
+ websocket_close(1002, "Closed with invalid status code");
+ return false;
+ elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
+ websocket_close(1002, "Closed with reserved status code");
+ return false;
+ end
+ end
+ end
+
+ if opcode >= 0x8 then
+ if length > 125 then -- Control frame with too much payload
+ websocket_close(1002, "Payload too large");
+ return false;
+ end
+
+ if not frame.FIN then -- Fragmented control frame
+ websocket_close(1002, "Fragmented control frame");
+ return false;
+ end
+ end
+
+ if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then
+ websocket_close(1002, "Reserved opcode");
+ return false;
+ end
+
+ if opcode == 0x0 and not dataBuffer then
+ websocket_close(1002, "Unexpected continuation frame");
+ return false;
+ end
+
+ if (opcode == 0x1 or opcode == 0x2) and dataBuffer then
+ websocket_close(1002, "Continuation frame expected");
+ return false;
+ end
+
+ -- Valid cases
+ if opcode == 0x0 then -- Continuation frame
+ dataBuffer[#dataBuffer+1] = frame.data;
+ elseif opcode == 0x1 then -- Text frame
+ dataBuffer = {frame.data};
+ elseif opcode == 0x2 then -- Binary frame
+ websocket_close(1003, "Only text frames are supported");
+ return;
+ elseif opcode == 0x8 then -- Close request
+ websocket_close(1000, "Goodbye");
+ return;
+ elseif opcode == 0x9 then -- Ping frame
+ frame.opcode = 0xA;
+ conn:write(build_frame(frame));
+ return "";
+ elseif opcode == 0xA then -- Pong frame, MAY be sent unsolicited, eg as keepalive
+ return "";
+ else
+ log("warn", "Received frame with unsupported opcode %i", opcode);
+ return "";
+ end
+
+ if frame.FIN then
+ local data = t_concat(dataBuffer, "");
+ dataBuffer = nil;
+ return data;
+ end
+ return "";
+ end
+
+ conn:setlistener(c2s_listener);
+ c2s_listener.onconnect(conn);
+
+ local session = sessions[conn];
+
+ session.secure = consider_websocket_secure or session.secure;
+ session.websocket_request = request;
+
+ session.open_stream = session_open_stream;
+ session.close = session_close;
+
+ local frameBuffer = "";
+ add_filter(session, "bytes/in", function(data)
+ local cache = {};
+ frameBuffer = frameBuffer .. data;
+ local frame, length = parse_frame(frameBuffer);
+
+ while frame do
+ frameBuffer = frameBuffer:sub(length + 1);
+ local result = handle_frame(frame);
+ if not result then return; end
+ cache[#cache+1] = filter_open_close(result);
+ frame, length = parse_frame(frameBuffer);
+ end
+ return t_concat(cache, "");
+ end);
+
+ add_filter(session, "stanzas/out", function(stanza)
+ local attr = stanza.attr;
+ attr.xmlns = attr.xmlns or xmlns_client;
+ if stanza.name:find("^stream:") then
+ attr["xmlns:stream"] = attr["xmlns:stream"] or xmlns_streams;
+ end
+ return stanza;
+ end, -1000);
+
+ add_filter(session, "bytes/out", function(data)
+ return build_frame({ FIN = true, opcode = 0x01, data = tostring(data)});
+ end);
+
+ response.status_code = 101;
+ response.headers.upgrade = "websocket";
+ response.headers.connection = "Upgrade";
+ response.headers.sec_webSocket_accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
+ response.headers.sec_webSocket_protocol = "xmpp";
+
+ session.log("debug", "Sending WebSocket handshake");
+
+ return "";
+end
+
+local function keepalive(event)
+ local session = event.session;
+ if session.open_stream == session_open_stream then
+ return session.conn:write(build_frame({ opcode = 0x9, FIN = true }));
+ end
+end
+
+module:hook("c2s-read-timeout", keepalive, -0.9);
+
+function module.add_host(module)
+ module:depends("http");
+ module:provides("http", {
+ name = "websocket";
+ default_path = "xmpp-websocket";
+ route = {
+ ["GET"] = handle_request;
+ ["GET /"] = handle_request;
+ };
+ });
+ module:hook("c2s-read-timeout", keepalive, -0.9);
+
+ if cross_domain ~= true then
+ local url = require "socket.url";
+ local ws_url = module:http_url("websocket", "xmpp-websocket");
+ local url_components = url.parse(ws_url);
+ -- The 'Origin' consists of the base URL without path
+ url_components.path = nil;
+ local this_origin = url.build(url_components);
+ local local_cross_domain = module:get_option_set("cross_domain_websocket", { this_origin });
+ -- Don't add / remove something added by another host
+ -- This might be weird with random load order
+ local_cross_domain:exclude(cross_domain);
+ cross_domain:include(local_cross_domain);
+ module:log("debug", "cross_domain = %s", tostring(cross_domain));
+ function module.unload()
+ cross_domain:exclude(local_cross_domain);
+ end
+ end
+end
diff --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua
index c4ebaf30..f6b13df5 100644
--- a/plugins/mod_welcome.lua
+++ b/plugins/mod_welcome.lua
@@ -1,13 +1,13 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local host = module:get_host();
-local welcome_text = module:get_option("welcome_message") or "Hello $username, welcome to the $host IM server!";
+local welcome_text = module:get_option_string("welcome_message", "Hello $username, welcome to the $host IM server!");
local st = require "util.stanza";
diff --git a/plugins/mod_windows.lua b/plugins/mod_windows.lua
new file mode 100644
index 00000000..8085fd88
--- /dev/null
+++ b/plugins/mod_windows.lua
@@ -0,0 +1,4 @@
+-- Windows platform stub
+module:set_global();
+
+-- TODO Add Windows-specific things here
diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua
index acc2da0d..0f58bfbc 100644
--- a/plugins/muc/mod_muc.lua
+++ b/plugins/muc/mod_muc.lua
@@ -1,27 +1,30 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
+local array = require "util.array";
if module:get_host_type() ~= "component" then
error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0);
end
local muc_host = module:get_host();
-local muc_name = module:get_option("name");
-if type(muc_name) ~= "string" then muc_name = "Prosody Chatrooms"; end
+local muc_name = module:get_option_string("name", "Prosody Chatrooms");
local restrict_room_creation = module:get_option("restrict_room_creation");
if restrict_room_creation then
- if restrict_room_creation == true then
+ if restrict_room_creation == true then
restrict_room_creation = "admin";
elseif restrict_room_creation ~= "admin" and restrict_room_creation ~= "local" then
restrict_room_creation = nil;
end
end
+local lock_rooms = module:get_option_boolean("muc_room_locking", false);
+local lock_room_timeout = module:get_option_number("muc_room_lock_timeout", 300);
+
local muclib = module:require "muc";
local muc_new_room = muclib.new_room;
local jid_split = require "util.jid".split;
@@ -34,18 +37,31 @@ local hosts = prosody.hosts;
rooms = {};
local rooms = rooms;
local persistent_rooms_storage = module:open_store("persistent");
-local persistent_rooms = persistent_rooms_storage:get() or {};
+local persistent_rooms, err = persistent_rooms_storage:get();
+if not persistent_rooms then
+ if err then
+ module:log("error", "Error loading list of persistent rooms from storage. Reload mod_muc or restart to recover.");
+ error("Storage error: "..err);
+ end
+ module:log("debug", "No persistent rooms found in the database");
+ persistent_rooms = {};
+end
local room_configs = module:open_store("config");
-- Configurable options
muclib.set_max_history_length(module:get_option_number("max_history_messages"));
+module:depends("disco");
+module:add_identity("conference", "text", muc_name);
+module:add_feature("http://jabber.org/protocol/muc");
+
local function is_admin(jid)
return um_is_admin(jid, module.host);
end
-local _set_affiliation = muc_new_room.room_mt.set_affiliation;
-local _get_affiliation = muc_new_room.room_mt.get_affiliation;
+room_mt = muclib.room_mt; -- Yes, global.
+local _set_affiliation = room_mt.set_affiliation;
+local _get_affiliation = room_mt.get_affiliation;
function muclib.room_mt:get_affiliation(jid)
if is_admin(jid) then return "owner"; end
return _get_affiliation(self, jid);
@@ -78,11 +94,21 @@ local function room_save(room, forced)
if forced then persistent_rooms_storage:set(nil, persistent_rooms); end
end
-function create_room(jid)
+function create_room(jid, locked)
local room = muc_new_room(jid);
room.route_stanza = room_route_stanza;
room.save = room_save;
rooms[jid] = room;
+ if locked then
+ room.locked = true;
+ if lock_room_timeout and lock_room_timeout > 0 then
+ module:add_timer(lock_room_timeout, function ()
+ if room.locked then
+ room:destroy(); -- Not unlocked in time
+ end
+ end);
+ end
+ end
module:fire_event("muc-room-created", { room = room });
return room;
end
@@ -90,15 +116,21 @@ end
local persistent_errors = false;
for jid in pairs(persistent_rooms) do
local node = jid_split(jid);
- local data = room_configs:get(node);
+ local data, err = room_configs:get(node);
if data then
local room = create_room(jid);
room._data = data._data;
room._affiliations = data._affiliations;
- else -- missing room data
+ elseif not err then -- missing room data
persistent_rooms[jid] = nil;
module:log("error", "Missing data for room '%s', removing from persistent room list", jid);
persistent_errors = true;
+ else -- error
+ module:log("error", "Error loading data for room '%s', locking it until service restart. Error was: %s", jid, err);
+ local room = muc_new_room(jid);
+ room.locked = true;
+ room._affiliations = { [muc_host] = "owner" }; -- To prevent unlocking
+ rooms[jid] = room;
end
end
if persistent_errors then persistent_rooms_storage:set(nil, persistent_rooms); end
@@ -107,20 +139,15 @@ local host_room = muc_new_room(muc_host);
host_room.route_stanza = room_route_stanza;
host_room.save = room_save;
-local function get_disco_info(stanza)
- return st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info")
- :tag("identity", {category='conference', type='text', name=muc_name}):up()
- :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply
-end
-local function get_disco_items(stanza)
- local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
+module:hook("host-disco-items", function(event)
+ local reply = event.reply;
+ module:log("debug", "host-disco-items called");
for jid, room in pairs(rooms) do
- if not room:is_hidden() then
+ if not room:get_hidden() then
reply:tag("item", {jid=jid, name=room:get_name()}):up();
end
end
- return reply; -- TODO cache disco reply
-end
+end);
local function handle_to_domain(event)
local origin, stanza = event.origin, event.stanza;
@@ -129,11 +156,7 @@ local function handle_to_domain(event)
if stanza.name == "iq" and type == "get" then
local xmlns = stanza.tags[1].attr.xmlns;
local node = stanza.tags[1].attr.node;
- if xmlns == "http://jabber.org/protocol/disco#info" and not node then
- origin.send(get_disco_info(stanza));
- elseif xmlns == "http://jabber.org/protocol/disco#items" and not node then
- origin.send(get_disco_items(stanza));
- elseif xmlns == "http://jabber.org/protocol/muc#unique" then
+ if xmlns == "http://jabber.org/protocol/muc#unique" then
origin.send(st.reply(stanza):tag("unique", {xmlns = xmlns}):text(uuid_gen())); -- FIXME Random UUIDs can theoretically have collisions
else
origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc
@@ -150,14 +173,16 @@ function stanza_handler(event)
local bare = jid_bare(stanza.attr.to);
local room = rooms[bare];
if not room then
- if stanza.name ~= "presence" then
- origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+ if stanza.name ~= "presence" or stanza.attr.type ~= nil then
+ if stanza.attr.type ~= "error" then
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+ end
return true;
end
if not(restrict_room_creation) or
is_admin(stanza.attr.from) or
(restrict_room_creation == "local" and select(2, jid_split(stanza.attr.from)) == module.host:gsub("^[^%.]+%.", "")) then
- room = create_room(bare);
+ room = create_room(bare, lock_rooms);
end
end
if room then
@@ -220,7 +245,8 @@ function shutdown_component()
if not saved then
local stanza = st.presence({type = "unavailable"})
:tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
- :tag("item", { affiliation='none', role='none' }):up();
+ :tag("item", { affiliation='none', role='none' }):up()
+ :tag("status", { code = "332"}):up();
for roomjid, room in pairs(rooms) do
shutdown_room(room, stanza);
end
@@ -229,3 +255,39 @@ function shutdown_component()
end
module.unload = shutdown_component;
module:hook_global("server-stopping", shutdown_component);
+
+-- Ad-hoc commands
+module:depends("adhoc")
+local t_concat = table.concat;
+local keys = require "util.iterators".keys;
+local adhoc_new = module:require "adhoc".new;
+local adhoc_initial = require "util.adhoc".new_initial_data_form;
+local dataforms_new = require "util.dataforms".new;
+
+local destroy_rooms_layout = dataforms_new {
+ title = "Destroy rooms";
+ instructions = "Select the rooms to destroy";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#destroy" };
+ { name = "rooms", type = "list-multi", required = true, label = "Rooms to destroy:"};
+};
+
+local destroy_rooms_handler = adhoc_initial(destroy_rooms_layout, function()
+ return { rooms = array.collect(keys(rooms)):sort() };
+end, function(fields, errors)
+ if errors then
+ local errmsg = {};
+ for name, err in pairs(errors) do
+ errmsg[#errmsg + 1] = name .. ": " .. err;
+ end
+ return { status = "completed", error = { message = t_concat(errmsg, "\n") } };
+ end
+ for _, room in ipairs(fields.rooms) do
+ rooms[room]:destroy();
+ rooms[room] = nil;
+ end
+ return { status = "completed", info = "The following rooms were destroyed:\n"..t_concat(fields.rooms, "\n") };
+end);
+local destroy_rooms_desc = adhoc_new("Destroy Rooms", "http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin");
+
+module:provides("adhoc", destroy_rooms_desc);
diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua
index e8d565f2..5d8c6df5 100644
--- a/plugins/muc/muc.lib.lua
+++ b/plugins/muc/muc.lib.lua
@@ -1,7 +1,7 @@
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
---
+--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
@@ -27,28 +27,16 @@ local muc_domain = nil; --module:get_host();
local default_history_length, max_history_length = 20, math.huge;
------------
-local function filter_xmlns_from_array(array, filters)
- local count = 0;
- for i=#array,1,-1 do
- local attr = array[i].attr;
- if filters[attr and attr.xmlns] then
- t_remove(array, i);
- count = count + 1;
- end
- end
- return count;
-end
-local function filter_xmlns_from_stanza(stanza, filters)
- if filters then
- if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then
- return stanza, filter_xmlns_from_array(stanza, filters);
- end
+local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true};
+local function presence_filter(tag)
+ if presence_filters[tag.attr.xmlns] then
+ return nil;
end
- return stanza, 0;
+ return tag;
end
-local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true};
+
local function get_filtered_presence(stanza)
- return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters);
+ return st.clone(stanza):maptags(presence_filter);
end
local kickable_error_conditions = {
["gone"] = true;
@@ -72,17 +60,6 @@ local function is_kickable_error(stanza)
local cond = get_error_condition(stanza);
return kickable_error_conditions[cond] and cond;
end
-local function getUsingPath(stanza, path, getText)
- local tag = stanza;
- for _, name in ipairs(path) do
- if type(tag) ~= 'table' then return; end
- tag = tag:child_with_name(name);
- end
- if tag and getText then tag = table.concat(tag); end
- return tag;
-end
-local function getTag(stanza, path) return getUsingPath(stanza, path); end
-local function getText(stanza, path) return getUsingPath(stanza, path, true); end
-----------
local room_mt = {};
@@ -98,8 +75,8 @@ function room_mt:get_default_role(affiliation)
elseif affiliation == "member" then
return "participant";
elseif not affiliation then
- if not self:is_members_only() then
- return self:is_moderated() and "visitor" or "participant";
+ if not self:get_members_only() then
+ return self:get_moderated() and "visitor" or "participant";
end
end
end
@@ -146,18 +123,21 @@ function room_mt:broadcast_message(stanza, historic)
end
stanza.attr.to = to;
if historic then -- add to history
- local history = self._data['history'];
- if not history then history = {}; self._data['history'] = history; end
- stanza = st.clone(stanza);
- stanza.attr.to = "";
- local stamp = datetime.datetime();
- stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = self.jid, stamp = stamp}):up(); -- XEP-0203
- stanza:tag("x", {xmlns = "jabber:x:delay", from = self.jid, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
- local entry = { stanza = stanza, stamp = stamp };
- t_insert(history, entry);
- while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end
+ return self:save_to_history(stanza)
end
end
+function room_mt:save_to_history(stanza)
+ local history = self._data['history'];
+ if not history then history = {}; self._data['history'] = history; end
+ stanza = st.clone(stanza);
+ stanza.attr.to = "";
+ local stamp = datetime.datetime();
+ stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = self.jid, stamp = stamp}):up(); -- XEP-0203
+ stanza:tag("x", {xmlns = "jabber:x:delay", from = self.jid, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
+ local entry = { stanza = stanza, stamp = stamp };
+ t_insert(history, entry);
+ while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end
+end
function room_mt:broadcast_except_nick(stanza, nick)
for rnick, occupant in pairs(self._occupants) do
if rnick ~= nick then
@@ -186,10 +166,10 @@ function room_mt:send_history(to, stanza)
if history then
local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc");
local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc");
-
+
local maxchars = history_tag and tonumber(history_tag.attr.maxchars);
if maxchars then maxchars = math.floor(maxchars); end
-
+
local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history);
if not history_tag then maxstanzas = 20; end
@@ -202,7 +182,7 @@ function room_mt:send_history(to, stanza)
local n = 0;
local charcount = 0;
-
+
for i=#history,1,-1 do
local entry = history[i];
if maxchars then
@@ -223,26 +203,36 @@ function room_mt:send_history(to, stanza)
self:_route_stanza(msg);
end
end
+end
+function room_mt:send_subject(to)
self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject']));
end
function room_mt:get_disco_info(stanza)
local count = 0; for _ in pairs(self._occupants) do count = count + 1; end
- return st.reply(stanza):query("http://jabber.org/protocol/disco#info")
+ local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info")
:tag("identity", {category="conference", type="text", name=self:get_name()}):up()
:tag("feature", {var="http://jabber.org/protocol/muc"}):up()
+ :tag("feature", {var="http://jabber.org/protocol/muc#stable_id"}):up()
:tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up()
- :tag("feature", {var=self:is_moderated() and "muc_moderated" or "muc_unmoderated"}):up()
- :tag("feature", {var=self:is_members_only() and "muc_membersonly" or "muc_open"}):up()
- :tag("feature", {var=self:is_persistent() and "muc_persistent" or "muc_temporary"}):up()
- :tag("feature", {var=self:is_hidden() and "muc_hidden" or "muc_public"}):up()
+ :tag("feature", {var=self:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up()
+ :tag("feature", {var=self:get_members_only() and "muc_membersonly" or "muc_open"}):up()
+ :tag("feature", {var=self:get_persistent() and "muc_persistent" or "muc_temporary"}):up()
+ :tag("feature", {var=self:get_hidden() and "muc_hidden" or "muc_public"}):up()
:tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up()
- :add_child(dataform.new({
- { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" },
- { name = "muc#roominfo_description", label = "Description", value = "" },
- { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) }
- }):form({["muc#roominfo_description"] = self:get_description()}, 'result'))
;
+ local dataform = dataform.new({
+ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" },
+ { name = "muc#roominfo_description", label = "Description", value = "" },
+ { name = "muc#roominfo_occupants", label = "Number of occupants", value = "" }
+ });
+ local formdata = {
+ ["muc#roominfo_description"] = self:get_description(),
+ ["muc#roominfo_occupants"] = tostring(count),
+ };
+ module:fire_event("muc-disco#info", { room = self, reply = reply, form = dataform, formdata = formdata });
+ reply:add_child(dataform:form(formdata, 'result'))
+ return reply;
end
function room_mt:get_disco_items(stanza)
local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
@@ -252,7 +242,6 @@ function room_mt:get_disco_items(stanza)
return reply;
end
function room_mt:set_subject(current_nick, subject)
- -- TODO check nick's authority
if subject == "" then subject = nil; end
self._data['subject'] = subject;
self._data['subject_from'] = current_nick;
@@ -310,7 +299,7 @@ function room_mt:set_moderated(moderated)
if self.save then self:save(true); end
end
end
-function room_mt:is_moderated()
+function room_mt:get_moderated()
return self._data.moderated;
end
function room_mt:set_members_only(members_only)
@@ -320,7 +309,7 @@ function room_mt:set_members_only(members_only)
if self.save then self:save(true); end
end
end
-function room_mt:is_members_only()
+function room_mt:get_members_only()
return self._data.members_only;
end
function room_mt:set_persistent(persistent)
@@ -330,7 +319,7 @@ function room_mt:set_persistent(persistent)
if self.save then self:save(true); end
end
end
-function room_mt:is_persistent()
+function room_mt:get_persistent()
return self._data.persistent;
end
function room_mt:set_hidden(hidden)
@@ -340,9 +329,15 @@ function room_mt:set_hidden(hidden)
if self.save then self:save(true); end
end
end
-function room_mt:is_hidden()
+function room_mt:get_hidden()
return self._data.hidden;
end
+function room_mt:get_public()
+ return not self:get_hidden();
+end
+function room_mt:set_public(public)
+ return self:set_hidden(not public);
+end
function room_mt:set_changesubject(changesubject)
changesubject = changesubject and true or nil;
if self._data.changesubject ~= changesubject then
@@ -365,12 +360,25 @@ function room_mt:set_historylength(length)
end
+local valid_whois = { moderators = true, anyone = true };
+
+function room_mt:set_whois(whois)
+ if valid_whois[whois] and self._data.whois ~= whois then
+ self._data.whois = whois;
+ if self.save then self:save(true); end
+ end
+end
+
+function room_mt:get_whois()
+ return self._data.whois;
+end
+
local function construct_stanza_id(room, stanza)
local from_jid, to_nick = stanza.attr.from, stanza.attr.to;
local from_nick = room._jid_nick[from_jid];
local occupant = room._occupants[to_nick];
local to_jid = occupant.jid;
-
+
return from_nick, to_jid, base64.encode(to_jid.."\0"..stanza.attr.id.."\0"..md5(from_jid));
end
local function deconstruct_stanza_id(room, stanza)
@@ -442,6 +450,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
self._occupants[current_nick].sessions[from] = pr;
self:broadcast_presence(pr, from);
else -- change nick
+ -- a MUC service MUST NOT allow empty or invisible Room Nicknames
+ -- (i.e., Room Nicknames that consist only of one or more space characters).
+ if not select(3, jid_split(to)):find("[^ ]") then -- resourceprep turns all whitespace into 0x20
+ module:log("debug", "Rejecting invisible nickname");
+ origin.send(st.error_reply(stanza, "cancel", "not-allowed"));
+ return;
+ end
local occupant = self._occupants[current_nick];
local is_multisession = next(occupant.sessions, next(occupant.sessions));
if self._occupants[to] or is_multisession then
@@ -474,6 +489,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
-- self:handle_to_occupant(origin, stanza); -- resend available
--end
else -- enter room
+ -- a MUC service MUST NOT allow empty or invisible Room Nicknames
+ -- (i.e., Room Nicknames that consist only of one or more space characters).
+ if not select(3, jid_split(to)):find("[^ ]") then -- resourceprep turns all whitespace into 0x20
+ module:log("debug", "Rejecting invisible nickname");
+ origin.send(st.error_reply(stanza, "cancel", "not-allowed"));
+ return;
+ end
local new_nick = to;
local is_merge;
if self._occupants[to] then
@@ -499,6 +521,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
log("debug", "%s joining as %s", from, to);
if not next(self._affiliations) then -- new room, no owners
self._affiliations[jid_bare(from)] = "owner";
+ if self.locked and not stanza:get_child("x", "http://jabber.org/protocol/muc") then
+ self.locked = nil; -- Older groupchat protocol doesn't lock
+ end
+ elseif self.locked then -- Deny entry
+ module:log("debug", "Room is locked, denying entry");
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+ return;
end
local affiliation = self:get_affiliation(from);
local role = self:get_default_role(affiliation)
@@ -520,9 +549,13 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
if self._data.whois == 'anyone' then
pr:tag("status", {code='100'}):up();
end
+ if self.locked then
+ pr:tag("status", {code='201'}):up();
+ end
pr.attr.to = from;
self:_route_stanza(pr);
self:send_history(from, stanza);
+ self:send_subject(from);
elseif not affiliation then -- registration required for entering members-only room
local reply = st.error_reply(stanza, "auth", "registration-required"):up();
reply.tags[1].attr.code = "407";
@@ -574,6 +607,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
end
stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
else -- message
+ stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up();
stanza.attr.from = current_nick;
for jid in pairs(o_data.sessions) do
stanza.attr.to = jid;
@@ -589,11 +623,11 @@ end
function room_mt:send_form(origin, stanza)
origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
- :add_child(self:get_form_layout():form())
+ :add_child(self:get_form_layout(stanza.attr.from):form())
);
end
-function room_mt:get_form_layout()
+function room_mt:get_form_layout(actor)
local form = dataform.new({
title = "Configuration for "..self.jid,
instructions = "Complete and submit this form to configure the room.",
@@ -618,13 +652,13 @@ function room_mt:get_form_layout()
name = 'muc#roomconfig_persistentroom',
type = 'boolean',
label = 'Make Room Persistent?',
- value = self:is_persistent()
+ value = self:get_persistent()
},
{
name = 'muc#roomconfig_publicroom',
type = 'boolean',
label = 'Make Room Publicly Searchable?',
- value = not self:is_hidden()
+ value = not self:get_hidden()
},
{
name = 'muc#roomconfig_changesubject',
@@ -651,13 +685,13 @@ function room_mt:get_form_layout()
name = 'muc#roomconfig_moderatedroom',
type = 'boolean',
label = 'Make Room Moderated?',
- value = self:is_moderated()
+ value = self:get_moderated()
},
{
name = 'muc#roomconfig_membersonly',
type = 'boolean',
label = 'Make Room Members-Only?',
- value = self:is_members_only()
+ value = self:get_members_only()
},
{
name = 'muc#roomconfig_historylength',
@@ -666,14 +700,9 @@ function room_mt:get_form_layout()
value = tostring(self:get_historylength())
}
});
- return module:fire_event("muc-config-form", { room = self, form = form }) or form;
+ return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form;
end
-local valid_whois = {
- moderators = true,
- anyone = true,
-}
-
function room_mt:process_form(origin, stanza)
local query = stanza.tags[1];
local form;
@@ -689,86 +718,52 @@ function room_mt:process_form(origin, stanza)
return true;
end
-
- local fields = self:get_form_layout():data(form);
- if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); return; end
-
- local dirty = false
-
- local event = { room = self, fields = fields, changed = dirty };
- module:fire_event("muc-config-submitted", event);
- dirty = event.changed or dirty;
-
- local name = fields['muc#roomconfig_roomname'];
- if name ~= self:get_name() then
- self:set_name(name);
- end
-
- local description = fields['muc#roomconfig_roomdesc'];
- if description ~= self:get_description() then
- self:set_description(description);
+ local fields, errors, present = self:get_form_layout(stanza.attr.from):data(form);
+ if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then
+ origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration"));
+ return;
end
- local persistent = fields['muc#roomconfig_persistentroom'];
- dirty = dirty or (self:is_persistent() ~= persistent)
- module:log("debug", "persistent=%s", tostring(persistent));
-
- local moderated = fields['muc#roomconfig_moderatedroom'];
- dirty = dirty or (self:is_moderated() ~= moderated)
- module:log("debug", "moderated=%s", tostring(moderated));
-
- local membersonly = fields['muc#roomconfig_membersonly'];
- dirty = dirty or (self:is_members_only() ~= membersonly)
- module:log("debug", "membersonly=%s", tostring(membersonly));
-
- local public = fields['muc#roomconfig_publicroom'];
- dirty = dirty or (self:is_hidden() ~= (not public and true or nil))
+ local changed = {};
- local changesubject = fields['muc#roomconfig_changesubject'];
- dirty = dirty or (self:get_changesubject() ~= (not changesubject and true or nil))
- module:log('debug', 'changesubject=%s', changesubject and "true" or "false")
-
- local historylength = tonumber(fields['muc#roomconfig_historylength']);
- dirty = dirty or (historylength and (self:get_historylength() ~= historylength));
- module:log('debug', 'historylength=%s', historylength)
-
-
- local whois = fields['muc#roomconfig_whois'];
- if not valid_whois[whois] then
- origin.send(st.error_reply(stanza, 'cancel', 'bad-request', "Invalid value for 'whois'"));
- return;
+ local function handle_option(name, field, allowed)
+ if not present[field] then return; end
+ local new = fields[field];
+ if allowed and not allowed[new] then return; end
+ if new == self["get_"..name](self) then return; end
+ changed[name] = true;
+ self["set_"..name](self, new);
end
- local whois_changed = self._data.whois ~= whois
- self._data.whois = whois
- module:log('debug', 'whois=%s', whois)
- local password = fields['muc#roomconfig_roomsecret'];
- if self:get_password() ~= password then
- self:set_password(password);
- end
- self:set_moderated(moderated);
- self:set_members_only(membersonly);
- self:set_persistent(persistent);
- self:set_hidden(not public);
- self:set_changesubject(changesubject);
- self:set_historylength(historylength);
+ local event = { room = self, fields = fields, changed = changed, stanza = stanza, origin = origin, update_option = handle_option };
+ module:fire_event("muc-config-submitted", event);
+
+ handle_option("name", "muc#roomconfig_roomname");
+ handle_option("description", "muc#roomconfig_roomdesc");
+ handle_option("persistent", "muc#roomconfig_persistentroom");
+ handle_option("moderated", "muc#roomconfig_moderatedroom");
+ handle_option("members_only", "muc#roomconfig_membersonly");
+ handle_option("public", "muc#roomconfig_publicroom");
+ handle_option("changesubject", "muc#roomconfig_changesubject");
+ handle_option("historylength", "muc#roomconfig_historylength");
+ handle_option("whois", "muc#roomconfig_whois", valid_whois);
+ handle_option("password", "muc#roomconfig_roomsecret");
if self.save then self:save(true); end
+ if self.locked then
+ module:fire_event("muc-room-unlocked", { room = self });
+ self.locked = nil;
+ end
origin.send(st.reply(stanza));
- if dirty or whois_changed then
+ if next(changed) then
local msg = st.message({type='groupchat', from=self.jid})
- :tag('x', {xmlns='http://jabber.org/protocol/muc#user'});
-
- if dirty then
- msg.tags[1]:tag('status', {code = '104'}):up();
- end
- if whois_changed then
- local code = (whois == 'moderators') and "173" or "172";
+ :tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag('status', {code = '104'}):up();
+ if changed.whois then
+ local code = (self:get_whois() == 'moderators') and "173" or "172";
msg.tags[1]:tag('status', {code = code}):up();
end
- msg:up();
-
self:broadcast_message(msg, false)
end
end
@@ -791,6 +786,7 @@ function room_mt:destroy(newjid, reason, password)
end
self:set_persistent(false);
module:fire_event("muc-room-destroyed", { room = self });
+ return true;
end
function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
@@ -838,7 +834,8 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
local _aff = item.attr.affiliation;
local _rol = item.attr.role;
if _aff and not _rol then
- if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then
+ if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin")
+ or (affiliation and affiliation ~= "outcast" and self:get_members_only() and self:get_whois() == "anyone") then
local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
for jid, affiliation in pairs(self._affiliations) do
if affiliation == _aff then
@@ -904,7 +901,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
end
elseif stanza.name == "message" and type == "groupchat" then
- local from, to = stanza.attr.from, stanza.attr.to;
+ local from = stanza.attr.from;
local current_nick = self._jid_nick[from];
local occupant = self._occupants[current_nick];
if not occupant then -- not in room
@@ -914,11 +911,11 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
else
local from = stanza.attr.from;
stanza.attr.from = current_nick;
- local subject = getText(stanza, {"subject"});
+ local subject = stanza:get_child_text("subject");
if subject then
if occupant.role == "moderator" or
( self._data.changesubject and occupant.role == "participant" ) then -- and participant
- self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza
+ self:set_subject(current_nick, subject);
else
stanza.attr.from = from;
origin.send(st.error_reply(stanza, "auth", "forbidden"));
@@ -966,7 +963,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
:tag('body') -- Add a plain message for clients which don't support invites
:text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or ""))
:up();
- if self:is_members_only() and not self:get_affiliation(_invitee) then
+ if self:get_members_only() and not self:get_affiliation(_invitee) then
log("debug", "%s invited %s into members only room %s, granting membership", _from, _invitee, _to);
self:set_affiliation(_from, _invitee, "member", nil, "Invited by " .. self._jid_nick[_from])
end
@@ -1041,6 +1038,9 @@ function room_mt:set_affiliation(actor, jid, affiliation, callback, reason)
x:tag("status", {code="321"}):up(); -- affiliation change
end
end
+ -- Your own presence should have status 110
+ local self_x = st.clone(x);
+ self_x:tag("status", {code="110"});
local modified_nicks = {};
for nick, occupant in pairs(self._occupants) do
if jid_bare(occupant.jid) == jid then
@@ -1055,11 +1055,14 @@ function room_mt:set_affiliation(actor, jid, affiliation, callback, reason)
p.attr.from = nick;
p.attr.type = presence_type;
p.attr.to = jid;
- p:add_child(x);
- self:_route_stanza(p);
if occupant.jid == jid then
- modified_nicks[nick] = p;
+ -- Broadcast this presence to everyone else later, with the public <x> variant
+ local bp = st.clone(p);
+ bp:add_child(x);
+ modified_nicks[nick] = bp;
end
+ p:add_child(self_x);
+ self:_route_stanza(p);
end
end
end
@@ -1115,17 +1118,20 @@ function room_mt:set_role(actor, occupant_jid, role, callback, reason)
else
occupant.role = role;
end
+ local self_x = st.clone(x);
+ self_x:tag("status", {code = "110"}):up();
local bp;
for jid,pres in pairs(occupant.sessions) do -- send to all sessions of the nick
local p = st.clone(pres);
p.attr.from = occupant_jid;
p.attr.type = presence_type;
p.attr.to = jid;
- p:add_child(x);
- self:_route_stanza(p);
if occupant.jid == jid then
- bp = p;
+ bp = st.clone(p);
+ bp:add_child(x);
end
+ p:add_child(self_x);
+ self:_route_stanza(p);
end
if callback then callback(); end
if bp then
diff --git a/plugins/sql.lib.lua b/plugins/sql.lib.lua
deleted file mode 100644
index 005ee45d..00000000
--- a/plugins/sql.lib.lua
+++ /dev/null
@@ -1,9 +0,0 @@
-local cache = module:shared("/*/sql.lib/util.sql");
-
-if not cache._M then
- prosody.unlock_globals();
- cache._M = require "util.sql";
- prosody.lock_globals();
-end
-
-return cache._M;
diff --git a/plugins/storage/sqlbasic.lib.lua b/plugins/storage/sqlbasic.lib.lua
deleted file mode 100644
index ab3648f9..00000000
--- a/plugins/storage/sqlbasic.lib.lua
+++ /dev/null
@@ -1,97 +0,0 @@
-
--- Basic SQL driver
--- This driver stores data as simple key-values
-
-local ser = require "util.serialization".serialize;
-local envload = require "util.envload".envload;
-local deser = function(data)
- module:log("debug", "deser: %s", tostring(data));
- if not data then return nil; end
- local f = envload("return "..data, nil, {});
- if not f then return nil; end
- local s, d = pcall(f);
- if not s then return nil; end
- return d;
-end;
-
-local driver = {};
-driver.__index = driver;
-
-driver.item_table = "item";
-driver.list_table = "list";
-
-function driver:prepare(sql)
- module:log("debug", "query: %s", sql);
- local err;
- if not self.sqlcache then self.sqlcache = {}; end
- local r = self.sqlcache[sql];
- if r then return r; end
- r, err = self.connection:prepare(sql);
- if not r then error("Unable to prepare SQL statement: "..err); end
- self.sqlcache[sql] = r;
- return r;
-end
-
-function driver:load(username, host, datastore)
- local select = self:prepare("select data from "..self.item_table.." where username=? and host=? and datastore=?");
- select:execute(username, host, datastore);
- local row = select:fetch();
- return row and deser(row[1]) or nil;
-end
-
-function driver:store(username, host, datastore, data)
- if not data or next(data) == nil then
- local delete = self:prepare("delete from "..self.item_table.." where username=? and host=? and datastore=?");
- delete:execute(username, host, datastore);
- return true;
- else
- local d = self:load(username, host, datastore);
- if d then -- update
- local update = self:prepare("update "..self.item_table.." set data=? where username=? and host=? and datastore=?");
- return update:execute(ser(data), username, host, datastore);
- else -- insert
- local insert = self:prepare("insert into "..self.item_table.." values (?, ?, ?, ?)");
- return insert:execute(username, host, datastore, ser(data));
- end
- end
-end
-
-function driver:list_append(username, host, datastore, data)
- if not data then return; end
- local insert = self:prepare("insert into "..self.list_table.." values (?, ?, ?, ?)");
- return insert:execute(username, host, datastore, ser(data));
-end
-
-function driver:list_store(username, host, datastore, data)
- -- remove existing data
- local delete = self:prepare("delete from "..self.list_table.." where username=? and host=? and datastore=?");
- delete:execute(username, host, datastore);
- if data and next(data) ~= nil then
- -- add data
- for _, d in ipairs(data) do
- self:list_append(username, host, datastore, ser(d));
- end
- end
- return true;
-end
-
-function driver:list_load(username, host, datastore)
- local select = self:prepare("select data from "..self.list_table.." where username=? and host=? and datastore=?");
- select:execute(username, host, datastore);
- local r = {};
- for row in select:rows() do
- table.insert(r, deser(row[1]));
- end
- return r;
-end
-
-local _M = {};
-function _M.new(dbtype, dbname, ...)
- local d = {};
- setmetatable(d, driver);
- local dbh = get_database(dbtype, dbname, ...);
- --d:set_connection(dbh);
- d.connection = dbh;
- return d;
-end
-return _M;
diff --git a/plugins/storage/xep227store.lib.lua b/plugins/storage/xep227store.lib.lua
deleted file mode 100644
index 5ef8df54..00000000
--- a/plugins/storage/xep227store.lib.lua
+++ /dev/null
@@ -1,168 +0,0 @@
-
-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;