aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/mod_bosh.lua37
-rw-r--r--plugins/mod_compression.lua122
-rw-r--r--plugins/mod_console.lua23
-rw-r--r--plugins/mod_disco.lua45
-rw-r--r--plugins/mod_httpserver.lua60
-rw-r--r--plugins/mod_legacyauth.lua17
-rw-r--r--plugins/mod_offline.lua3
-rw-r--r--plugins/mod_pep.lua2
-rw-r--r--plugins/mod_posix.lua40
-rw-r--r--plugins/mod_register.lua11
-rw-r--r--plugins/mod_roster.lua2
-rw-r--r--plugins/mod_saslauth.lua67
-rw-r--r--plugins/mod_selftests.lua5
-rw-r--r--plugins/mod_tls.lua7
-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.lua30
-rw-r--r--plugins/muc/mod_muc.lua (renamed from plugins/mod_muc.lua)72
-rw-r--r--plugins/muc/muc.lib.lua628
20 files changed, 1061 insertions, 146 deletions
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua
index 743ebdef..e310be28 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|body", 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;
@@ -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();
@@ -275,7 +288,7 @@ function on_timer()
end
end
-local ports = config.get(module.host, "core", "bosh_ports") or { 5280 };
+local ports = module:get_option("bosh_ports") or { 5280 };
httpserver.new_from_config(ports, "http-bind", handle_request);
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 716c2525..e8d31ceb 100644
--- a/plugins/mod_console.lua
+++ b/plugins/mod_console.lua
@@ -70,6 +70,9 @@ function console_listener.listener(conn, data)
if data:match("^>") then
data = data:gsub("^>", "");
useglobalenv = true;
+ elseif data == "\004" then
+ commands["bye"](session, data);
+ return;
else
local command = data:lower();
command = data:match("^%w+") or data:match("%p");
@@ -205,7 +208,8 @@ end
-- Anything in def_env will be accessible within the session as a global variable
def_env.server = {};
-function def_env.server:reload()
+
+function def_env.server:insane_reload()
prosody.unlock_globals();
dofile "prosody"
prosody = _G.prosody;
@@ -230,6 +234,11 @@ function def_env.server:uptime()
minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time));
end
+function def_env.server:shutdown(reason)
+ prosody.shutdown(reason);
+ return true, "Shutdown initiated";
+end
+
def_env.module = {};
local function get_hosts_set(hosts, module)
@@ -333,6 +342,11 @@ function def_env.config:get(host, section, key)
return true, tostring(config_get(host, section, key));
end
+function def_env.config:reload()
+ local ok, err = prosody.reload_config();
+ return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err);
+end
+
def_env.hosts = {};
function def_env.hosts:list()
for host, host_session in pairs(hosts) do
@@ -359,7 +373,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 "-");
@@ -371,7 +390,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";
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 73357f1b..72a4506a 100644
--- a/plugins/mod_httpserver.lua
+++ b/plugins/mod_httpserver.lua
@@ -11,45 +11,51 @@ local httpserver = require "net.httpserver";
local open = io.open;
local t_concat = table.concat;
-local check_http_path;
local http_base = config.get("*", "core", "http_path") or "www_files";
-local response_403 = { status = "403 Forbidden", body = "<h1>Invalid URL</h1>Sorry, we couldn't find what you were looking for :(" };
+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 = check_http_path(request.url.path:gsub("^/[^/]+%.*", ""));
- if not path then
- return response_403;
+local function preprocess_path(path)
+ if path:sub(1,1) ~= "/" then
+ path = "/"..path;
end
- http_path[2] = path;
- local f, err = open(t_concat(http_path), "r");
- if not f then return response_404; end
- local data = f:read("*a");
- f:close();
- return data;
-end
-
-local ports = config.get(module.host, "core", "http_ports") or { 5280 };
-httpserver.new_from_config(ports, "files", handle_request);
-
-function check_http_path(url)
- if url:sub(1,1) ~= "/" then
- url = "/"..url;
- end
-
local level = 0;
- for part in url:gmatch("%/([^/]+)") do
- if part == ".." then
+ for component in path:gmatch("([^/]+)/") do
+ if component == ".." then
level = level - 1;
- elseif part ~= "." then
+ elseif component ~= "." then
level = level + 1;
end
if level < 0 then
return nil;
end
end
- return url;
+ 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.set_default_handler(handle_default_request);
+httpserver.new_from_config(ports, "files", handle_file_request);
diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua
index 25967b33..4d0d7ca1 100644
--- a/plugins/mod_legacyauth.lua
+++ b/plugins/mod_legacyauth.lua
@@ -11,8 +11,7 @@
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("require_encryption");
local sessionmanager = require "core.sessionmanager";
local usermanager = require "core.usermanager";
@@ -45,14 +44,12 @@ 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 username and resource and usermanager.validate_credentials(session.host, username, password) then
+ if usermanager.validate_credentials(session.host, username, password) then
-- Authentication successful!
local success, err = sessionmanager.make_authenticated(session, username);
if success then
@@ -69,15 +66,9 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:auth",
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_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..842f1fce 100644
--- a/plugins/mod_pep.lua
+++ b/plugins/mod_pep.lua
@@ -25,7 +25,7 @@ local data = {};
local recipients = {};
local hash_map = {};
-module:add_identity("pubsub", "pep");
+module:add_identity("pubsub", "pep", "Prosody");
module:add_feature("http://jabber.org/protocol/pubsub#publish");
local function publish(session, node, item)
diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua
index 0f46888d..5f7dfc5b 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,7 @@ function syslog_sink_maker(config)
end
require "core.loggingmanager".register_sink_type("syslog", syslog_sink_maker);
-if not config_get("*", "core", "no_daemonize") then
+if not module:get_option("no_daemonize") then
local function daemonize_server()
local ok, ret = pposix.daemonize();
if not ok then
diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua
index 383ab811..0cb8d771 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];
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 f196e483..cbaaaa38 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -12,9 +12,13 @@ local st = require "util.stanza";
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;
+local usermanager_user_exists = require "core.usermanager".user_exists;
+local usermanager_get_password = require "core.usermanager".get_password;
local t_concat, t_insert = table.concat, table.insert;
local tostring = tostring;
local jid_split = require "util.jid".split
@@ -56,6 +60,7 @@ local function handle_status(session, status)
session.sasl_handler = nil;
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();
return;
end
@@ -64,22 +69,36 @@ local function handle_status(session, status)
end
end
-local function password_callback(node, hostname, realm, mechanism, decoder)
- local func = function(x) return x; end;
- local node = nodeprep(node);
- if not node then
- return func, nil;
- end
- local password = (datamanager_load(node, hostname, "accounts") or {}).password; -- FIXME handle hashed passwords
- if password then
- if mechanism == "PLAIN" then
- return func, password;
- elseif mechanism == "DIGEST-MD5" then
- if decoder then node, realm, password = decoder(node), decoder(realm), decoder(password); end
+local function credentials_callback(mechanism, ...)
+ if mechanism == "PLAIN" then
+ local username, hostname, password = ...;
+ username = nodeprep(username);
+ if not username then
+ return false;
+ end
+ 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 prepped_node = nodeprep(node);
+ if not prepped_node then
+ return func, nil;
+ end
+ local password = usermanager_get_password(prepped_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
- return func, nil;
end
local function sasl_handler(session, stanza)
@@ -92,7 +111,7 @@ local function sasl_handler(session, stanza)
elseif stanza.attr.mechanism == "ANONYMOUS" then
return session.send(build_reply("failure", "mechanism-too-weak"));
end
- session.sasl_handler = new_sasl(stanza.attr.mechanism, session.host, password_callback);
+ session.sasl_handler = new_sasl(stanza.attr.mechanism, session.host, credentials_callback);
if not session.sasl_handler then
return session.send(build_reply("failure", "invalid-mechanism"));
end
@@ -111,7 +130,7 @@ local function sasl_handler(session, stanza)
end
local status, ret, err_msg = session.sasl_handler:feed(text);
handle_status(session, status);
- local s = build_reply(status, ret, err_msg);
+ local s = build_reply(status, ret, err_msg);
log("debug", "sasl reply: %s", tostring(s));
session.send(s);
end
@@ -123,8 +142,8 @@ module:add_handler("c2s_unauthed", "response", xmlns_sasl, sasl_handler);
local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' };
local bind_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-bind' };
local xmpp_session_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-session' };
-module:add_event_hook("stream-features",
- function (session, features)
+module:add_event_hook("stream-features",
+ function (session, features)
if not session.username then
if secure_auth_only and not session.secure then
return;
@@ -134,8 +153,10 @@ module:add_event_hook("stream-features",
if config.get(session.host or "*", "core", "anonymous_login") then
features:tag("mechanism"):text("ANONYMOUS"):up();
else
- features:tag("mechanism"):text("DIGEST-MD5"):up();
- features:tag("mechanism"):text("PLAIN"):up();
+ mechanisms = usermanager_get_supported_methods(session.host or "*");
+ for k, v in pairs(mechanisms) do
+ features:tag("mechanism"):text(k):up();
+ end
end
features:up();
else
@@ -143,8 +164,8 @@ module:add_event_hook("stream-features",
features:tag("session", xmpp_session_attr):up();
end
end);
-
-module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind",
+
+module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind",
function (session, stanza)
log("debug", "Client requesting a resource bind");
local resource;
@@ -166,8 +187,8 @@ module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind",
:tag("jid"):text(session.full_jid));
end
end);
-
-module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-session",
+
+module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-session",
function (session, stanza)
log("debug", "Client requesting a session");
session.send(st.reply(stanza));
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_tls.lua b/plugins/mod_tls.lua
index 8926edfc..10455559 100644
--- a/plugins/mod_tls.lua
+++ b/plugins/mod_tls.lua
@@ -6,14 +6,11 @@
-- 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 config = require "core.configmanager";
-local secure_auth_only = config.get("*", "core", "require_encryption");
+local secure_auth_only = module:get_option("require_encryption");
module:add_handler("c2s_unauthed", "starttls", xmlns_starttls,
function (session, stanza)
@@ -31,7 +28,7 @@ module:add_handler("c2s_unauthed", "starttls", xmlns_starttls,
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
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 e9444363..cc50cba3 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 $username, welcome to the $host IM server!";
+local welcome_text = module:get_option("welcome_message") or "Hello $user, welcome to the $host IM server!";
local st = require "util.stanza";
diff --git a/plugins/mod_xmlrpc.lua b/plugins/mod_xmlrpc.lua
index 46edcaee..7165386a 100644
--- a/plugins/mod_xmlrpc.lua
+++ b/plugins/mod_xmlrpc.lua
@@ -16,6 +16,7 @@ local unpack = unpack;
local tostring = tostring;
local is_admin = require "core.usermanager".is_admin;
local jid_split = require "util.jid".split;
+local jid_bare = require "util.jid".bare;
local b64_decode = require "util.encodings".base64.decode;
local get_method = require "core.objectmanager".get_object;
local validate_credentials = require "core.usermanager".validate_credentials;
@@ -65,10 +66,15 @@ local function parse_xml(xml)
return stanza.tags[1];
end
-local function handle_xmlrpc_request(method, args)
+local function handle_xmlrpc_request(jid, method, args)
+ local is_secure_call = (method:sub(1,7) == "secure/");
+ if not is_admin(jid) and not is_secure_call then
+ return create_error_response(401, "not authorized");
+ end
method = get_method(method);
if not method then return create_error_response(404, "method not found"); end
args = args or {};
+ if is_secure_call then table.insert(args, 1, jid); end
local success, result = pcall(method, unpack(args));
if success then
success, result = pcall(create_response, result or "nil");
@@ -77,22 +83,20 @@ local function handle_xmlrpc_request(method, args)
end
return create_error_response(500, "Error in creating response: "..result);
end
- return create_error_response(0, result or "nil");
+ return create_error_response(0, tostring(result):gsub("^[^:]+:%d+: ", ""));
end
local function handle_xmpp_request(origin, stanza)
local query = stanza.tags[1];
if query.name == "query" then
if #query.tags == 1 then
- if is_admin(stanza.attr.from) then
- local success, method, args = pcall(translate_request, query.tags[1]);
- if success then
- local result = handle_xmlrpc_request(method, args);
- origin.send(st.reply(stanza):tag('query', {xmlns='jabber:iq:rpc'}):add_child(result));
- else
- origin.send(st.error_reply(stanza, "modify", "bad-request", method));
- end
- else origin.send(st.error_reply(stanza, "auth", "forbidden", "No content in XML-RPC request")); end
+ local success, method, args = pcall(translate_request, query.tags[1]);
+ if success then
+ local result = handle_xmlrpc_request(jid_bare(stanza.attr.from), method, args);
+ origin.send(st.reply(stanza):tag('query', {xmlns='jabber:iq:rpc'}):add_child(result));
+ else
+ origin.send(st.error_reply(stanza, "modify", "bad-request", method));
+ end
else origin.send(st.error_reply(stanza, "modify", "bad-request", "No content in XML-RPC request")); end
else origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end
end
@@ -106,7 +110,7 @@ local function handle_http_request(method, body, request)
-- authenticate user
local username, password = b64_decode(request['authorization'] or ''):gmatch('([^:]*):(.*)')(); -- TODO digest auth
local node, host = jid_split(username);
- if not validate_credentials(host, node, password) and is_admin(username) then
+ if not validate_credentials(host, node, password) then
return unauthorized_response;
end
-- parse request
@@ -117,7 +121,7 @@ local function handle_http_request(method, body, request)
-- execute request
local success, method, args = pcall(translate_request, stanza);
if success then
- return { headers = default_headers; body = tostring(handle_xmlrpc_request(method, args)) };
+ return { headers = default_headers; body = tostring(handle_xmlrpc_request(node.."@"..host, method, args)) };
end
return "<html><body>Error parsing XML-RPC request: "..tostring(method).."</body></html>";
end
diff --git a/plugins/mod_muc.lua b/plugins/muc/mod_muc.lua
index e99ef83c..3e6fafb8 100644
--- a/plugins/mod_muc.lua
+++ b/plugins/muc/mod_muc.lua
@@ -15,16 +15,52 @@ local muc_host = module:get_host();
local muc_name = "Chatrooms";
local history_length = 20;
-local muc_new_room = require "util.muc".new_room;
+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 st = require "util.stanza";
+local uuid_gen = require "util.uuid".generate;
+local datamanager = require "util.datamanager";
local rooms = {};
+local persistent_rooms = datamanager.load(nil, muc_host, "persistent") or {};
local component;
+
+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 = function(room, stanza) core_post_stanza(component, stanza); end;
+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")
@@ -34,7 +70,9 @@ 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();
+ if not room._data.hidden then
+ reply:tag("item", {jid=jid, name=jid}):up();
+ end
end
return reply; -- TODO cache disco reply
end
@@ -48,6 +86,8 @@ local function handle_to_domain(origin, stanza)
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
@@ -65,16 +105,27 @@ component = register_component(muc_host, function(origin, stanza)
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;
+ room.route_stanza = room_route_stanza;
+ room.save = room_save;
rooms[bare] = room;
end
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 --[[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);
@@ -83,5 +134,16 @@ module.save = function()
return {rooms = rooms};
end
module.restore = function(data)
- rooms = data.rooms or {};
+ 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..35bde4c6
--- /dev/null
+++ b/plugins/muc/muc.lib.lua
@@ -0,0 +1,628 @@
+-- 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 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), 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;
+};
+local function get_kickable_error(stanza)
+ for _, tag in ipairs(stanza.tags) do
+ if tag.name == "error" and 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 kickable_error_conditions[cond.name] and cond.name;
+ end
+ end
+ return true; -- malformed error message
+ end
+ end
+ return true; -- malformed error message
+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, 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, role=occupant.role, 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'});
+ for jid in pairs(me.sessions) do
+ stanza.attr.to = jid;
+ self:route_stanza(stanza);
+ end
+ 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, role=o_data.role}):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)
+ return st.reply(stanza):query("http://jabber.org/protocol/disco#items");
+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('This participant is kicked from the room because he sent an error presence')); -- 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 old_session = occupant.sessions[from];
+ local new_jid = next(occupant.sessions);
+ if new_jid == from then new_jid = next(occupant.sessions, new_jid); end
+ if new_jid then
+ occupant.jid = new_jid;
+ occupant.sessions[from] = nil;
+ local pr = st.clone(occupant[new_jid])
+ :tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("item", {affiliation=occupant.affiliation, role=occupant.role});
+ self:broadcast_except_nick(pr, current_nick);
+ else
+ occupant.role = 'none';
+ self:broadcast_presence(pr);
+ 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);
+ else -- change nick
+ if self._occupants[to] 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, '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);
+ 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;
+ if self._occupants[to] then
+ new_nick = nil;
+ 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
+ self._occupants[to] = {affiliation=affiliation, role=role, jid=from, sessions={[from]=get_filtered_presence(stanza)}};
+ self._jid_nick[from] = to;
+ self:send_occupant_list(from);
+ pr.attr.from = to;
+ self:broadcast_presence(pr);
+ 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
+ origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
+ 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"));
+ 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 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
+ 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);
+ 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);
+ 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 get_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('This participant is kicked from the room because he sent an error message to another occupant')); -- 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" and #stanza.tags[1].tags == 1
+ and stanza.tags[1].tags[1].name == "invite" and stanza.tags[1].tags[1].attr.to then
+ local _from, _to = stanza.attr.from, stanza.attr.to;
+ local _invitee = stanza.tags[1].tags[1].attr.to;
+ stanza.attr.from, stanza.attr.to = _to, _invitee;
+ stanza.tags[1].tags[1].attr.from, stanza.tags[1].tags[1].attr.to = _from, nil;
+ self:route_stanza(stanza);
+ stanza.tags[1].tags[1].attr.from, stanza.tags[1].tags[1].attr.to = nil, _invitee;
+ stanza.attr.from, stanza.attr.to = _from, _to;
+ 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)
+ 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"}):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)
+ 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"}):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
+
+local _M = {}; -- module "muc"
+
+function _M.new_room(jid)
+ return setmetatable({
+ jid = jid;
+ _jid_nick = {};
+ _occupants = {};
+ _data = {};
+ _affiliations = {};
+ }, room_mt);
+end
+
+return _M;