aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/mod_bosh.lua52
-rw-r--r--plugins/mod_compression.lua122
-rw-r--r--plugins/mod_console.lua100
-rw-r--r--plugins/mod_dialback.lua12
-rw-r--r--plugins/mod_disco.lua45
-rw-r--r--plugins/mod_httpserver.lua44
-rw-r--r--plugins/mod_lastactivity.lua52
-rw-r--r--plugins/mod_legacyauth.lua24
-rw-r--r--plugins/mod_muc.lua90
-rw-r--r--plugins/mod_offline.lua3
-rw-r--r--plugins/mod_pep.lua16
-rw-r--r--plugins/mod_ping.lua17
-rw-r--r--plugins/mod_posix.lua50
-rw-r--r--plugins/mod_presence.lua10
-rw-r--r--plugins/mod_register.lua33
-rw-r--r--plugins/mod_roster.lua2
-rw-r--r--plugins/mod_saslauth.lua65
-rw-r--r--plugins/mod_selftests.lua5
-rw-r--r--plugins/mod_time.lua40
-rw-r--r--plugins/mod_tls.lua66
-rw-r--r--plugins/mod_uptime.lua31
-rw-r--r--plugins/mod_vcard.lua95
-rw-r--r--plugins/mod_version.lua24
-rw-r--r--plugins/mod_watchregistrations.lua8
-rw-r--r--plugins/mod_welcome.lua4
-rw-r--r--plugins/mod_xmlrpc.lua2
-rw-r--r--plugins/muc/mod_muc.lua164
-rw-r--r--plugins/muc/muc.lib.lua735
28 files changed, 1578 insertions, 333 deletions
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua
index 743ebdef..af13bde9 100644
--- a/plugins/mod_bosh.lua
+++ b/plugins/mod_bosh.lua
@@ -6,7 +6,6 @@
-- COPYING file in the source package for more information.
--
-
module.host = "*" -- Global module
local hosts = _G.hosts;
@@ -22,17 +21,18 @@ local core_process_stanza = core_process_stanza;
local st = require "util.stanza";
local logger = require "util.logger";
local log = logger.init("mod_bosh");
-local stream_callbacks = { stream_tag = "http://jabber.org/protocol/httpbind|body" };
-local config = require "core.configmanager";
+
local xmlns_bosh = "http://jabber.org/protocol/httpbind"; -- (hard-coded into a literal in session.send)
+local stream_callbacks = { stream_tag = "http://jabber.org/protocol/httpbind\1body", default_ns = xmlns_bosh };
-local BOSH_DEFAULT_HOLD = tonumber(config.get("*", "core", "bosh_default_hold")) or 1;
-local BOSH_DEFAULT_INACTIVITY = tonumber(config.get("*", "core", "bosh_max_inactivity")) or 60;
-local BOSH_DEFAULT_POLLING = tonumber(config.get("*", "core", "bosh_max_polling")) or 5;
-local BOSH_DEFAULT_REQUESTS = tonumber(config.get("*", "core", "bosh_max_requests")) or 2;
-local BOSH_DEFAULT_MAXPAUSE = tonumber(config.get("*", "core", "bosh_max_pause")) or 300;
+local BOSH_DEFAULT_HOLD = tonumber(module:get_option("bosh_default_hold")) or 1;
+local BOSH_DEFAULT_INACTIVITY = tonumber(module:get_option("bosh_max_inactivity")) or 60;
+local BOSH_DEFAULT_POLLING = tonumber(module:get_option("bosh_max_polling")) or 5;
+local BOSH_DEFAULT_REQUESTS = tonumber(module:get_option("bosh_max_requests")) or 2;
+local BOSH_DEFAULT_MAXPAUSE = tonumber(module:get_option("bosh_max_pause")) or 300;
local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" };
+local session_close_reply = { headers = default_headers, body = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate" }), attr = {} };
local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
local os_time = os.time;
@@ -70,7 +70,7 @@ function handle_request(method, body, request)
--log("debug", "Handling new request %s: %s\n----------", request.id, tostring(body));
request.notopen = true;
request.log = log;
- local parser = lxp.new(init_xmlhandlers(request, stream_callbacks), "|");
+ local parser = lxp.new(init_xmlhandlers(request, stream_callbacks), "\1");
parser:parse(body);
@@ -112,11 +112,9 @@ end
local function bosh_reset_stream(session) session.notopen = true; end
-local session_close_reply = { headers = default_headers, body = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate" }), attr = {} };
local function bosh_close_stream(session, reason)
(session.log or log)("info", "BOSH client disconnected");
session_close_reply.attr.condition = reason;
- local session_close_reply = tostring(session_close_reply);
for _, held_request in ipairs(session.requests) do
held_request:send(session_close_reply);
held_request:destroy();
@@ -144,7 +142,7 @@ function stream_callbacks.streamopened(request, attr)
-- New session
sid = new_uuid();
- local session = { type = "c2s_unauthed", conn = {}, sid = sid, rid = attr.rid, host = attr.to, bosh_version = attr.ver, bosh_wait = attr.wait, streamid = sid,
+ local session = { type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid), host = attr.to, bosh_version = attr.ver, bosh_wait = attr.wait, streamid = sid,
bosh_hold = BOSH_DEFAULT_HOLD, bosh_max_inactive = BOSH_DEFAULT_INACTIVITY,
requests = { }, send_buffer = {}, reset_stream = bosh_reset_stream, close = bosh_close_stream,
dispatch_stanza = core_process_stanza, log = logger.init("bosh"..sid), secure = request.secure };
@@ -209,6 +207,21 @@ function stream_callbacks.streamopened(request, attr)
return;
end
+ if session.rid then
+ local rid = tonumber(attr.rid);
+ local diff = rid - session.rid;
+ if diff > 1 then
+ session.log("warn", "rid too large (means a request was lost). Last rid: %d New rid: %s", session.rid, attr.rid);
+ elseif diff <= 0 then
+ -- Repeated, ignore
+ session.log("debug", "rid repeated (on request %s), ignoring: %d", request.id, session.rid);
+ request.notopen = nil;
+ t_insert(session.requests, request);
+ return;
+ end
+ session.rid = rid;
+ end
+
if attr.type == "terminate" then
-- Client wants to end this session
session:close();
@@ -245,6 +258,7 @@ function stream_callbacks.handlestanza(request, stanza)
end
end
+local dead_sessions = {};
function on_timer()
-- log("debug", "Checking for requests soon to timeout...");
-- Identify requests timing out within the next few seconds
@@ -261,21 +275,29 @@ function on_timer()
end
now = now - 3;
+ local n_dead_sessions = 0;
for session, inactive_since in pairs(inactive_sessions) do
if session.bosh_max_inactive then
if now - inactive_since > session.bosh_max_inactive then
(session.log or log)("debug", "BOSH client inactive too long, destroying session at %d", now);
sessions[session.sid] = nil;
inactive_sessions[session] = nil;
- sm_destroy_session(session, "BOSH client silent for over "..session.bosh_max_inactive.." seconds");
+ n_dead_sessions = n_dead_sessions + 1;
+ dead_sessions[n_dead_sessions] = session;
end
else
inactive_sessions[session] = nil;
end
end
+
+ for i=1,n_dead_sessions do
+ local session = dead_sessions[i];
+ dead_sessions[i] = nil;
+ sm_destroy_session(session, "BOSH client silent for over "..session.bosh_max_inactive.." seconds");
+ end
end
-local ports = config.get(module.host, "core", "bosh_ports") or { 5280 };
-httpserver.new_from_config(ports, "http-bind", handle_request);
+local ports = module:get_option("bosh_ports") or { 5280 };
+httpserver.new_from_config(ports, handle_request, { base = "http-bind" });
server.addtimer(on_timer);
diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua
new file mode 100644
index 00000000..f1cae737
--- /dev/null
+++ b/plugins/mod_compression.lua
@@ -0,0 +1,122 @@
+-- Prosody IM
+-- Copyright (C) 2009 Tobias Markmann
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza";
+local zlib = require "zlib";
+local pcall = pcall;
+
+local xmlns_compression_feature = "http://jabber.org/features/compress"
+local xmlns_compression_protocol = "http://jabber.org/protocol/compress"
+local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up();
+
+local compression_level = module:get_option("compression_level");
+
+-- if not defined assume admin wants best compression
+if compression_level == nil then compression_level = 9 end;
+
+compression_level = tonumber(compression_level);
+if not compression_level or compression_level < 1 or compression_level > 9 then
+ module:log("warn", "Invalid compression level in config: %s", tostring(compression_level));
+ module:log("warn", "Module loading aborted. Compression won't be available.");
+ return;
+end
+
+module:add_event_hook("stream-features",
+ function (session, features)
+ if not session.compressed then
+ -- FIXME only advertise compression support when TLS layer has no compression enabled
+ features:add_child(compression_stream_feature);
+ end
+ end
+);
+
+-- TODO Support compression on S2S level too.
+module:add_handler({"c2s_unauthed", "c2s"}, "compress", xmlns_compression_protocol,
+ function(session, stanza)
+ -- fail if we are already compressed
+ if session.compressed then
+ local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method");
+ session.send(error_st);
+ session:log("warn", "Tried to establish another compression layer.");
+ end
+
+ -- checking if the compression method is supported
+ local method = stanza:child_with_name("method")[1];
+ if method == "zlib" then
+ session.log("info", method.." compression selected.");
+ session.send(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
+ session:reset_stream();
+
+ -- create deflate and inflate streams
+ 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.send(error_st);
+ session:log("error", "Failed to create zlib.deflate filter.");
+ module:log("error", deflate_stream);
+ return
+ end
+
+ 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.send(error_st);
+ session:log("error", "Failed to create zlib.deflate filter.");
+ module:log("error", inflate_stream);
+ return
+ end
+
+ -- setup compression for session.w
+ local old_send = session.send;
+
+ session.send = function(t)
+ local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
+ if status == false then
+ session:close({
+ condition = "undefined-condition";
+ text = compressed;
+ extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
+ });
+ module:log("warn", compressed);
+ return;
+ end
+ old_send(compressed);
+ end;
+
+ -- setup decompression for session.data
+ local function setup_decompression(session)
+ local old_data = session.data
+ session.data = function(conn, data)
+ local status, decompressed, eof = pcall(inflate_stream, data);
+ if status == false then
+ session:close({
+ condition = "undefined-condition";
+ text = decompressed;
+ extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
+ });
+ module:log("warn", decompressed);
+ return;
+ end
+ old_data(conn, decompressed);
+ end;
+ end
+ setup_decompression(session);
+
+ local session_reset_stream = session.reset_stream;
+ session.reset_stream = function(session)
+ session_reset_stream(session);
+ setup_decompression(session);
+ return true;
+ end;
+ session.compressed = true;
+ else
+ session.log("info", method.." compression selected. But we don't support it.");
+ local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method");
+ session.send(error_st);
+ end
+ end
+);
diff --git a/plugins/mod_console.lua b/plugins/mod_console.lua
index 367c46b8..5a092298 100644
--- a/plugins/mod_console.lua
+++ b/plugins/mod_console.lua
@@ -127,7 +127,11 @@ function console_listener.listener(conn, data)
end
function console_listener.disconnect(conn, err)
-
+ local session = sessions[conn];
+ if session then
+ session.disconnect();
+ sessions[conn] = nil;
+ end
end
connlisteners_register('console', console_listener);
@@ -170,6 +174,7 @@ function commands.help(session, data)
print [[s2s - Commands to manage sessions between this server and others]]
print [[module - Commands to load/reload/unload modules/plugins]]
print [[server - Uptime, version, shutting down, etc.]]
+ print [[config - Reloading the configuration, etc.]]
print [[console - Help regarding the console itself]]
elseif section == "c2s" then
print [[c2s:show(jid) - Show all client sessions with the specified JID (or all if no JID given)]]
@@ -183,10 +188,13 @@ function commands.help(session, data)
print [[module:load(module, host) - Load the specified module on the specified host (or all hosts if none given)]]
print [[module:reload(module, host) - The same, but unloads and loads the module (saving state if the module supports it)]]
print [[module:unload(module, host) - The same, but just unloads the module from memory]]
+ print [[module:list(host) - List the modules loaded on the specified host]]
elseif section == "server" then
print [[server:version() - Show the server's version number]]
print [[server:uptime() - Show how long the server has been running]]
--print [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]]
+ elseif section == "config" then
+ print [[config:reload() - Reload the server configuration. Modules may need to be reloaded for changes to take effect.]]
elseif section == "console" then
print [[Hey! Welcome to Prosody's admin console.]]
print [[First thing, if you're ever wondering how to get out, simply type 'quit'.]]
@@ -327,6 +335,35 @@ function def_env.module:reload(name, hosts)
return ok, (ok and "Module reloaded on "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
end
+function def_env.module:list(hosts)
+ if hosts == nil then
+ hosts = array.collect(keys(prosody.hosts));
+ end
+ if type(hosts) == "string" then
+ hosts = { hosts };
+ end
+ if type(hosts) ~= "table" then
+ return false, "Please supply a host or a list of hosts you would like to see";
+ end
+
+ local print = self.session.print;
+ for _, host in ipairs(hosts) do
+ print(host..":");
+ local modules = array.collect(keys(prosody.hosts[host] and prosody.hosts[host].modules or {})):sort();
+ if #modules == 0 then
+ if prosody.hosts[host] then
+ print(" No modules loaded");
+ else
+ print(" Host not found");
+ end
+ else
+ for _, name in ipairs(modules) do
+ print(" "..name);
+ end
+ end
+ end
+end
+
def_env.config = {};
function def_env.config:load(filename, format)
local config_load = require "core.configmanager".load;
@@ -373,7 +410,12 @@ end
function def_env.c2s:show(match_jid)
local print, count = self.session.print, 0;
+ local curr_host;
show_c2s(function (jid, session)
+ if curr_host ~= session.host then
+ curr_host = session.host;
+ print(curr_host);
+ end
if (not match_jid) or jid:match(match_jid) then
count = count + 1;
local status, priority = "unavailable", tostring(session.priority or "-");
@@ -385,7 +427,7 @@ function def_env.c2s:show(match_jid)
status = "available";
end
end
- print(jid.." - "..status.."("..priority..")");
+ print(" "..jid.." - "..status.."("..priority..")");
end
end);
return true, "Total: "..count.." clients";
@@ -436,7 +478,7 @@ function def_env.s2s:show(match_jid)
for remotehost, session in pairs(host_session.s2sout) do
if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then
count_out = count_out + 1;
- print(" "..host.." -> "..remotehost);
+ print(" "..host.." -> "..remotehost..(session.secure and " (encrypted)" or ""));
if session.sendq then
print(" There are "..#session.sendq.." queued outgoing stanzas for this connection");
end
@@ -464,12 +506,16 @@ function def_env.s2s:show(match_jid)
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))) then
+ or (session.from_host and session.from_host:match(match_jid))
+ -- Pft! is what I say to list comprehensions
+ or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then
count_in = count_in + 1;
- print(" "..host.." <- "..(session.from_host or "(unknown)"));
+ print(" "..host.." <- "..(session.from_host or "(unknown)")..(session.secure and " (encrypted)" or ""));
if session.type == "s2sin_unauthed" then
print(" Connection not yet authenticated");
end
@@ -510,7 +556,7 @@ function def_env.s2s:close(from, to)
if not session then
print("No outgoing connection from "..from.." to "..to)
else
- s2smanager.destroy_session(session);
+ (session.close or s2smanager.destroy_session)(session);
count = count + 1;
print("Closed outgoing session from "..from.." to "..to);
end
@@ -518,7 +564,7 @@ function def_env.s2s:close(from, to)
-- Is an incoming connection
for session in pairs(incoming_s2s) do
if session.to_host == to and session.from_host == from then
- s2smanager.destroy_session(session);
+ (session.close or s2smanager.destroy_session)(session);
count = count + 1;
end
end
@@ -537,6 +583,44 @@ function def_env.s2s:close(from, to)
return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s");
end
+def_env.host = {}; def_env.hosts = def_env.host;
+function def_env.host:activate(hostname, config)
+ local hostmanager_activate = require "core.hostmanager".activate;
+ if hosts[hostname] then
+ return false, "The host "..tostring(hostname).." is already activated";
+ end
+
+ local defined_hosts = config or configmanager.getconfig();
+ if not config and not defined_hosts[hostname] then
+ return false, "Couldn't find "..tostring(hostname).." defined in the config, perhaps you need to config:reload()?";
+ end
+ hostmanager_activate(hostname, config or defined_hosts[hostname]);
+ return true, "Host "..tostring(hostname).." activated";
+end
+
+function def_env.host:deactivate(hostname, reason)
+ local hostmanager_deactivate = require "core.hostmanager".deactivate;
+ local host = hosts[hostname];
+ if not host then
+ return false, "The host "..tostring(hostname).." is not activated";
+ end
+ if reason then
+ reason = { condition = "host-gone", text = reason };
+ end
+ hostmanager_deactivate(hostname, reason);
+ return true, "Host "..tostring(hostname).." deactivated";
+end
+
+function def_env.host:list()
+ local print = self.session.print;
+ local i = 0;
+ for host in values(array.collect(keys(prosody.hosts)):sort()) do
+ i = i + 1;
+ print(host);
+ end
+ return true, i.." hosts";
+end
+
-------------
function printbanner(session)
diff --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua
index 5c956103..469044cd 100644
--- a/plugins/mod_dialback.lua
+++ b/plugins/mod_dialback.lua
@@ -10,6 +10,7 @@
local hosts = _G.hosts;
local send_s2s = require "core.s2smanager".send_to_host;
local s2s_make_authenticated = require "core.s2smanager".make_authenticated;
+local s2s_initiate_dialback = require "core.s2smanager".initiate_dialback;
local s2s_verify_dialback = require "core.s2smanager".verify_dialback;
local s2s_destroy_session = require "core.s2smanager".destroy_session;
@@ -17,6 +18,7 @@ local log = module._log;
local st = require "util.stanza";
+local xmlns_stream = "http://etherx.jabber.org/streams";
local xmlns_dialback = "jabber:server:dialback";
local dialback_requests = setmetatable({}, { __mode = 'v' });
@@ -113,3 +115,13 @@ module:add_handler({ "s2sout_unauthed", "s2sout" }, "result", xmlns_dialback,
s2s_destroy_session(origin)
end
end);
+
+module:hook_stanza(xmlns_stream, "features", function (origin, stanza)
+ s2s_initiate_dialback(origin);
+ return true;
+ end, 100);
+
+-- Offer dialback to incoming hosts
+module:hook("s2s-stream-features", function (data)
+ data.features:tag("dialback", { xmlns='urn:xmpp:features:dialback' }):tag("optional"):up():up();
+ end);
diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua
index 00ea01d8..06b29f0e 100644
--- a/plugins/mod_disco.lua
+++ b/plugins/mod_disco.lua
@@ -6,16 +6,47 @@
-- COPYING file in the source package for more information.
--
+local componentmanager_get_children = require "core.componentmanager".get_children;
+local st = require "util.stanza"
-
-local discomanager_handle = require "core.discomanager".handle;
-
+module:add_identity("server", "im", "Prosody"); -- FIXME should be in the non-existing mod_router
module:add_feature("http://jabber.org/protocol/disco#info");
module:add_feature("http://jabber.org/protocol/disco#items");
-module:add_iq_handler({"c2s", "s2sin"}, "http://jabber.org/protocol/disco#info", function (session, stanza)
- session.send(discomanager_handle(stanza));
+module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type ~= "get" then return; end
+ local node = stanza.tags[1].attr.node;
+ if node and node ~= "" then return; end -- TODO fire event?
+
+ local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info");
+ local done = {};
+ for _,identity in ipairs(module:get_host_items("identity")) do
+ local identity_s = identity.category.."\0"..identity.type;
+ if not done[identity_s] then
+ reply:tag("identity", identity):up();
+ done[identity_s] = true;
+ end
+ end
+ for _,feature in ipairs(module:get_host_items("feature")) do
+ if not done[feature] then
+ reply:tag("feature", {var=feature}):up();
+ done[feature] = true;
+ end
+ end
+ origin.send(reply);
+ return true;
end);
-module:add_iq_handler({"c2s", "s2sin"}, "http://jabber.org/protocol/disco#items", function (session, stanza)
- session.send(discomanager_handle(stanza));
+module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type ~= "get" then return; end
+ local node = stanza.tags[1].attr.node;
+ if node and node ~= "" then return; end -- TODO fire event?
+
+ local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
+ for jid in pairs(componentmanager_get_children(module.host)) do
+ reply:tag("item", {jid = jid}):up();
+ end
+ origin.send(reply);
+ return true;
end);
diff --git a/plugins/mod_httpserver.lua b/plugins/mod_httpserver.lua
index a8639281..545d4faf 100644
--- a/plugins/mod_httpserver.lua
+++ b/plugins/mod_httpserver.lua
@@ -12,20 +12,50 @@ local httpserver = require "net.httpserver";
local open = io.open;
local t_concat = table.concat;
-local http_base = "www_files";
+local http_base = config.get("*", "core", "http_path") or "www_files";
+local response_400 = { status = "400 Bad Request", body = "<h1>Bad Request</h1>Sorry, we didn't understand your request :(" };
local response_404 = { status = "404 Not Found", body = "<h1>Page Not Found</h1>Sorry, we couldn't find what you were looking for :(" };
-local http_path = { http_base };
-local function handle_request(method, body, request)
- local path = request.url.path:gsub("%.%.%/", ""):gsub("^/[^/]+", "");
- http_path[2] = path;
- local f, err = open(t_concat(http_path), "r");
+local function preprocess_path(path)
+ if path:sub(1,1) ~= "/" then
+ path = "/"..path;
+ end
+ local level = 0;
+ for component in path:gmatch("([^/]+)/") do
+ if component == ".." then
+ level = level - 1;
+ elseif component ~= "." then
+ level = level + 1;
+ end
+ if level < 0 then
+ return nil;
+ end
+ end
+ return path;
+end
+
+function serve_file(path)
+ local f, err = open(http_base..path, "r");
if not f then return response_404; end
local data = f:read("*a");
f:close();
return data;
end
+local function handle_file_request(method, body, request)
+ local path = preprocess_path(request.url.path);
+ if not path then return response_400; end
+ path = path:gsub("^/[^/]+", ""); -- Strip /files/
+ return serve_file(path);
+end
+
+local function handle_default_request(method, body, request)
+ local path = preprocess_path(request.url.path);
+ if not path then return response_400; end
+ return serve_file(path);
+end
+
local ports = config.get(module.host, "core", "http_ports") or { 5280 };
-httpserver.new_from_config(ports, "files", handle_request);
+httpserver.set_default_handler(handle_default_request);
+httpserver.new_from_config(ports, handle_file_request, { base = "files" });
diff --git a/plugins/mod_lastactivity.lua b/plugins/mod_lastactivity.lua
new file mode 100644
index 00000000..a0da9829
--- /dev/null
+++ b/plugins/mod_lastactivity.lua
@@ -0,0 +1,52 @@
+-- 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 st = require "util.stanza";
+local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
+local jid_bare = require "util.jid".bare;
+local jid_split = require "util.jid".split;
+
+module:add_feature("jabber:iq:last");
+
+local map = {};
+
+module:hook("pre-presence/bare", function(event)
+ local stanza = event.stanza;
+ if not(stanza.attr.to) and stanza.attr.type == "unavailable" then
+ local t = os.time();
+ local s = stanza:child_with_name("status");
+ s = s and #s.tags == 0 and s[1] or "";
+ map[event.origin.username] = {s = s, t = t};
+ end
+end, 10);
+
+module:hook("iq/bare/jabber:iq:last:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type == "get" then
+ local username = jid_split(stanza.attr.to) or origin.username;
+ if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+ local seconds, text = "0", "";
+ if map[username] then
+ seconds = tostring(os.difftime(os.time(), map[username].t));
+ text = map[username].s;
+ end
+ origin.send(st.reply(stanza):tag('query', {xmlns='jabber:iq:last', seconds=seconds}):text(text));
+ else
+ origin.send(st.error_reply(stanza, 'auth', 'forbidden'));
+ end
+ return true;
+ end
+end);
+
+module.save = function()
+ return {map = map};
+end
+module.restore = function(data)
+ map = data.map or {};
+end
+
diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua
index de94411e..c678dce1 100644
--- a/plugins/mod_legacyauth.lua
+++ b/plugins/mod_legacyauth.lua
@@ -11,11 +11,12 @@
local st = require "util.stanza";
local t_concat = table.concat;
-local config = require "core.configmanager";
-local secure_auth_only = config.get(module:get_host(), "core", "require_encryption");
+local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
local sessionmanager = require "core.sessionmanager";
local usermanager = require "core.usermanager";
+local nodeprep = require "util.encodings".stringprep.nodeprep;
+local resourceprep = require "util.encodings".stringprep.resourceprep;
module:add_feature("jabber:iq:auth");
module:add_event_hook("stream-features", function (session, features)
@@ -43,11 +44,11 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:auth",
:tag("username"):up()
:tag("password"):up()
:tag("resource"):up());
- return true;
else
username, password, resource = t_concat(username), t_concat(password), t_concat(resource);
+ username = nodeprep(username);
+ resource = resourceprep(resource)
local reply = st.reply(stanza);
- require "core.usermanager"
if usermanager.validate_credentials(session.host, username, password) then
-- Authentication successful!
local success, err = sessionmanager.make_authenticated(session, username);
@@ -56,19 +57,18 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:auth",
success, err_type, err, err_msg = sessionmanager.bind_resource(session, resource);
if not success then
session.send(st.error_reply(stanza, err_type, err, err_msg));
+ session.username, session.type = nil, "c2s_unauthed"; -- FIXME should this be placed in sessionmanager?
+ return true;
+ elseif resource ~= session.resource then -- server changed resource, not supported by legacy auth
+ session.send(st.error_reply(stanza, "cancel", "conflict", "The requested resource could not be assigned to this session."));
+ session:close(); -- FIXME undo resource bind and auth instead of closing the session?
return true;
end
end
session.send(st.reply(stanza));
- return true;
else
- local reply = st.reply(stanza);
- reply.attr.type = "error";
- reply:tag("error", { code = "401", type = "auth" })
- :tag("not-authorized", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" });
- session.send(reply);
- return true;
+ session.send(st.error_reply(stanza, "auth", "not-authorized"));
end
end
-
+ return true;
end);
diff --git a/plugins/mod_muc.lua b/plugins/mod_muc.lua
deleted file mode 100644
index b38468ea..00000000
--- a/plugins/mod_muc.lua
+++ /dev/null
@@ -1,90 +0,0 @@
--- 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.
---
-
-
-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 = "Chatrooms";
-local history_length = 20;
-
-local muc_new_room = require "util.muc".new_room;
-local register_component = require "core.componentmanager".register_component;
-local deregister_component = require "core.componentmanager".deregister_component;
-local jid_split = require "util.jid".split;
-local st = require "util.stanza";
-
-local rooms = {};
-local component;
-local host_room = muc_new_room(muc_host);
-host_room.route_stanza = function(room, stanza) core_post_stanza(component, stanza); end;
-
-local function get_disco_info(stanza)
- return st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info")
- :tag("identity", {category='conference', type='text', name=muc_name}):up()
- :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply
-end
-local function get_disco_items(stanza)
- local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
- for jid, room in pairs(rooms) do
- reply:tag("item", {jid=jid, name=jid}):up();
- end
- return reply; -- TODO cache disco reply
-end
-
-local function handle_to_domain(origin, stanza)
- local type = stanza.attr.type;
- if type == "error" or type == "result" then return; end
- if stanza.name == "iq" and type == "get" then
- local xmlns = stanza.tags[1].attr.xmlns;
- if xmlns == "http://jabber.org/protocol/disco#info" then
- origin.send(get_disco_info(stanza));
- elseif xmlns == "http://jabber.org/protocol/disco#items" then
- origin.send(get_disco_items(stanza));
- else
- origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc
- end
- else
- host_room:handle_stanza(origin, stanza);
- --origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it"));
- end
-end
-
-component = register_component(muc_host, function(origin, stanza)
- local to_node, to_host, to_resource = jid_split(stanza.attr.to);
- if to_node then
- local bare = to_node.."@"..to_host;
- if to_host == muc_host or bare == muc_host then
- local room = rooms[bare];
- if not room then
- room = muc_new_room(bare);
- room.route_stanza = function(room, stanza) core_post_stanza(component, stanza); end;
- rooms[bare] = room;
- end
- room:handle_stanza(origin, stanza);
- else --[[not for us?]] end
- return;
- end
- -- to the main muc domain
- handle_to_domain(origin, stanza);
-end);
-
-prosody.hosts[module:get_host()].muc = { rooms = rooms };
-
-module.unload = function()
- deregister_component(muc_host);
-end
-module.save = function()
- return {rooms = rooms};
-end
-module.restore = function(data)
- rooms = data.rooms or {};
- prosody.hosts[module:get_host()].muc = { rooms = rooms };
-end
diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua
index c9acf9e6..c74d011e 100644
--- a/plugins/mod_offline.lua
+++ b/plugins/mod_offline.lua
@@ -10,7 +10,8 @@
local datamanager = require "util.datamanager";
local st = require "util.stanza";
local datetime = require "util.datetime";
-local ipairs = ipairs;
+local ipairs = ipairs;
+local jid_split = require "util.jid".split;
module:add_feature("msgoffline");
diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua
index e07759f0..bfe22867 100644
--- a/plugins/mod_pep.lua
+++ b/plugins/mod_pep.lua
@@ -25,10 +25,20 @@ local data = {};
local recipients = {};
local hash_map = {};
-module:add_identity("pubsub", "pep");
+module.save = function()
+ return { data = data, recipients = recipients, hash_map = hash_map };
+end
+module.restore = function(state)
+ data = state.data or {};
+ recipients = state.recipients or {};
+ hash_map = state.hash_map or {};
+end
+
+module:add_identity("pubsub", "pep", "Prosody");
module:add_feature("http://jabber.org/protocol/pubsub#publish");
local function publish(session, node, item)
+ item.attr.xmlns = nil;
local disable = #item.tags ~= 1 or #item.tags[1].tags == 0;
if #item.tags == 0 then item.name = "retract"; end
local bare = session.username..'@'..session.host;
@@ -132,9 +142,9 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event)
if payload and (payload.name == 'publish' or payload.name == 'retract') and payload.attr.node then -- <publish node='http://jabber.org/protocol/tune'>
local node = payload.attr.node;
payload = payload.tags[1];
- if payload then -- <item>
- publish(session, node, payload);
+ if payload and payload.name == "item" then -- <item>
session.send(st.reply(stanza));
+ publish(session, node, st.clone(payload));
return true;
end
end
diff --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua
index e0324c80..1dc9fbec 100644
--- a/plugins/mod_ping.lua
+++ b/plugins/mod_ping.lua
@@ -6,15 +6,16 @@
-- COPYING file in the source package for more information.
--
-
-
local st = require "util.stanza";
module:add_feature("urn:xmpp:ping");
-module:add_iq_handler({"c2s", "s2sin"}, "urn:xmpp:ping",
- function(session, stanza)
- if stanza.attr.type == "get" then
- session.send(st.reply(stanza));
- end
- end);
+local function ping_handler(event)
+ if event.stanza.attr.type == "get" then
+ event.origin.send(st.reply(event.stanza));
+ return true;
+ end
+end
+
+module:hook("iq/bare/urn:xmpp:ping:ping", ping_handler);
+module:hook("iq/host/urn:xmpp:ping:ping", ping_handler);
diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua
index 0f46888d..b75b9610 100644
--- a/plugins/mod_posix.lua
+++ b/plugins/mod_posix.lua
@@ -17,19 +17,45 @@ if type(signal) == "string" then
module:log("warn", "Couldn't load signal library, won't respond to SIGTERM");
end
-local config_get = require "core.configmanager".get;
local logger_set = require "util.logger".setwriter;
local prosody = _G.prosody;
module.host = "*"; -- we're a global module
+-- Allow switching away from root, some people like strange ports.
+module:add_event_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 "..gid.." successfully.");
+ else
+ module:log("error", "Failed to change group to "..gid..". Error: "..msg);
+ prosody.shutdown("Failed to change group to "..gid);
+ end
+ end
+ if uid then
+ local success, msg = pposix.setuid(uid);
+ if success then
+ module:log("debug", "Changed user to "..uid.." successfully.");
+ else
+ module:log("error", "Failed to change user to "..uid..". Error: "..msg);
+ prosody.shutdown("Failed to change user to "..uid);
+ end
+ end
+ end);
+
-- Don't even think about it!
module:add_event_hook("server-starting", function ()
- if pposix.getuid() == 0 and not config_get("*", "core", "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");
+ local suid = module:get_option("setuid");
+ if not suid or suid == 0 or suid == "root" then
+ if pposix.getuid() == 0 and not module:get_option("run_as_root") then
+ module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!");
+ module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root");
+ prosody.shutdown("Refusing to run as root");
+ end
end
end);
@@ -46,7 +72,7 @@ local function write_pidfile()
if pidfile_written then
remove_pidfile();
end
- local pidfile = config_get("*", "core", "pidfile");
+ local pidfile = module:get_option("pidfile");
if pidfile then
local pf, err = io.open(pidfile, "w+");
if not pf then
@@ -76,7 +102,17 @@ function syslog_sink_maker(config)
end
require "core.loggingmanager".register_sink_type("syslog", syslog_sink_maker);
-if not config_get("*", "core", "no_daemonize") then
+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
+
+if daemonize then
local function daemonize_server()
local ok, ret = pposix.daemonize();
if not ok then
diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua
index 02ec6f79..f83e017b 100644
--- a/plugins/mod_presence.lua
+++ b/plugins/mod_presence.lua
@@ -29,7 +29,7 @@ function core_route_stanza(origin, stanza)
if stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" and stanza.attr.type ~= "error" then
local node, host = jid_split(stanza.attr.to);
host = hosts[host];
- if host and host.type == "local" then
+ if node and host and host.type == "local" then
handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza);
return;
end
@@ -142,7 +142,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
stanza.attr.to = nil; -- reset it
end
-function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza)
+function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza, stanza)
local h = hosts[host];
local count = 0;
if h and h.type == "local" then
@@ -151,6 +151,7 @@ function send_presence_of_available_resources(user, host, jid, recipient_session
for k, session in pairs(u.sessions) do
local pres = session.presence;
if pres then
+ if stanza then pres = stanza; pres.attr.from = session.full_jid; end
pres.attr.to = jid;
core_route_stanza(session, pres);
pres.attr.to = nil;
@@ -165,7 +166,7 @@ end
function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza)
local node, host = jid_split(from_bare);
- if node == origin.username and host == origin.host then return; end -- No self contacts
+ if to_bare == origin.username.."@"..origin.host then return; end -- No self contacts
local st_from, st_to = stanza.attr.from, stanza.attr.to;
stanza.attr.from, stanza.attr.to = from_bare, to_bare;
log("debug", "outbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);
@@ -199,6 +200,9 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_
rostermanager.roster_push(node, host, to_bare);
end
core_route_stanza(origin, stanza);
+ -- COMPAT: Some legacy clients keep displaying unsubscribed contacts as online unless an unavailable presence is sent:
+ send_presence_of_available_resources(node, host, to_bare, origin, core_route_stanza,
+ st.presence({ type="unavailable", from=from_bare, to=to_bare, id=stanza.attr.id }));
end
stanza.attr.from, stanza.attr.to = st_from, st_to;
end
diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua
index 383ab811..22724130 100644
--- a/plugins/mod_register.lua
+++ b/plugins/mod_register.lua
@@ -9,7 +9,6 @@
local hosts = _G.hosts;
local st = require "util.stanza";
-local config = require "core.configmanager";
local datamanager = require "util.datamanager";
local usermanager_user_exists = require "core.usermanager".user_exists;
local usermanager_create_user = require "core.usermanager".create_user;
@@ -90,16 +89,16 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza)
end);
local recent_ips = {};
-local min_seconds_between_registrations = config.get(module.host, "core", "min_seconds_between_registrations");
-local whitelist_only = config.get(module.host, "core", "whitelist_registration_only");
-local whitelisted_ips = config.get(module.host, "core", "registration_whitelist") or { "127.0.0.1" };
-local blacklisted_ips = config.get(module.host, "core", "registration_blacklist") or {};
+local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations");
+local whitelist_only = module:get_option("whitelist_registration_only");
+local whitelisted_ips = module:get_option("registration_whitelist") or { "127.0.0.1" };
+local blacklisted_ips = module:get_option("registration_blacklist") or {};
for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end
for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end
module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, stanza)
- if config.get(module.host, "core", "allow_registration") == false then
+ if module:get_option("allow_registration") == false then
session.send(st.error_reply(stanza, "cancel", "service-unavailable"));
elseif stanza.tags[1].name == "query" then
local query = stanza.tags[1];
@@ -119,19 +118,18 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s
if username and password then
-- Check that the user is not blacklisted or registering too often
if blacklisted_ips[session.ip] or (whitelist_only and not whitelisted_ips[session.ip]) then
- session.send(st.error_reply(stanza, "cancel", "not-acceptable"));
- return;
+ session.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not allowed to register an account."));
+ return;
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, "cancel", "not-acceptable"));
+ session.send(st.error_reply(stanza, "wait", "not-acceptable"));
return;
end
ip.time = os_time();
@@ -140,18 +138,21 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s
-- FIXME shouldn't use table.concat
username = nodeprep(table.concat(username));
password = table.concat(password);
- if usermanager_user_exists(username, session.host) then
- session.send(st.error_reply(stanza, "cancel", "conflict"));
+ local host = module.host;
+ if not username then
+ session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is invalid."));
+ elseif usermanager_user_exists(username, host) then
+ session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists."));
else
- if usermanager_create_user(username, password, session.host) then
+ if usermanager_create_user(username, password, host) then
session.send(st.reply(stanza)); -- user created!
- module:log("info", "User account created: %s@%s", username, session.host);
+ module:log("info", "User account created: %s@%s", username, host);
module:fire_event("user-registered", {
- username = username, host = session.host, source = "mod_register",
+ username = username, host = host, source = "mod_register",
session = session });
else
-- TODO unable to write file, file may be locked, etc, what's the correct error?
- session.send(st.error_reply(stanza, "wait", "internal-server-error"));
+ session.send(st.error_reply(stanza, "wait", "internal-server-error", "Failed to write data to disk."));
end
end
else
diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua
index 8f25ed64..7ca22aa1 100644
--- a/plugins/mod_roster.lua
+++ b/plugins/mod_roster.lua
@@ -24,7 +24,7 @@ module:add_feature("jabber:iq:roster");
local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"}):tag("optional"):up();
module:add_event_hook("stream-features",
- function (session, features)
+ function (session, features)
if session.username then
features:add_child(rosterver_stream_feature);
end
diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua
index ec3857b8..641b08f0 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -13,6 +13,7 @@ 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 nodeprep = require "util.encodings".stringprep.nodeprep;
local datamanager_load = require "util.datamanager".load;
local usermanager_validate_credentials = require "core.usermanager".validate_credentials;
local usermanager_get_supported_methods = require "core.usermanager".get_supported_methods;
@@ -24,7 +25,7 @@ local jid_split = require "util.jid".split
local md5 = require "util.hashes".md5;
local config = require "core.configmanager";
-local secure_auth_only = config.get(module:get_host(), "core", "require_encryption");
+local secure_auth_only = config.get(module:get_host(), "core", "c2s_require_encryption") or config.get(module:get_host(), "core", "require_encryption");
local log = module._log;
@@ -36,10 +37,25 @@ local new_sasl = require "util.sasl".new;
default_authentication_profile = {
plain = function(username, realm)
- return usermanager_get_password(username, realm), true;
+ local prepped_username = nodeprep(username);
+ if not prepped_username then
+ log("debug", "NODEprep failed on username: %s", username);
+ return "", nil;
+ end
+ local password = usermanager_get_password(prepped_username, realm);
+ if not password then
+ return "", nil;
+ end
+ return password, true;
end
};
+anonymous_authentication_profile = {
+ anonymous = function(username, realm)
+ return true; -- for normal usage you should always return true here
+ end
+}
+
local function build_reply(status, ret, err_msg)
local reply = st.stanza(status, {xmlns = xmlns_sasl});
if status == "challenge" then
@@ -61,7 +77,8 @@ local function handle_status(session, status)
if status == "failure" then
session.sasl_handler = nil;
elseif status == "success" then
- if not session.sasl_handler.username then -- TODO move this to sessionmanager
+ local username = nodeprep(session.sasl_handler.username);
+ if not username then -- TODO move this to sessionmanager
module:log("warn", "SASL succeeded but we didn't get a username!");
session.sasl_handler = nil;
session:reset_stream();
@@ -73,30 +90,6 @@ local function handle_status(session, status)
end
end
-local function credentials_callback(mechanism, ...)
- if mechanism == "PLAIN" then
- local username, hostname, password = ...;
- local response = usermanager_validate_credentials(hostname, username, password, mechanism);
- if response == nil then
- return false;
- else
- return response;
- end
- elseif mechanism == "DIGEST-MD5" then
- function func(x) return x; end
- local node, domain, realm, decoder = ...;
- local password = usermanager_get_password(node, domain);
- if password then
- if decoder then
- node, realm, password = decoder(node), decoder(realm), decoder(password);
- end
- return func, md5(node..":"..realm..":"..password);
- else
- return func, nil;
- end
- end
-end
-
local function sasl_handler(session, stanza)
if stanza.name == "auth" then
-- FIXME ignoring duplicates because ejabberd does
@@ -144,20 +137,20 @@ module:add_event_hook("stream-features",
if secure_auth_only and not session.secure then
return;
end
- session.sasl_handler = new_sasl(session.host, default_authentication_profile);
+ if config.get(session.host or "*", "core", "anonymous_login") then
+ session.sasl_handler = new_sasl(session.host, anonymous_authentication_profile);
+ else
+ session.sasl_handler = new_sasl(session.host, default_authentication_profile);
+ end
features:tag("mechanisms", mechanisms_attr);
-- TODO: Provide PLAIN only if TLS is active, this is a SHOULD from the introduction of RFC 4616. This behavior could be overridden via configuration but will issuing a warning or so.
- if config.get(session.host or "*", "core", "anonymous_login") then
- features:tag("mechanism"):text("ANONYMOUS"):up();
- else
- for k, v in pairs(session.sasl_handler:mechanisms()) do
- features:tag("mechanism"):text(v):up();
- end
- end
+ for k, v in pairs(session.sasl_handler:mechanisms()) do
+ features:tag("mechanism"):text(v):up();
+ end
features:up();
else
features:tag("bind", bind_attr):tag("required"):up():up();
- features:tag("session", xmpp_session_attr):up();
+ features:tag("session", xmpp_session_attr):tag("optional"):up():up();
end
end);
diff --git a/plugins/mod_selftests.lua b/plugins/mod_selftests.lua
index 6a26dfc3..1f413634 100644
--- a/plugins/mod_selftests.lua
+++ b/plugins/mod_selftests.lua
@@ -6,14 +6,13 @@
-- COPYING file in the source package for more information.
--
-
+module.host = "*" -- Global module
local st = require "util.stanza";
local register_component = require "core.componentmanager".register_component;
local core_route_stanza = core_route_stanza;
local socket = require "socket";
-local config = require "core.configmanager";
-local ping_hosts = config.get("*", "mod_selftests", "ping_hosts") or { "coversant.interop.xmpp.org", "djabberd.interop.xmpp.org", "djabberd-trunk.interop.xmpp.org", "ejabberd.interop.xmpp.org", "openfire.interop.xmpp.org" };
+local ping_hosts = module:get_option("ping_hosts") or { "coversant.interop.xmpp.org", "djabberd.interop.xmpp.org", "djabberd-trunk.interop.xmpp.org", "ejabberd.interop.xmpp.org", "openfire.interop.xmpp.org" };
local open_pings = {};
diff --git a/plugins/mod_time.lua b/plugins/mod_time.lua
index 26088396..7d900ae9 100644
--- a/plugins/mod_time.lua
+++ b/plugins/mod_time.lua
@@ -6,8 +6,6 @@
-- COPYING file in the source package for more information.
--
-
-
local st = require "util.stanza";
local datetime = require "util.datetime".datetime;
local legacy = require "util.datetime".legacy;
@@ -16,23 +14,31 @@ local legacy = require "util.datetime".legacy;
module:add_feature("urn:xmpp:time");
-module:add_iq_handler({"c2s", "s2sin"}, "urn:xmpp:time",
- function(session, stanza)
- if stanza.attr.type == "get" then
- session.send(st.reply(stanza):tag("time", {xmlns="urn:xmpp:time"})
- :tag("tzo"):text("+00:00"):up() -- FIXME get the timezone in a platform independent fashion
- :tag("utc"):text(datetime()));
- end
- end);
+local function time_handler(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type == "get" then
+ origin.send(st.reply(stanza):tag("time", {xmlns="urn:xmpp:time"})
+ :tag("tzo"):text("+00:00"):up() -- TODO get the timezone in a platform independent fashion
+ :tag("utc"):text(datetime()));
+ return true;
+ end
+end
+
+module:hook("iq/bare/urn:xmpp:time:time", time_handler);
+module:hook("iq/host/urn:xmpp:time:time", time_handler);
-- XEP-0090: Entity Time (deprecated)
module:add_feature("jabber:iq:time");
-module:add_iq_handler({"c2s", "s2sin"}, "jabber:iq:time",
- function(session, stanza)
- if stanza.attr.type == "get" then
- session.send(st.reply(stanza):tag("query", {xmlns="jabber:iq:time"})
- :tag("utc"):text(legacy()));
- end
- end);
+local function legacy_time_handler(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type == "get" then
+ origin.send(st.reply(stanza):tag("query", {xmlns="jabber:iq:time"})
+ :tag("utc"):text(legacy()));
+ return true;
+ end
+end
+
+module:hook("iq/bare/jabber:iq:time:query", legacy_time_handler);
+module:hook("iq/host/jabber:iq:time:query", legacy_time_handler);
diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua
index 8926edfc..8a450803 100644
--- a/plugins/mod_tls.lua
+++ b/plugins/mod_tls.lua
@@ -6,20 +6,22 @@
-- COPYING file in the source package for more information.
--
-
-
local st = require "util.stanza";
-local xmlns_starttls ='urn:ietf:params:xml:ns:xmpp-tls';
+local xmlns_stream = 'http://etherx.jabber.org/streams';
+local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls';
-local config = require "core.configmanager";
-local secure_auth_only = config.get("*", "core", "require_encryption");
+local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
+local secure_s2s_only = module:get_option("s2s_require_encryption");
module:add_handler("c2s_unauthed", "starttls", xmlns_starttls,
function (session, stanza)
if session.conn.starttls then
session.send(st.stanza("proceed", { xmlns = xmlns_starttls }));
session:reset_stream();
+ if session.host and hosts[session.host].ssl_ctx_in then
+ session.conn.set_sslctx(hosts[session.host].ssl_ctx_in);
+ end
session.conn.starttls();
session.log("info", "TLS negotiation started...");
session.secure = false;
@@ -29,9 +31,27 @@ module:add_handler("c2s_unauthed", "starttls", xmlns_starttls,
end
end);
+module:add_handler("s2sin_unauthed", "starttls", xmlns_starttls,
+ function (session, stanza)
+ if session.conn.starttls then
+ session.sends2s(st.stanza("proceed", { xmlns = xmlns_starttls }));
+ session:reset_stream();
+ if session.to_host and hosts[session.to_host].ssl_ctx_in then
+ session.conn.set_sslctx(hosts[session.to_host].ssl_ctx_in);
+ end
+ session.conn.starttls();
+ session.log("info", "TLS negotiation started for incoming s2s...");
+ session.secure = false;
+ else
+ -- FIXME: What reply?
+ session.log("warn", "Attempt to start TLS, but TLS is not available on this s2s connection");
+ end
+ end);
+
+
local starttls_attr = { xmlns = xmlns_starttls };
module:add_event_hook("stream-features",
- function (session, features)
+ function (session, features)
if session.conn.starttls then
features:tag("starttls", starttls_attr);
if secure_auth_only then
@@ -41,3 +61,37 @@ module:add_event_hook("stream-features",
end
end
end);
+
+module:hook("s2s-stream-features",
+ function (data)
+ local session, features = data.session, data.features;
+ if session.to_host and session.conn.starttls then
+ features:tag("starttls", starttls_attr):up();
+ if secure_s2s_only then
+ features:tag("required"):up():up();
+ else
+ features:up();
+ end
+ end
+ end);
+
+-- For s2sout connections, start TLS if we can
+module:hook_stanza(xmlns_stream, "features",
+ function (session, stanza)
+ module:log("debug", "Received features element");
+ if session.conn.starttls and stanza:child_with_ns(xmlns_starttls) then
+ module:log("%s is offering TLS, taking up the offer...", session.to_host);
+ session.sends2s("<starttls xmlns='"..xmlns_starttls.."'/>");
+ return true;
+ end
+ end, 500);
+
+module:hook_stanza(xmlns_starttls, "proceed",
+ function (session, stanza)
+ module:log("debug", "Proceeding with TLS on s2sout...");
+ local format, to_host, from_host = string.format, session.to_host, session.from_host;
+ session:reset_stream();
+ session.conn.starttls(true);
+ session.secure = false;
+ return true;
+ end);
diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua
index eb0ca7cc..cf6c6b64 100644
--- a/plugins/mod_uptime.lua
+++ b/plugins/mod_uptime.lua
@@ -6,30 +6,17 @@
-- COPYING file in the source package for more information.
--
-
-
-local st = require "util.stanza"
-
-local jid_split = require "util.jid".split;
-local t_concat = table.concat;
+local st = require "util.stanza";
local start_time = prosody.start_time;
-
-prosody.events.add_handler("server-started", function () start_time = prosody.start_time end);
+prosody.events.add_handler("server-started", function() start_time = prosody.start_time end);
module:add_feature("jabber:iq:last");
-module:add_iq_handler({"c2s", "s2sin"}, "jabber:iq:last",
- function (origin, stanza)
- if stanza.tags[1].name == "query" then
- if stanza.attr.type == "get" then
- local node, host, resource = jid_split(stanza.attr.to);
- if node or resource then
- -- TODO
- else
- origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:last", seconds = tostring(os.difftime(os.time(), start_time))}));
- return true;
- end
- end
- end
- end);
+module:hook("iq/host/jabber:iq:last:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type == "get" then
+ origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:last", seconds = tostring(os.difftime(os.time(), start_time))}));
+ return true;
+ end
+end);
diff --git a/plugins/mod_vcard.lua b/plugins/mod_vcard.lua
index db67d9e5..0efc1638 100644
--- a/plugins/mod_vcard.lua
+++ b/plugins/mod_vcard.lua
@@ -6,58 +6,53 @@
-- COPYING file in the source package for more information.
--
-
-
-local hosts = _G.hosts;
-local datamanager = require "util.datamanager"
-
local st = require "util.stanza"
-local t_concat, t_insert = table.concat, table.insert;
-
-local jid = require "util.jid"
-local jid_split = jid.split;
+local jid_split = require "util.jid".split;
+local datamanager = require "util.datamanager"
module:add_feature("vcard-temp");
-module:add_iq_handler({"c2s", "s2sin"}, "vcard-temp",
- function (session, stanza)
- if stanza.tags[1].name == "vCard" then
- local to = stanza.attr.to;
- if stanza.attr.type == "get" then
- local vCard;
- if to then
- local node, host = jid_split(to);
- if hosts[host] and hosts[host].type == "local" then
- vCard = st.deserialize(datamanager.load(node, host, "vcard")); -- load vCard for user or server
- end
- else
- vCard = st.deserialize(datamanager.load(session.username, session.host, "vcard"));-- load user's own vCard
- end
- if vCard then
- session.send(st.reply(stanza):add_child(vCard)); -- send vCard!
- else
- session.send(st.error_reply(stanza, "cancel", "item-not-found"));
- end
- elseif stanza.attr.type == "set" then
- if not to or to == session.username.."@"..session.host then
- if datamanager.store(session.username, session.host, "vcard", st.preserialize(stanza.tags[1])) then
- session.send(st.reply(stanza));
- else
- -- TODO unable to write file, file may be locked, etc, what's the correct error?
- session.send(st.error_reply(stanza, "wait", "internal-server-error"));
- end
- else
- session.send(st.error_reply(stanza, "auth", "forbidden"));
- end
- end
- return true;
+local function handle_vcard(event)
+ local session, stanza = event.origin, event.stanza;
+ local to = stanza.attr.to;
+ if stanza.attr.type == "get" then
+ local vCard;
+ if to then
+ local node, host = jid_split(to);
+ vCard = st.deserialize(datamanager.load(node, host, "vcard")); -- load vCard for user or server
+ else
+ vCard = st.deserialize(datamanager.load(session.username, session.host, "vcard"));-- load user's own vCard
+ end
+ if vCard then
+ session.send(st.reply(stanza):add_child(vCard)); -- send vCard!
+ else
+ session.send(st.error_reply(stanza, "cancel", "item-not-found"));
+ end
+ else
+ if not to then
+ if datamanager.store(session.username, session.host, "vcard", st.preserialize(stanza.tags[1])) then
+ session.send(st.reply(stanza));
+ else
+ -- TODO unable to write file, file may be locked, etc, what's the correct error?
+ session.send(st.error_reply(stanza, "wait", "internal-server-error"));
end
- end);
-
-local feature_vcard_attr = { var='vcard-temp' };
-module:add_event_hook("stream-features",
- function (session, features)
- if session.type == "c2s" then
- features:tag("feature", feature_vcard_attr):up();
- end
- end);
+ else
+ session.send(st.error_reply(stanza, "auth", "forbidden"));
+ end
+ end
+ return true;
+end
+
+module:hook("iq/bare/vcard-temp:vCard", handle_vcard);
+module:hook("iq/host/vcard-temp:vCard", handle_vcard);
+
+-- COMPAT: https://support.process-one.net/browse/EJAB-1045
+if module:get_option("vcard_compatibility") then
+ module:hook("iq/full", function(data)
+ local stanza = data.stanza;
+ local payload = stanza.tags[1];
+ if stanza.attr.type == "get" or stanza.attr.type == "set" and payload.name == "vCard" and payload.attr.xmlns == "vcard-temp" then
+ return handle_vcard(data);
+ end
+ end, 1);
+end
diff --git a/plugins/mod_version.lua b/plugins/mod_version.lua
index 87bff5d9..9af830f8 100644
--- a/plugins/mod_version.lua
+++ b/plugins/mod_version.lua
@@ -6,17 +6,13 @@
-- COPYING file in the source package for more information.
--
-
-local prosody = prosody;
local st = require "util.stanza";
-local xmlns_version = "jabber:iq:version"
-
-module:add_feature(xmlns_version);
+module:add_feature("jabber:iq:version");
local version = "the best operating system ever!";
-if not require "core.configmanager".get("*", "core", "hide_os_type") then
+if not module:get_option("hide_os_type") then
if os.getenv("WINDIR") then
version = "Windows";
else
@@ -31,11 +27,15 @@ end
version = version:match("^%s*(.-)%s*$") or version;
-module:add_iq_handler({"c2s", "s2sin"}, xmlns_version, function(session, stanza)
- if stanza.attr.type == "get" then
- session.send(st.reply(stanza):query(xmlns_version)
- :tag("name"):text("Prosody"):up()
- :tag("version"):text(prosody.version):up()
- :tag("os"):text(version));
+local query = st.stanza("query", {xmlns = "jabber:iq:version"})
+ :tag("name"):text("Prosody"):up()
+ :tag("version"):text(prosody.version):up()
+ :tag("os"):text(version);
+
+module:hook("iq/host/jabber:iq:version:query", function(event)
+ local stanza = event.stanza;
+ if stanza.attr.type == "get" and stanza.attr.to == module.host then
+ event.origin.send(st.reply(stanza):add_child(query));
+ return true;
end
end);
diff --git a/plugins/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua
index 9457313f..6a2af853 100644
--- a/plugins/mod_watchregistrations.lua
+++ b/plugins/mod_watchregistrations.lua
@@ -9,12 +9,10 @@
local host = module:get_host();
-local config = require "core.configmanager";
+local registration_watchers = module:get_option("registration_watchers")
+ or module:get_option("admins") or {};
-local registration_watchers = config.get(host, "core", "registration_watchers")
- or config.get(host, "core", "admins") or {};
-
-local registration_alert = config.get(host, "core", "registration_notification") or "User $username just registered on $host from $ip";
+local registration_alert = module:get_option("registration_notification") or "User $username just registered on $host from $ip";
local st = require "util.stanza";
diff --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua
index 5c0da8b8..edcfbd8c 100644
--- a/plugins/mod_welcome.lua
+++ b/plugins/mod_welcome.lua
@@ -6,10 +6,8 @@
-- COPYING file in the source package for more information.
--
-local config = require "core.configmanager";
-
local host = module:get_host();
-local welcome_text = config.get("*", "core", "welcome_message") or "Hello $user, welcome to the $host IM server!";
+local welcome_text = module:get_option("welcome_message") or "Hello $username, welcome to the $host IM server!";
local st = require "util.stanza";
diff --git a/plugins/mod_xmlrpc.lua b/plugins/mod_xmlrpc.lua
index 05c0b8b0..7165386a 100644
--- a/plugins/mod_xmlrpc.lua
+++ b/plugins/mod_xmlrpc.lua
@@ -83,7 +83,7 @@ local function handle_xmlrpc_request(jid, method, args)
end
return create_error_response(500, "Error in creating response: "..result);
end
- return create_error_response(0, (result and result:gmatch("[^:]*:[^:]*: (.*)")()) or "nil");
+ return create_error_response(0, tostring(result):gsub("^[^:]+:%d+: ", ""));
end
local function handle_xmpp_request(origin, stanza)
diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua
new file mode 100644
index 00000000..856f3cba
--- /dev/null
+++ b/plugins/muc/mod_muc.lua
@@ -0,0 +1,164 @@
+-- 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.
+--
+
+
+if module:get_host_type() ~= "component" then
+ error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0);
+end
+
+local muc_host = module:get_host();
+local muc_name = module:get_option("name");
+if type(muc_name) ~= "string" then muc_name = "Prosody Chatrooms"; end
+local restrict_room_creation = module:get_option("restrict_room_creation");
+if restrict_room_creation and restrict_room_creation ~= true then restrict_room_creation = nil; end
+local history_length = 20;
+
+local muc_new_room = module:require "muc".new_room;
+local register_component = require "core.componentmanager".register_component;
+local deregister_component = require "core.componentmanager".deregister_component;
+local jid_split = require "util.jid".split;
+local jid_bare = require "util.jid".bare;
+local st = require "util.stanza";
+local uuid_gen = require "util.uuid".generate;
+local datamanager = require "util.datamanager";
+local um_is_admin = require "core.usermanager".is_admin;
+
+local rooms = {};
+local persistent_rooms = datamanager.load(nil, muc_host, "persistent") or {};
+local component;
+
+local function is_admin(jid)
+ return um_is_admin(jid) or um_is_admin(jid, module.host);
+end
+
+local function room_route_stanza(room, stanza) core_post_stanza(component, stanza); end
+local function room_save(room, forced)
+ local node = jid_split(room.jid);
+ persistent_rooms[room.jid] = room._data.persistent;
+ if room._data.persistent then
+ local history = room._data.history;
+ room._data.history = nil;
+ local data = {
+ jid = room.jid;
+ _data = room._data;
+ _affiliations = room._affiliations;
+ };
+ datamanager.store(node, muc_host, "config", data);
+ room._data.history = history;
+ elseif forced then
+ datamanager.store(node, muc_host, "config", nil);
+ end
+ if forced then datamanager.store(nil, muc_host, "persistent", persistent_rooms); end
+end
+
+for jid in pairs(persistent_rooms) do
+ local node = jid_split(jid);
+ local data = datamanager.load(node, muc_host, "config") or {};
+ local room = muc_new_room(jid);
+ room._data = data._data;
+ room._affiliations = data._affiliations;
+ room.route_stanza = room_route_stanza;
+ room.save = room_save;
+ rooms[jid] = room;
+end
+
+local host_room = muc_new_room(muc_host);
+host_room.route_stanza = room_route_stanza;
+host_room.save = room_save;
+
+local function get_disco_info(stanza)
+ return st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info")
+ :tag("identity", {category='conference', type='text', name=muc_name}):up()
+ :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply
+end
+local function get_disco_items(stanza)
+ local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
+ for jid, room in pairs(rooms) do
+ if not room._data.hidden then
+ reply:tag("item", {jid=jid, name=jid}):up();
+ end
+ end
+ return reply; -- TODO cache disco reply
+end
+
+local function handle_to_domain(origin, stanza)
+ local type = stanza.attr.type;
+ if type == "error" or type == "result" then return; end
+ if stanza.name == "iq" and type == "get" then
+ local xmlns = stanza.tags[1].attr.xmlns;
+ if xmlns == "http://jabber.org/protocol/disco#info" then
+ origin.send(get_disco_info(stanza));
+ elseif xmlns == "http://jabber.org/protocol/disco#items" then
+ origin.send(get_disco_items(stanza));
+ elseif xmlns == "http://jabber.org/protocol/muc#unique" then
+ origin.send(st.reply(stanza):tag("unique", {xmlns = xmlns}):text(uuid_gen())); -- FIXME Random UUIDs can theoretically have collisions
+ else
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc
+ end
+ else
+ host_room:handle_stanza(origin, stanza);
+ --origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it"));
+ end
+end
+
+component = register_component(muc_host, function(origin, stanza)
+ local to_node, to_host, to_resource = jid_split(stanza.attr.to);
+ if to_node then
+ local bare = to_node.."@"..to_host;
+ if to_host == muc_host or bare == muc_host then
+ local room = rooms[bare];
+ if not room then
+ if not(restrict_room_creation) or is_admin(stanza.attr.from) then
+ room = muc_new_room(bare);
+ room.route_stanza = room_route_stanza;
+ room.save = room_save;
+ rooms[bare] = room;
+ end
+ end
+ if room then
+ room:handle_stanza(origin, stanza);
+ if not next(room._occupants) and not persistent_rooms[room.jid] then -- empty, non-persistent room
+ rooms[bare] = nil; -- discard room
+ end
+ else
+ origin.send(st.error_reply(stanza, "cancel", "not-allowed"));
+ end
+ else --[[not for us?]] end
+ return;
+ end
+ -- to the main muc domain
+ handle_to_domain(origin, stanza);
+end);
+function component.send(stanza) -- FIXME do a generic fix
+ if stanza.attr.type == "result" or stanza.attr.type == "error" then
+ core_post_stanza(component, stanza);
+ else error("component.send only supports result and error stanzas at the moment"); end
+end
+
+prosody.hosts[module:get_host()].muc = { rooms = rooms };
+
+module.unload = function()
+ deregister_component(muc_host);
+end
+module.save = function()
+ return {rooms = rooms};
+end
+module.restore = function(data)
+ rooms = {};
+ for jid, oldroom in pairs(data.rooms or {}) do
+ local room = muc_new_room(jid);
+ room._jid_nick = oldroom._jid_nick;
+ room._occupants = oldroom._occupants;
+ room._data = oldroom._data;
+ room._affiliations = oldroom._affiliations;
+ room.route_stanza = room_route_stanza;
+ room.save = room_save;
+ rooms[jid] = room;
+ end
+ prosody.hosts[module:get_host()].muc = { rooms = rooms };
+end
diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua
new file mode 100644
index 00000000..3a185e17
--- /dev/null
+++ b/plugins/muc/muc.lib.lua
@@ -0,0 +1,735 @@
+-- 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 datetime = require "util.datetime";
+
+local jid_split = require "util.jid".split;
+local jid_bare = require "util.jid".bare;
+local jid_prep = require "util.jid".prep;
+local st = require "util.stanza";
+local log = require "util.logger".init("mod_muc");
+local multitable_new = require "util.multitable".new;
+local t_insert, t_remove = table.insert, table.remove;
+local setmetatable = setmetatable;
+local base64 = require "util.encodings".base64;
+local md5 = require "util.hashes".md5;
+
+local muc_domain = nil; --module:get_host();
+local history_length = 20;
+
+------------
+local function filter_xmlns_from_array(array, filters)
+ local count = 0;
+ for i=#array,1,-1 do
+ local attr = array[i].attr;
+ if filters[attr and attr.xmlns] then
+ t_remove(array, i);
+ count = count + 1;
+ end
+ end
+ return count;
+end
+local function filter_xmlns_from_stanza(stanza, filters)
+ if filters then
+ if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then
+ return stanza, filter_xmlns_from_array(stanza, filters);
+ end
+ end
+ return stanza, 0;
+end
+local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true};
+local function get_filtered_presence(stanza)
+ return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters);
+end
+local kickable_error_conditions = {
+ ["gone"] = true;
+ ["internal-server-error"] = true;
+ ["item-not-found"] = true;
+ ["jid-malformed"] = true;
+ ["recipient-unavailable"] = true;
+ ["redirect"] = true;
+ ["remote-server-not-found"] = true;
+ ["remote-server-timeout"] = true;
+ ["service-unavailable"] = true;
+ ["malformed error"] = true;
+};
+local function get_error_condition(stanza)
+ for _, tag in ipairs(stanza.tags) do
+ if tag.name == "error" and (not(tag.attr.xmlns) or tag.attr.xmlns == "jabber:client") then
+ for _, cond in ipairs(tag.tags) do
+ if cond.attr.xmlns == "urn:ietf:params:xml:ns:xmpp-stanzas" then
+ return cond.name;
+ end
+ end
+ return "malformed error";
+ end
+ end
+ return "malformed error";
+end
+local function is_kickable_error(stanza)
+ local cond = get_error_condition(stanza);
+ return kickable_error_conditions[cond] and cond;
+end
+local function getUsingPath(stanza, path, getText)
+ local tag = stanza;
+ for _, name in ipairs(path) do
+ if type(tag) ~= 'table' then return; end
+ tag = tag:child_with_name(name);
+ end
+ if tag and getText then tag = table.concat(tag); end
+ return tag;
+end
+local function getTag(stanza, path) return getUsingPath(stanza, path); end
+local function getText(stanza, path) return getUsingPath(stanza, path, true); end
+-----------
+
+--[[function get_room_disco_info(room, stanza)
+ return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info")
+ :tag("identity", {category='conference', type='text', name=room._data["name"]):up()
+ :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply
+end
+function get_room_disco_items(room, stanza)
+ return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
+end -- TODO allow non-private rooms]]
+
+--
+
+local room_mt = {};
+room_mt.__index = room_mt;
+
+function room_mt:get_default_role(affiliation)
+ if affiliation == "owner" or affiliation == "admin" then
+ return "moderator";
+ elseif affiliation == "member" or not affiliation then
+ return "participant";
+ end
+end
+
+function room_mt:broadcast_presence(stanza, sid, code, nick)
+ stanza = get_filtered_presence(stanza);
+ local occupant = self._occupants[stanza.attr.from];
+ stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none", nick=nick}):up();
+ if code then
+ stanza:tag("status", {code=code}):up();
+ end
+ self:broadcast_except_nick(stanza, stanza.attr.from);
+ local me = self._occupants[stanza.attr.from];
+ if me then
+ stanza:tag("status", {code='110'});
+ stanza.attr.to = sid;
+ self:_route_stanza(stanza);
+ end
+end
+function room_mt:broadcast_message(stanza, historic)
+ for occupant, o_data in pairs(self._occupants) do
+ for jid in pairs(o_data.sessions) do
+ stanza.attr.to = jid;
+ self:_route_stanza(stanza);
+ end
+ end
+ 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:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203
+ stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
+ t_insert(history, st.clone(st.preserialize(stanza)));
+ while #history > history_length do t_remove(history, 1) end
+ end
+end
+function room_mt:broadcast_except_nick(stanza, nick)
+ for rnick, occupant in pairs(self._occupants) do
+ if rnick ~= nick then
+ for jid in pairs(occupant.sessions) do
+ stanza.attr.to = jid;
+ self:_route_stanza(stanza);
+ end
+ end
+ end
+end
+
+function room_mt:send_occupant_list(to)
+ local current_nick = self._jid_nick[to];
+ for occupant, o_data in pairs(self._occupants) do
+ if occupant ~= current_nick then
+ local pres = get_filtered_presence(o_data.sessions[o_data.jid]);
+ pres.attr.to, pres.attr.from = to, occupant;
+ pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("item", {affiliation=o_data.affiliation or "none", role=o_data.role or "none"}):up();
+ self:_route_stanza(pres);
+ end
+ end
+end
+function room_mt:send_history(to)
+ local history = self._data['history']; -- send discussion history
+ if history then
+ for _, msg in ipairs(history) do
+ msg = st.deserialize(msg);
+ msg.attr.to=to;
+ self:_route_stanza(msg);
+ end
+ end
+ if self._data['subject'] then
+ self:_route_stanza(st.message({type='groupchat', from=self.jid, to=to}):tag("subject"):text(self._data['subject']));
+ end
+end
+
+local function room_get_disco_info(self, stanza)
+ return st.reply(stanza):query("http://jabber.org/protocol/disco#info")
+ :tag("identity", {category="conference", type="text"}):up()
+ :tag("feature", {var="http://jabber.org/protocol/muc"});
+end
+local function room_get_disco_items(self, stanza)
+ local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
+ for room_jid in pairs(self._occupants) do
+ reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up();
+ end
+ return reply;
+end
+function room_mt:set_subject(current_nick, subject)
+ -- TODO check nick's authority
+ if subject == "" then subject = nil; end
+ self._data['subject'] = subject;
+ if self.save then self:save(); end
+ local msg = st.message({type='groupchat', from=current_nick})
+ :tag('subject'):text(subject):up();
+ self:broadcast_message(msg, false);
+ return true;
+end
+
+function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
+ local from, to = stanza.attr.from, stanza.attr.to;
+ local room = jid_bare(to);
+ local current_nick = self._jid_nick[from];
+ local type = stanza.attr.type;
+ log("debug", "room: %s, current_nick: %s, stanza: %s", room or "nil", current_nick or "nil", stanza:top_tag());
+ if (select(2, jid_split(from)) == muc_domain) then error("Presence from the MUC itself!!!"); end
+ if stanza.name == "presence" then
+ local pr = get_filtered_presence(stanza);
+ pr.attr.from = current_nick;
+ if type == "error" then -- error, kick em out!
+ if current_nick then
+ log("debug", "kicking %s from %s", current_nick, room);
+ self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to})
+ :tag('status'):text('Kicked: '..get_error_condition(stanza))); -- send unavailable
+ end
+ elseif type == "unavailable" then -- unavailable
+ if current_nick then
+ log("debug", "%s leaving %s", current_nick, room);
+ local occupant = self._occupants[current_nick];
+ local new_jid = next(occupant.sessions);
+ if new_jid == from then new_jid = next(occupant.sessions, new_jid); end
+ if new_jid then
+ local jid = occupant.jid;
+ occupant.jid = new_jid;
+ occupant.sessions[from] = nil;
+ pr.attr.to = from;
+ pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("item", {affiliation=occupant.affiliation or "none", role='none'}):up()
+ :tag("status", {code='110'});
+ self:_route_stanza(pr);
+ if jid ~= new_jid then
+ pr = st.clone(occupant.sessions[new_jid])
+ :tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none"});
+ pr.attr.from = current_nick;
+ self:broadcast_except_nick(pr, current_nick);
+ end
+ else
+ occupant.role = 'none';
+ self:broadcast_presence(pr, from);
+ self._occupants[current_nick] = nil;
+ end
+ self._jid_nick[from] = nil;
+ end
+ elseif not type then -- available
+ if current_nick then
+ --if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence
+ if current_nick == to then -- simple presence
+ log("debug", "%s broadcasted presence", current_nick);
+ self._occupants[current_nick].sessions[from] = pr;
+ self:broadcast_presence(pr, from);
+ else -- change nick
+ local occupant = self._occupants[current_nick];
+ local is_multisession = next(occupant.sessions, next(occupant.sessions));
+ if self._occupants[to] or is_multisession then
+ log("debug", "%s couldn't change nick", current_nick);
+ local reply = st.error_reply(stanza, "cancel", "conflict"):up();
+ reply.tags[1].attr.code = "409";
+ origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ else
+ local data = self._occupants[current_nick];
+ local to_nick = select(3, jid_split(to));
+ if to_nick then
+ log("debug", "%s (%s) changing nick to %s", current_nick, data.jid, to);
+ local p = st.presence({type='unavailable', from=current_nick});
+ self:broadcast_presence(p, from, '303', to_nick);
+ self._occupants[current_nick] = nil;
+ self._occupants[to] = data;
+ self._jid_nick[from] = to;
+ pr.attr.from = to;
+ self._occupants[to].sessions[from] = pr;
+ self:broadcast_presence(pr, from);
+ else
+ --TODO malformed-jid
+ end
+ end
+ end
+ --else -- possible rejoin
+ -- log("debug", "%s had connection replaced", current_nick);
+ -- self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to})
+ -- :tag('status'):text('Replaced by new connection'):up()); -- send unavailable
+ -- self:handle_to_occupant(origin, stanza); -- resend available
+ --end
+ else -- enter room
+ local new_nick = to;
+ local is_merge;
+ if self._occupants[to] then
+ if jid_bare(from) ~= jid_bare(self._occupants[to].jid) then
+ new_nick = nil;
+ end
+ is_merge = true;
+ end
+ if not new_nick then
+ log("debug", "%s couldn't join due to nick conflict: %s", from, to);
+ local reply = st.error_reply(stanza, "cancel", "conflict"):up();
+ reply.tags[1].attr.code = "409";
+ origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ else
+ log("debug", "%s joining as %s", from, to);
+ if not next(self._affiliations) then -- new room, no owners
+ self._affiliations[jid_bare(from)] = "owner";
+ end
+ local affiliation = self:get_affiliation(from);
+ local role = self:get_default_role(affiliation)
+ if role then -- new occupant
+ if not is_merge then
+ self._occupants[to] = {affiliation=affiliation, role=role, jid=from, sessions={[from]=get_filtered_presence(stanza)}};
+ else
+ self._occupants[to].sessions[from] = get_filtered_presence(stanza);
+ end
+ self._jid_nick[from] = to;
+ self:send_occupant_list(from);
+ pr.attr.from = to;
+ if not is_merge then
+ self:broadcast_presence(pr, from);
+ else
+ pr.attr.to = from;
+ self:_route_stanza(pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up()
+ :tag("status", {code='110'}));
+ end
+ self:send_history(from);
+ else -- banned
+ local reply = st.error_reply(stanza, "auth", "forbidden"):up();
+ reply.tags[1].attr.code = "403";
+ origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ end
+ end
+ end
+ elseif type ~= 'result' then -- bad type
+ if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences
+ origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
+ end
+ end
+ elseif not current_nick then -- not in room
+ if type == "error" or type == "result" then
+ local id = stanza.name == "iq" and stanza.attr.id and base64.decode(stanza.attr.id);
+ local _nick, _id, _hash = (id or ""):match("^(.+)%z(.*)%z(.+)$");
+ local occupant = self._occupants[stanza.attr.to];
+ if occupant and _nick and self._jid_nick[_nick] and _id and _hash then
+ local id, _to = stanza.attr.id;
+ for jid in pairs(occupant.sessions) do
+ if md5(jid) == _hash then
+ _to = jid;
+ break;
+ end
+ end
+ if _to then
+ stanza.attr.to, stanza.attr.from, stanza.attr.id = _to, self._jid_nick[_nick], _id;
+ self:_route_stanza(stanza);
+ stanza.attr.to, stanza.attr.from, stanza.attr.id = to, from, id;
+ end
+ end
+ else
+ origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
+ end
+ elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM
+ origin.send(st.error_reply(stanza, "modify", "bad-request"));
+ elseif current_nick and stanza.name == "message" and type == "error" and is_kickable_error(stanza) then
+ log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
+ self:handle_to_occupant(origin, st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to})
+ :tag('status'):text('Kicked: '..get_error_condition(stanza))); -- send unavailable
+ else -- private stanza
+ local o_data = self._occupants[to];
+ if o_data then
+ log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid);
+ local jid = o_data.jid;
+ local bare = jid_bare(jid);
+ stanza.attr.to, stanza.attr.from = jid, current_nick;
+ local id = stanza.attr.id;
+ if stanza.name=='iq' and type=='get' and stanza.tags[1].attr.xmlns == 'vcard-temp' and bare ~= jid then
+ stanza.attr.to = bare;
+ stanza.attr.id = base64.encode(jid.."\0"..id.."\0"..md5(from));
+ end
+ self:_route_stanza(stanza);
+ stanza.attr.to, stanza.attr.from, stanza.attr.id = to, from, id;
+ elseif type ~= "error" and type ~= "result" then -- recipient not in room
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
+ end
+ end
+end
+
+function room_mt:handle_form(origin, stanza)
+ if self:get_affiliation(stanza.attr.from) ~= "owner" then origin.send(st.error_reply(stanza, "auth", "forbidden")); return; end
+ if stanza.attr.type == "get" then
+ local title = "Configuration for "..self.jid;
+ origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
+ :tag("x", {xmlns='jabber:x:data', type='form'})
+ :tag("title"):text(title):up()
+ :tag("instructions"):text(title):up()
+ :tag("field", {type='hidden', var='FORM_TYPE'}):tag("value"):text("http://jabber.org/protocol/muc#roomconfig"):up():up()
+ :tag("field", {type='boolean', label='Make Room Persistent?', var='muc#roomconfig_persistentroom'})
+ :tag("value"):text(self._data.persistent and "1" or "0"):up()
+ :up()
+ :tag("field", {type='boolean', label='Make Room Publicly Searchable?', var='muc#roomconfig_publicroom'})
+ :tag("value"):text(self._data.hidden and "0" or "1"):up()
+ :up()
+ );
+ elseif stanza.attr.type == "set" then
+ local query = stanza.tags[1];
+ local form;
+ for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end
+ if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end
+ if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end
+ if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ local fields = {};
+ for _, field in pairs(form.tags) do
+ if field.name == "field" and field.attr.var and field.tags[1].name == "value" and #field.tags[1].tags == 0 then
+ fields[field.attr.var] = field.tags[1][1] or "";
+ end
+ end
+ if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+
+ local persistent = fields['muc#roomconfig_persistentroom'];
+ if persistent == "0" or persistent == "false" then persistent = nil; elseif persistent == "1" or persistent == "true" then persistent = true;
+ else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ self._data.persistent = persistent;
+ module:log("debug", "persistent=%s", tostring(persistent));
+
+ local public = fields['muc#roomconfig_publicroom'];
+ if public == "0" or public == "false" then public = nil; elseif public == "1" or public == "true" then public = true;
+ else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ self._data.hidden = not public and true or nil;
+
+ if self.save then self:save(true); end
+ origin.send(st.reply(stanza));
+ end
+end
+
+function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
+ local type = stanza.attr.type;
+ local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns;
+ if stanza.name == "iq" then
+ if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" then
+ origin.send(room_get_disco_info(self, stanza));
+ elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" then
+ origin.send(room_get_disco_items(self, stanza));
+ elseif xmlns == "http://jabber.org/protocol/muc#admin" then
+ local actor = stanza.attr.from;
+ local affiliation = self:get_affiliation(actor);
+ local current_nick = self._jid_nick[actor];
+ local role = current_nick and self._occupants[current_nick].role or self:get_default_role(affiliation);
+ local item = stanza.tags[1].tags[1];
+ if item and item.name == "item" then
+ if type == "set" then
+ local callback = function() origin.send(st.reply(stanza)); end
+ if item.attr.jid then -- Validate provided JID
+ item.attr.jid = jid_prep(item.attr.jid);
+ if not item.attr.jid then
+ origin.send(st.error_reply(stanza, "modify", "jid-malformed"));
+ return;
+ end
+ end
+ if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation
+ local occupant = self._occupants[self.jid.."/"..item.attr.nick];
+ if occupant then item.attr.jid = occupant.jid; end
+ end
+ local reason = item.tags[1] and item.tags[1].name == "reason" and #item.tags[1] == 1 and item.tags[1][1];
+ if item.attr.affiliation and item.attr.jid and not item.attr.role then
+ local success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, callback, reason);
+ if not success then origin.send(st.error_reply(stanza, errtype, err)); end
+ elseif item.attr.role and item.attr.nick and not item.attr.affiliation then
+ local success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, callback, reason);
+ if not success then origin.send(st.error_reply(stanza, errtype, err)); end
+ else
+ origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+ end
+ elseif type == "get" then
+ local _aff = item.attr.affiliation;
+ local _rol = item.attr.role;
+ if _aff and not _rol then
+ if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then
+ local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
+ for jid, affiliation in pairs(self._affiliations) do
+ if affiliation == _aff then
+ reply:tag("item", {affiliation = _aff, jid = jid}):up();
+ end
+ end
+ origin.send(reply);
+ else
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
+ end
+ elseif _rol and not _aff then
+ if role == "moderator" then
+ -- TODO allow admins and owners not in room? Provide read-only access to everyone who can see the participants anyway?
+ if _rol == "none" then _rol = nil; end
+ local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
+ for nick, occupant in pairs(self._occupants) do
+ if occupant.role == _rol then
+ reply:tag("item", {nick = nick, role = _rol or "none", affiliation = occupant.affiliation or "none", jid = occupant.jid}):up();
+ end
+ end
+ origin.send(reply);
+ else
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
+ end
+ else
+ origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+ end
+ end
+ elseif type == "set" or type == "get" then
+ origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+ end
+ elseif xmlns == "http://jabber.org/protocol/muc#owner" and (type == "get" or type == "set") and stanza.tags[1].name == "query" then
+ self:handle_form(origin, stanza);
+ elseif type == "set" or type == "get" then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end
+ elseif stanza.name == "message" and type == "groupchat" then
+ local from, to = stanza.attr.from, stanza.attr.to;
+ local room = jid_bare(to);
+ local current_nick = self._jid_nick[from];
+ if not current_nick then -- not in room
+ origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
+ else
+ local from = stanza.attr.from;
+ stanza.attr.from = current_nick;
+ local subject = getText(stanza, {"subject"});
+ if subject then
+ self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza
+ else
+ self:broadcast_message(stanza, true);
+ end
+ end
+ elseif stanza.name == "message" and type == "error" and is_kickable_error(stanza) then
+ local current_nick = self._jid_nick[stanza.attr.from];
+ log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
+ self:handle_to_occupant(origin, st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to})
+ :tag('status'):text('Kicked: '..get_error_condition(stanza))); -- send unavailable
+ elseif stanza.name == "presence" then -- hack - some buggy clients send presence updates to the room rather than their nick
+ local to = stanza.attr.to;
+ local current_nick = self._jid_nick[stanza.attr.from];
+ if current_nick then
+ stanza.attr.to = current_nick;
+ self:handle_to_occupant(origin, stanza);
+ stanza.attr.to = to;
+ elseif type ~= "error" and type ~= "result" then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end
+ elseif stanza.name == "message" and not stanza.attr.type and #stanza.tags == 1 and self._jid_nick[stanza.attr.from]
+ and stanza.tags[1].name == "x" and stanza.tags[1].attr.xmlns == "http://jabber.org/protocol/muc#user" then
+ local x = stanza.tags[1];
+ local payload = (#x.tags == 1 and x.tags[1]);
+ if payload and payload.name == "invite" and payload.attr.to then
+ local _from, _to = stanza.attr.from, stanza.attr.to;
+ local _invitee = jid_prep(payload.attr.to);
+ if _invitee then
+ local _reason = payload.tags[1] and payload.tags[1].name == 'reason' and #payload.tags[1].tags == 0 and payload.tags[1][1];
+ local invite = st.message({from = _to, to = _invitee, id = stanza.attr.id})
+ :tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag('invite', {from=_from})
+ :tag('reason'):text(_reason or ""):up()
+ :up()
+ :up()
+ :tag('x', {xmlns="jabber:x:conference", jid=_to}) -- COMPAT: Some older clients expect this
+ :text(_reason or "")
+ :up()
+ :tag('body') -- Add a plain message for clients which don't support invites
+ :text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or ""))
+ :up();
+ self:_route_stanza(invite);
+ else
+ origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
+ end
+ else
+ origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+ end
+ else
+ if type == "error" or type == "result" then return; end
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end
+end
+
+function room_mt:handle_stanza(origin, stanza)
+ local to_node, to_host, to_resource = jid_split(stanza.attr.to);
+ if to_resource then
+ self:handle_to_occupant(origin, stanza);
+ else
+ self:handle_to_room(origin, stanza);
+ end
+end
+
+function room_mt:route_stanza(stanza) end -- Replace with a routing function, e.g., function(room, stanza) core_route_stanza(origin, stanza); end
+
+function room_mt:get_affiliation(jid)
+ local node, host, resource = jid_split(jid);
+ local bare = node and node.."@"..host or host;
+ local result = self._affiliations[bare]; -- Affiliations are granted, revoked, and maintained based on the user's bare JID.
+ if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned
+ return result;
+end
+function room_mt:set_affiliation(actor, jid, affiliation, callback, reason)
+ jid = jid_bare(jid);
+ if affiliation == "none" then affiliation = nil; end
+ if affiliation and affiliation ~= "outcast" and affiliation ~= "owner" and affiliation ~= "admin" and affiliation ~= "member" then
+ return nil, "modify", "not-acceptable";
+ end
+ if self:get_affiliation(actor) ~= "owner" then return nil, "cancel", "not-allowed"; end
+ if jid_bare(actor) == jid then return nil, "cancel", "not-allowed"; end
+ self._affiliations[jid] = affiliation;
+ local role = self:get_default_role(affiliation);
+ local p = st.presence()
+ :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
+ :tag("item", {affiliation=affiliation or "none", role=role or "none"})
+ :tag("reason"):text(reason or ""):up()
+ :up();
+ local x = p.tags[1];
+ local item = x.tags[1];
+ if not role then -- getting kicked
+ p.attr.type = "unavailable";
+ if affiliation == "outcast" then
+ x:tag("status", {code="301"}):up(); -- banned
+ else
+ x:tag("status", {code="321"}):up(); -- affiliation change
+ end
+ end
+ local modified_nicks = {};
+ for nick, occupant in pairs(self._occupants) do
+ if jid_bare(occupant.jid) == jid then
+ if not role then -- getting kicked
+ self._occupants[nick] = nil;
+ else
+ t_insert(modified_nicks, nick);
+ occupant.affiliation, occupant.role = affiliation, role;
+ end
+ p.attr.from = nick;
+ for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick
+ if not role then self._jid_nick[jid] = nil; end
+ p.attr.to = jid;
+ self:_route_stanza(p);
+ end
+ end
+ end
+ if self.save then self:save(); end
+ if callback then callback(); end
+ for _, nick in ipairs(modified_nicks) do
+ p.attr.from = nick;
+ self:broadcast_except_nick(p, nick);
+ end
+ return true;
+end
+
+function room_mt:get_role(nick)
+ local session = self._occupants[nick];
+ return session and session.role or nil;
+end
+function room_mt:set_role(actor, nick, role, callback, reason)
+ if role == "none" then role = nil; end
+ if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end
+ if self:get_affiliation(actor) ~= "owner" then return nil, "cancel", "not-allowed"; end
+ local occupant = self._occupants[nick];
+ if not occupant then return nil, "modify", "not-acceptable"; end
+ if occupant.affiliation == "owner" or occupant.affiliation == "admin" then return nil, "cancel", "not-allowed"; end
+ local p = st.presence({from = nick})
+ :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
+ :tag("item", {affiliation=occupant.affiliation or "none", nick=nick, role=role or "none"})
+ :tag("reason"):text(reason or ""):up()
+ :up();
+ if not role then -- kick
+ p.attr.type = "unavailable";
+ self._occupants[nick] = nil;
+ for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick
+ self._jid_nick[jid] = nil;
+ end
+ p:tag("status", {code = "307"}):up();
+ else
+ occupant.role = role;
+ end
+ for jid in pairs(occupant.sessions) do -- send to all sessions of the nick
+ p.attr.to = jid;
+ self:_route_stanza(p);
+ end
+ if callback then callback(); end
+ self:broadcast_except_nick(p, nick);
+ return true;
+end
+
+function room_mt:_route_stanza(stanza)
+ local muc_child;
+ local to_occupant = self._occupants[self._jid_nick[stanza.attr.to]];
+ local from_occupant = self._occupants[stanza.attr.from];
+ if stanza.name == "presence" then
+ if to_occupant and from_occupant then
+ if to_occupant.role == "moderator" or jid_bare(to_occupant.jid) == jid_bare(from_occupant.jid) then
+ for i=#stanza.tags,1,-1 do
+ local tag = stanza.tags[i];
+ if tag.name == "x" and tag.attr.xmlns == "http://jabber.org/protocol/muc#user" then
+ muc_child = tag;
+ break;
+ end
+ end
+ end
+ end
+ end
+ if muc_child then
+ for _, item in pairs(muc_child.tags) do
+ if item.name == "item" then
+ if from_occupant == to_occupant then
+ item.attr.jid = stanza.attr.to;
+ else
+ item.attr.jid = from_occupant.jid;
+ end
+ end
+ end
+ end
+ self:route_stanza(stanza);
+ if muc_child then
+ for _, item in pairs(muc_child.tags) do
+ if item.name == "item" then
+ item.attr.jid = nil;
+ end
+ end
+ end
+end
+
+local _M = {}; -- module "muc"
+
+function _M.new_room(jid)
+ return setmetatable({
+ jid = jid;
+ _jid_nick = {};
+ _occupants = {};
+ _data = {};
+ _affiliations = {};
+ }, room_mt);
+end
+
+return _M;