aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/mod_actions_http.lua86
-rw-r--r--plugins/mod_announce.lua1
-rw-r--r--plugins/mod_bosh.lua65
-rw-r--r--plugins/mod_component.lua13
-rw-r--r--plugins/mod_compression.lua209
-rw-r--r--plugins/mod_console.lua143
-rw-r--r--plugins/mod_disco.lua53
-rw-r--r--plugins/mod_groups.lua36
-rw-r--r--plugins/mod_httpserver.lua13
-rw-r--r--plugins/mod_iq.lua12
-rw-r--r--plugins/mod_legacyauth.lua7
-rw-r--r--plugins/mod_pep.lua18
-rw-r--r--plugins/mod_posix.lua12
-rw-r--r--plugins/mod_presence.lua21
-rw-r--r--plugins/mod_privacy.lua457
-rw-r--r--plugins/mod_private.lua6
-rw-r--r--plugins/mod_proxy65.lua279
-rw-r--r--plugins/mod_register.lua8
-rw-r--r--plugins/mod_roster.lua19
-rw-r--r--plugins/mod_saslauth.lua217
-rw-r--r--plugins/mod_tls.lua155
-rw-r--r--plugins/mod_xmlrpc.lua128
-rw-r--r--plugins/muc/mod_muc.lua1
-rw-r--r--plugins/muc/muc.lib.lua236
24 files changed, 1510 insertions, 685 deletions
diff --git a/plugins/mod_actions_http.lua b/plugins/mod_actions_http.lua
deleted file mode 100644
index e125f167..00000000
--- a/plugins/mod_actions_http.lua
+++ /dev/null
@@ -1,86 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-local httpserver = require "net.httpserver";
-local t_concat, t_insert = table.concat, table.insert;
-
-local log = log;
-
-local response_404 = { status = "404 Not Found", body = "<h1>No such action</h1>Sorry, I don't have the action you requested" };
-
-local control = require "core.actions".actions;
-
-
-local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = string.char(tonumber("0x"..k)); return t[k]; end });
-
-local function urldecode(s)
- return s and (s:gsub("+", " "):gsub("%%([a-fA-F0-9][a-fA-F0-9])", urlcodes));
-end
-
-local function query_to_table(query)
- if type(query) == "string" and #query > 0 then
- if query:match("=") then
- local params = {};
- for k, v in query:gmatch("&?([^=%?]+)=([^&%?]+)&?") do
- if k and v then
- params[urldecode(k)] = urldecode(v);
- end
- end
- return params;
- else
- return urldecode(query);
- end
- end
-end
-
-
-
-local http_path = { http_base };
-local function handle_request(method, body, request)
- local path = request.url.path:gsub("^/[^/]+/", "");
-
- local curr = control;
-
- for comp in path:gmatch("([^/]+)") do
- curr = curr[comp];
- if not curr then
- return response_404;
- end
- end
-
- if type(curr) == "table" then
- local s = {};
- for k,v in pairs(curr) do
- t_insert(s, tostring(k));
- t_insert(s, " = ");
- if type(v) == "function" then
- t_insert(s, "action")
- elseif type(v) == "table" then
- t_insert(s, "list");
- else
- t_insert(s, tostring(v));
- end
- t_insert(s, "\n");
- end
- return t_concat(s);
- elseif type(curr) == "function" then
- local params = query_to_table(request.url.query);
- params.host = request.headers.host:gsub(":%d+", "");
- local ok, ret1, ret2 = pcall(curr, params);
- if not ok then
- return "EPIC FAIL: "..tostring(ret1);
- elseif not ret1 then
- return "FAIL: "..tostring(ret2);
- else
- return "OK: "..tostring(ret2);
- end
- end
-end
-
-httpserver.new{ port = 5280, base = "control", handler = handle_request, ssl = false } \ No newline at end of file
diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua
index 7f08a6e0..d3017f6c 100644
--- a/plugins/mod_announce.lua
+++ b/plugins/mod_announce.lua
@@ -22,7 +22,6 @@ function handle_announcement(data)
if not is_admin(stanza.attr.from) then
-- Not an admin? Not allowed!
module:log("warn", "Non-admin %s tried to send server announcement", tostring(jid.bare(stanza.attr.from)));
- origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
return;
end
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua
index 04887c29..66a79785 100644
--- a/plugins/mod_bosh.lua
+++ b/plugins/mod_bosh.lua
@@ -23,7 +23,7 @@ local logger = require "util.logger";
local log = logger.init("mod_bosh");
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 = "jabber:client" };
+local stream_callbacks = { stream_ns = "http://jabber.org/protocol/httpbind", stream_tag = "body", default_ns = "jabber:client" };
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;
@@ -31,9 +31,27 @@ local BOSH_DEFAULT_POLLING = tonumber(module:get_option("bosh_max_polling")) or
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 consider_bosh_secure = module:get_option_boolean("consider_bosh_secure");
+
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 cross_domain = module:get_option("cross_domain_bosh");
+if cross_domain then
+ default_headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
+ default_headers["Access-Control-Allow-Headers"] = "Content-Type";
+ default_headers["Access-Control-Max-Age"] = "7200";
+
+ if cross_domain == true then
+ default_headers["Access-Control-Allow-Origin"] = "*";
+ elseif type(cross_domain) == "table" then
+ cross_domain = table.concat(cross_domain, ", ");
+ end
+ if type(cross_domain) == "string" then
+ default_headers["Access-Control-Allow-Origin"] = cross_domain;
+ end
+end
+
local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
local os_time = os.time;
@@ -64,9 +82,13 @@ end
function handle_request(method, body, request)
if (not body) or request.method ~= "POST" then
- return "<html><body>You really don't look like a BOSH client to me... what do you want?</body></html>";
+ if request.method == "OPTIONS" then
+ return { headers = default_headers, body = "" };
+ else
+ return "<html><body>You really don't look like a BOSH client to me... what do you want?</body></html>";
+ end
end
- if not method then
+ if not method then
log("debug", "Request %s suffered error %s", tostring(request.id), body);
return;
end
@@ -154,10 +176,14 @@ function stream_callbacks.streamopened(request, attr)
-- New session
sid = new_uuid();
- local session = { type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid)-1, host = attr.to, bosh_version = attr.ver, bosh_wait = 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 };
+ local session = {
+ type = "c2s_unauthed", conn = {}, sid = sid, rid = tonumber(attr.rid)-1, 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 = consider_bosh_secure or request.secure
+ };
sessions[sid] = session;
log("info", "New BOSH session, assigned it sid '%s'", sid);
@@ -192,14 +218,15 @@ function stream_callbacks.streamopened(request, attr)
-- Send creation response
local features = st.stanza("stream:features");
+ hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
fire_event("stream-features", session, features);
--xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh'
- local response = st.stanza("body", { xmlns = xmlns_bosh,
- inactivity = tostring(BOSH_DEFAULT_INACTIVITY), polling = tostring(BOSH_DEFAULT_POLLING), requests = tostring(BOSH_DEFAULT_REQUESTS), hold = tostring(session.bosh_hold), maxpause = "120",
- sid = sid, authid = sid, ver = '1.6', from = session.host, secure = 'true', ["xmpp:version"] = "1.0",
+ local response = st.stanza("body", { xmlns = xmlns_bosh,
+ inactivity = tostring(BOSH_DEFAULT_INACTIVITY), polling = tostring(BOSH_DEFAULT_POLLING), requests = tostring(BOSH_DEFAULT_REQUESTS), hold = tostring(session.bosh_hold), maxpause = "120",
+ sid = sid, authid = sid, ver = '1.6', from = session.host, secure = 'true', ["xmpp:version"] = "1.0",
["xmlns:xmpp"] = "urn:xmpp:xbosh", ["xmlns:stream"] = "http://etherx.jabber.org/streams" }):add_child(features);
request:send{ headers = default_headers, body = tostring(response) };
-
+
request.sid = sid;
return;
end
@@ -238,6 +265,7 @@ function stream_callbacks.streamopened(request, attr)
if session.notopen then
local features = st.stanza("stream:features");
+ hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
fire_event("stream-features", session, features);
session.send(features);
session.notopen = nil;
@@ -255,7 +283,7 @@ function stream_callbacks.handlestanza(request, stanza)
if stanza.attr.xmlns == xmlns_bosh then
stanza.attr.xmlns = nil;
end
- session.ip = request.handler.ip();
+ session.ip = request.handler:ip();
core_process_stanza(session, stanza);
end
end
@@ -299,7 +327,14 @@ function on_timer()
end
end
-local ports = module:get_option("bosh_ports") or { 5280 };
-httpserver.new_from_config(ports, handle_request, { base = "http-bind" });
-server.addtimer(on_timer);
+local function setup()
+ local ports = module:get_option("bosh_ports") or { 5280 };
+ httpserver.new_from_config(ports, handle_request, { base = "http-bind" });
+ server.addtimer(on_timer);
+end
+if prosody.start_time then -- already started
+ setup();
+else
+ prosody.events.add_handler("server-started", setup);
+end
diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua
index 8a7454cd..7efb4f9c 100644
--- a/plugins/mod_component.lua
+++ b/plugins/mod_component.lua
@@ -14,25 +14,14 @@ local hosts = _G.hosts;
local t_concat = table.concat;
-local lxp = require "lxp";
-local logger = require "util.logger";
local config = require "core.configmanager";
-local connlisteners = require "net.connlisteners";
local cm_register_component = require "core.componentmanager".register_component;
local cm_deregister_component = require "core.componentmanager".deregister_component;
-local uuid_gen = require "util.uuid".generate;
local sha1 = require "util.hashes".sha1;
local st = require "util.stanza";
-local init_xmlhandlers = require "core.xmlhandlers";
-
-local sessions = {};
local log = module._log;
-local component_listener = { default_port = 5347; default_mode = "*a"; default_interface = config.get("*", "core", "component_interface") or "127.0.0.1" };
-
-local xmlns_component = 'jabber:component:accept';
-
--- Handle authentication attempts by components
function handle_component_auth(session, stanza)
log("info", "Handling component auth");
@@ -80,4 +69,4 @@ function handle_component_auth(session, stanza)
session.send(st.stanza("handshake"));
end
-module:add_handler("component", "handshake", xmlns_component, handle_component_auth);
+module:add_handler("component", "handshake", "jabber:component:accept", handle_component_auth);
diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua
index fe1e0c67..53341492 100644
--- a/plugins/mod_compression.lua
+++ b/plugins/mod_compression.lua
@@ -12,6 +12,7 @@ local tostring = tostring;
local xmlns_compression_feature = "http://jabber.org/features/compress"
local xmlns_compression_protocol = "http://jabber.org/protocol/compress"
+local xmlns_stream = "http://etherx.jabber.org/streams";
local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up();
local compression_level = module:get_option("compression_level");
@@ -26,22 +27,148 @@ if not compression_level or compression_level < 1 or compression_level > 9 then
return;
end
-module:add_event_hook("stream-features",
- function (session, features)
+module:hook("stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if not origin.compressed then
+ -- FIXME only advertise compression support when TLS layer has no compression enabled
+ features:add_child(compression_stream_feature);
+ end
+end);
+
+module:hook("s2s-stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ -- FIXME only advertise compression support when TLS layer has no compression enabled
+ if not origin.compressed then
+ features:add_child(compression_stream_feature);
+ end
+end);
+
+-- Hook to activate compression if remote server supports it.
+module:hook_stanza(xmlns_stream, "features",
+ function (session, stanza)
if not session.compressed then
- -- FIXME only advertise compression support when TLS layer has no compression enabled
- features:add_child(compression_stream_feature);
+ -- does remote server support compression?
+ local comp_st = stanza:child_with_name("compression");
+ if comp_st then
+ -- do we support the mechanism
+ for a in comp_st:children() do
+ local algorithm = a[1]
+ if algorithm == "zlib" then
+ session.sends2s(st.stanza("compress", {xmlns=xmlns_compression_protocol}):tag("method"):text("zlib"))
+ session.log("debug", "Enabled compression using zlib.")
+ return true;
+ end
+ end
+ session.log("debug", "Remote server supports no compression algorithm we support.")
+ end
+ end
+ end
+, 250);
+
+
+-- returns either nil or a fully functional ready to use inflate stream
+local function get_deflate_stream(session)
+ local status, deflate_stream = pcall(zlib.deflate, compression_level);
+ if status == false then
+ local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
+ (session.sends2s or session.send)(error_st);
+ session.log("error", "Failed to create zlib.deflate filter.");
+ module:log("error", "%s", tostring(deflate_stream));
+ return
+ end
+ return deflate_stream
+end
+
+-- returns either nil or a fully functional ready to use inflate stream
+local function get_inflate_stream(session)
+ local status, inflate_stream = pcall(zlib.inflate);
+ if status == false then
+ local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
+ (session.sends2s or session.send)(error_st);
+ session.log("error", "Failed to create zlib.inflate filter.");
+ module:log("error", "%s", tostring(inflate_stream));
+ return
+ end
+ return inflate_stream
+end
+
+-- setup compression for a stream
+local function setup_compression(session, deflate_stream)
+ local old_send = (session.sends2s or session.send);
+
+ local new_send = function(t)
+ --TODO: Better code injection in the sending process
+ local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
+ if status == false then
+ session:close({
+ condition = "undefined-condition";
+ text = compressed;
+ extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
+ });
+ module:log("warn", "%s", tostring(compressed));
+ return;
+ end
+ session.conn:write(compressed);
+ end;
+
+ if session.sends2s then session.sends2s = new_send
+ elseif session.send then session.send = new_send end
+end
+
+-- setup decompression for a stream
+local function setup_decompression(session, inflate_stream)
+ local old_data = session.data
+ session.data = function(conn, data)
+ local status, decompressed, eof = pcall(inflate_stream, data);
+ if status == false then
+ session:close({
+ condition = "undefined-condition";
+ text = decompressed;
+ extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
+ });
+ module:log("warn", "%s", tostring(decompressed));
+ return;
end
+ old_data(conn, decompressed);
+ end;
+end
+
+module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compression_protocol,
+ function(session ,stanza)
+ session.log("debug", "Activating compression...")
+ -- create deflate and inflate streams
+ local deflate_stream = get_deflate_stream(session);
+ if not deflate_stream then return end
+
+ local inflate_stream = get_inflate_stream(session);
+ if not inflate_stream then return end
+
+ -- setup compression for session.w
+ setup_compression(session, deflate_stream);
+
+ -- setup decompression for session.data
+ setup_decompression(session, inflate_stream);
+ local session_reset_stream = session.reset_stream;
+ session.reset_stream = function(session)
+ session_reset_stream(session);
+ setup_decompression(session, inflate_stream);
+ return true;
+ end;
+ session:reset_stream();
+ local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams",
+ ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host};
+ session.sends2s("<?xml version='1.0'?>");
+ session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag());
+ session.compressed = true;
end
);
--- TODO Support compression on S2S level too.
-module:add_handler({"c2s_unauthed", "c2s"}, "compress", xmlns_compression_protocol,
+module:add_handler({"c2s_unauthed", "c2s", "s2sin_unauthed", "s2sin"}, "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("setup-failed");
- session.send(error_st);
+ (session.sends2s or session.send)(error_st);
session.log("debug", "Client tried to establish another compression layer.");
return;
end
@@ -50,78 +177,38 @@ module:add_handler({"c2s_unauthed", "c2s"}, "compress", xmlns_compression_protoc
local method = stanza:child_with_name("method");
method = method and (method[1] or "");
if method == "zlib" then
+ session.log("debug", "zlib compression enabled.");
+
-- create deflate and inflate streams
- local 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", "%s", tostring(deflate_stream));
- return
- end
+ local deflate_stream = get_deflate_stream(session);
+ if not deflate_stream then 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.inflate filter.");
- module:log("error", "%s", tostring(inflate_stream));
- return
- end
+ local inflate_stream = get_inflate_stream(session);
+ if not inflate_stream then return end
- session.log("debug", "zlib compression enabled.");
- session.send(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
+ (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
session:reset_stream();
-
- -- 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", "%s", tostring(compressed));
- return;
- end
- old_send(compressed);
- end;
+ -- setup compression for session.w
+ setup_compression(session, deflate_stream);
-- 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", "%s", tostring(decompressed));
- return;
- end
- old_data(conn, decompressed);
- end;
- end
- setup_decompression(session);
+ setup_decompression(session, inflate_stream);
local session_reset_stream = session.reset_stream;
session.reset_stream = function(session)
session_reset_stream(session);
- setup_decompression(session);
+ setup_decompression(session, inflate_stream);
return true;
end;
session.compressed = true;
elseif method then
session.log("debug", "%s compression selected, but we don't support it.", tostring(method));
local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method");
- session.send(error_st);
+ (session.sends2s or session.send)(error_st);
else
- session.send(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"));
+ (session.sends2s or session.send)(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"));
end
end
);
+
diff --git a/plugins/mod_console.lua b/plugins/mod_console.lua
index a33302d9..e87ef536 100644
--- a/plugins/mod_console.lua
+++ b/plugins/mod_console.lua
@@ -33,11 +33,11 @@ end
console = {};
function console:new_session(conn)
- local w = function(s) conn.write(s:gsub("\n", "\r\n")); end;
+ local w = function(s) conn:write(s:gsub("\n", "\r\n")); end;
local session = { conn = conn;
send = function (t) w(tostring(t)); end;
print = function (t) w("| "..tostring(t).."\n"); end;
- disconnect = function () conn.close(); end;
+ disconnect = function () conn:close(); end;
};
session.env = setmetatable({}, default_env_mt);
@@ -53,81 +53,82 @@ end
local sessions = {};
-function console_listener.listener(conn, data)
+function console_listener.onconnect(conn)
+ -- Handle new connection
+ local session = console:new_session(conn);
+ sessions[conn] = session;
+ printbanner(session);
+ session.send(string.char(0));
+end
+
+function console_listener.onincoming(conn, data)
local session = sessions[conn];
-
- if not session then
- -- Handle new connection
- session = console:new_session(conn);
- sessions[conn] = session;
- printbanner(session);
- end
- if data then
- -- Handle data
- (function(session, data)
- local useglobalenv;
-
- if data:match("^>") then
- data = data:gsub("^>", "");
- useglobalenv = true;
- elseif data == "\004" then
- commands["bye"](session, data);
+
+ -- Handle data
+ (function(session, data)
+ local useglobalenv;
+
+ 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");
+ if commands[command] then
+ commands[command](session, data);
return;
- else
- local command = data:lower();
- command = data:match("^%w+") or data:match("%p");
- if commands[command] then
- commands[command](session, data);
- return;
- end
end
+ end
- session.env._ = data;
-
- local chunkname = "=console";
- local chunk, err = loadstring("return "..data, chunkname);
+ session.env._ = data;
+
+ local chunkname = "=console";
+ local chunk, err = loadstring("return "..data, chunkname);
+ if not chunk then
+ chunk, err = loadstring(data, chunkname);
if not chunk then
- chunk, err = loadstring(data, chunkname);
- if not chunk then
- err = err:gsub("^%[string .-%]:%d+: ", "");
- err = err:gsub("^:%d+: ", "");
- err = err:gsub("'<eof>'", "the end of the line");
- session.print("Sorry, I couldn't understand that... "..err);
- return;
- end
- end
-
- setfenv(chunk, (useglobalenv and redirect_output(_G, session)) or session.env or nil);
-
- local ranok, taskok, message = pcall(chunk);
-
- if not (ranok or message or useglobalenv) and commands[data:lower()] then
- commands[data:lower()](session, data);
- return;
- end
-
- if not ranok then
- session.print("Fatal error while running command, it did not complete");
- session.print("Error: "..taskok);
- return;
- end
-
- if not message then
- session.print("Result: "..tostring(taskok));
- return;
- elseif (not taskok) and message then
- session.print("Command completed with a problem");
- session.print("Message: "..tostring(message));
+ err = err:gsub("^%[string .-%]:%d+: ", "");
+ err = err:gsub("^:%d+: ", "");
+ err = err:gsub("'<eof>'", "the end of the line");
+ session.print("Sorry, I couldn't understand that... "..err);
return;
end
-
- session.print("OK: "..tostring(message));
- end)(session, data);
- end
+ end
+
+ setfenv(chunk, (useglobalenv and redirect_output(_G, session)) or session.env or nil);
+
+ local ranok, taskok, message = pcall(chunk);
+
+ if not (ranok or message or useglobalenv) and commands[data:lower()] then
+ commands[data:lower()](session, data);
+ return;
+ end
+
+ if not ranok then
+ session.print("Fatal error while running command, it did not complete");
+ session.print("Error: "..taskok);
+ return;
+ end
+
+ if not message then
+ session.print("Result: "..tostring(taskok));
+ return;
+ elseif (not taskok) and message then
+ session.print("Command completed with a problem");
+ session.print("Message: "..tostring(message));
+ return;
+ end
+
+ session.print("OK: "..tostring(message));
+ end)(session, data);
+
session.send(string.char(0));
end
-function console_listener.disconnect(conn, err)
+function console_listener.ondisconnect(conn, err)
local session = sessions[conn];
if session then
session.disconnect();
@@ -149,7 +150,7 @@ commands.quit, commands.exit = commands.bye, commands.bye;
commands["!"] = function (session, data)
if data:match("^!!") then
session.print("!> "..session.env._);
- return console_listener.listener(session.conn, session.env._);
+ return console_listener.onincoming(session.conn, session.env._);
end
local old, new = data:match("^!(.-[^\\])!(.-)!$");
if old and new then
@@ -159,7 +160,7 @@ commands["!"] = function (session, data)
return;
end
session.print("!> "..res);
- return console_listener.listener(session.conn, res);
+ return console_listener.onincoming(session.conn, res);
end
session.print("Sorry, not sure what you want");
end
@@ -479,7 +480,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..(session.secure and " (encrypted)" or ""));
+ print(" "..host.." -> "..remotehost..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
if session.sendq then
print(" There are "..#session.sendq.." queued outgoing stanzas for this connection");
end
@@ -516,7 +517,7 @@ function def_env.s2s:show(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)")..(session.secure and " (encrypted)" or ""));
+ print(" "..host.." <- "..(session.from_host or "(unknown)")..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
if session.type == "s2sin_unauthed" then
print(" Connection not yet authenticated");
end
diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua
index 8181c85b..ee0043f1 100644
--- a/plugins/mod_disco.lua
+++ b/plugins/mod_disco.lua
@@ -7,8 +7,30 @@
--
local componentmanager_get_children = require "core.componentmanager".get_children;
+local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
+local jid_split = require "util.jid".split;
+local jid_bare = require "util.jid".bare;
local st = require "util.stanza"
+local disco_items = module:get_option("disco_items") or {};
+do -- validate disco_items
+ for _, item in ipairs(disco_items) do
+ local err;
+ if type(item) ~= "table" then
+ err = "item is not a table";
+ elseif type(item[1]) ~= "string" then
+ err = "item jid is not a string";
+ elseif item[2] and type(item[2]) ~= "string" then
+ err = "item name is not a string";
+ end
+ if err then
+ module:log("error", "option disco_items is malformed: %s", err);
+ disco_items = {}; -- TODO clean up data instead of removing it?
+ break;
+ end
+ end
+end
+
module:add_identity("server", "im", "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");
@@ -47,6 +69,37 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve
for jid in pairs(componentmanager_get_children(module.host)) do
reply:tag("item", {jid = jid}):up();
end
+ for _, item in ipairs(disco_items) do
+ reply:tag("item", {jid=item[1], name=item[2]}):up();
+ end
origin.send(reply);
return true;
end);
+module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type ~= "get" then return; end
+ local node = stanza.tags[1].attr.node;
+ if node and node ~= "" then return; end -- TODO fire event?
+ local username = jid_split(stanza.attr.to) or origin.username;
+ if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+ local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'});
+ if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
+ module:fire_event("account-disco-info", { session = origin, stanza = reply });
+ origin.send(reply);
+ return true;
+ end
+end);
+module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(event)
+ local origin, stanza = event.origin, event.stanza;
+ if stanza.attr.type ~= "get" then return; end
+ local node = stanza.tags[1].attr.node;
+ if node and node ~= "" then return; end -- TODO fire event?
+ local username = jid_split(stanza.attr.to) or origin.username;
+ if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+ local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items'});
+ if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
+ module:fire_event("account-disco-items", { session = origin, stanza = reply });
+ origin.send(reply);
+ return true;
+ end
+end);
diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua
index 9534be80..5f821cbc 100644
--- a/plugins/mod_groups.lua
+++ b/plugins/mod_groups.lua
@@ -7,8 +7,8 @@
--
-local groups = { default = {} };
-local members = { [false] = {} };
+local groups;
+local members;
local groups_file;
@@ -18,9 +18,9 @@ local jid_bare, jid_prep = jid.bare, jid.prep;
local module_host = module:get_host();
function inject_roster_contacts(username, host, roster)
- module:log("warn", "Injecting group members to roster");
+ --module:log("debug", "Injecting group members to roster");
local bare_jid = username.."@"..host;
- if not members[bare_jid] then return; end -- Not a member of any groups
+ if not members[bare_jid] and not members[false] then return; end -- Not a member of any groups
local function import_jids_to_roster(group_name)
for jid in pairs(groups[group_name]) do
@@ -39,13 +39,23 @@ function inject_roster_contacts(username, host, roster)
end
-- Find groups this JID is a member of
- for _, group_name in ipairs(members[bare_jid]) do
- import_jids_to_roster(group_name);
+ if members[bare_jid] then
+ for _, group_name in ipairs(members[bare_jid]) do
+ --module:log("debug", "Importing group %s", group_name);
+ import_jids_to_roster(group_name);
+ end
end
-- Import public groups
- for _, group_name in ipairs(members[false]) do
- import_jids_to_roster(group_name);
+ if members[false] then
+ for _, group_name in ipairs(members[false]) do
+ --module:log("debug", "Importing group %s", group_name);
+ import_jids_to_roster(group_name);
+ end
+ end
+
+ if roster[false] then
+ roster[false].version = true;
end
end
@@ -57,6 +67,9 @@ function remove_virtual_contacts(username, host, datastore, data)
new_roster[jid] = contact;
end
end
+ if new_roster[false] then
+ new_roster[false].version = nil; -- Version is void
+ end
return username, host, datastore, new_roster;
end
@@ -71,20 +84,23 @@ function module.load()
datamanager.add_callback(remove_virtual_contacts);
groups = { default = {} };
- members = { [false] = {} };
+ members = { };
local curr_group = "default";
for line in io.lines(groups_file) do
if line:match("^%s*%[.-%]%s*$") then
curr_group = line:match("^%s*%[(.-)%]%s*$");
if curr_group:match("^%+") then
curr_group = curr_group:gsub("^%+", "");
+ if not members[false] then
+ members[false] = {};
+ end
members[false][#members[false]+1] = curr_group; -- Is a public group
end
module:log("debug", "New group: %s", tostring(curr_group));
groups[curr_group] = groups[curr_group] or {};
else
-- Add JID
- local jid = jid_prep(line);
+ local jid = jid_prep(line:match("%S+"));
if jid then
module:log("debug", "New member of %s: %s", tostring(curr_group), tostring(jid));
groups[curr_group][jid] = true;
diff --git a/plugins/mod_httpserver.lua b/plugins/mod_httpserver.lua
index a554be08..c55bd20f 100644
--- a/plugins/mod_httpserver.lua
+++ b/plugins/mod_httpserver.lua
@@ -76,6 +76,13 @@ local function handle_default_request(method, body, request)
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, handle_file_request, { base = "files" });
+local function setup()
+ local ports = config.get(module.host, "core", "http_ports") or { 5280 };
+ httpserver.set_default_handler(handle_default_request);
+ httpserver.new_from_config(ports, handle_file_request, { base = "files" });
+end
+if prosody.start_time then -- already started
+ setup();
+else
+ prosody.events.add_handler("server-started", setup);
+end
diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua
index f13e9768..b3001fe5 100644
--- a/plugins/mod_iq.lua
+++ b/plugins/mod_iq.lua
@@ -53,6 +53,18 @@ module:hook("iq/bare", function(data)
end
end);
+module:hook("iq/self", function(data)
+ -- IQ to bare JID recieved
+ local origin, stanza = data.origin, data.stanza;
+
+ if stanza.attr.type == "get" or stanza.attr.type == "set" then
+ return module:fire_event("iq/self/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data);
+ else
+ module:fire_event("iq/self/"..stanza.attr.id, data);
+ return true;
+ end
+end);
+
module:hook("iq/host", function(data)
-- IQ to a local host recieved
local origin, stanza = data.origin, data.stanza;
diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua
index 3593ad38..0134d736 100644
--- a/plugins/mod_legacyauth.lua
+++ b/plugins/mod_legacyauth.lua
@@ -19,11 +19,12 @@ 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)
- if secure_auth_only and not session.secure then
+module:hook("stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if secure_auth_only and not origin.secure then
-- Sorry, not offering to insecure streams!
return;
- elseif not session.username then
+ elseif not origin.username then
features:tag("auth", {xmlns='http://jabber.org/features/iq-auth'}):up();
end
end);
diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua
index faa7251b..aa46d2d3 100644
--- a/plugins/mod_pep.lua
+++ b/plugins/mod_pep.lua
@@ -277,3 +277,21 @@ module:hook("iq/bare/disco", function(event)
end
end
end);
+
+module:hook("account-disco-info", function(event)
+ local stanza = event.stanza;
+ stanza:tag('identity', {category='pubsub', type='pep'}):up();
+ stanza:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up();
+end);
+
+module:hook("account-disco-items", function(event)
+ local session, stanza = event.session, event.stanza;
+ local bare = session.username..'@'..session.host;
+ local user_data = data[bare];
+
+ if user_data then
+ for node, _ in pairs(user_data) do
+ stanza:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes
+ end
+ end
+end);
diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua
index 9155289d..c38f7eba 100644
--- a/plugins/mod_posix.lua
+++ b/plugins/mod_posix.lua
@@ -7,7 +7,7 @@
--
-local want_pposix_version = "0.3.1";
+local want_pposix_version = "0.3.3";
local pposix = assert(require "util.pposix");
if pposix._VERSION ~= want_pposix_version then module:log("warn", "Unknown version (%s) of binary pposix module, expected %s", tostring(pposix._VERSION), want_pposix_version); end
@@ -26,6 +26,9 @@ local prosody = _G.prosody;
module.host = "*"; -- we're a global module
+local umask = module:get_option("umask") or "027";
+pposix.umask(umask);
+
-- Allow switching away from root, some people like strange ports.
module:add_event_hook("server-started", function ()
local uid = module:get_option("setuid");
@@ -160,4 +163,11 @@ if signal.signal then
prosody.reload_config();
prosody.reopen_logfiles();
end);
+
+ signal.signal("SIGINT", function ()
+ module:log("info", "Received SIGINT");
+ prosody.unlock_globals();
+ prosody.shutdown("Received SIGINT");
+ prosody.lock_globals();
+ end);
end
diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua
index 108ab0d3..4fb8c3e4 100644
--- a/plugins/mod_presence.lua
+++ b/plugins/mod_presence.lua
@@ -62,7 +62,17 @@ local function recalc_resource_map(user)
end
end
+local ignore_presence_priority = module:get_option("ignore_presence_priority");
+
function handle_normal_presence(origin, stanza, core_route_stanza)
+ if ignore_presence_priority then
+ local priority = stanza:child_with_name("priority");
+ if priority and priority[1] ~= "0" then
+ for i=#priority.tags,1,-1 do priority.tags[i] = nil; end
+ for i=#priority,1,-1 do priority[i] = nil; end
+ priority[1] = "0";
+ end
+ end
if full_sessions[origin.full_jid] then -- if user is still connected
origin.send(stanza); -- reflect their presence back to them
end
@@ -217,16 +227,13 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b
stanza.attr.from, stanza.attr.to = from_bare, to_bare;
log("debug", "inbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);
- if not node then
- log("debug", "dropping presence sent to host or invalid address '%s'", tostring(to_bare));
- end
-
if stanza.attr.type == "probe" then
- if rostermanager.is_contact_subscribed(node, host, from_bare) then
+ local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
+ if result then
if 0 == send_presence_of_available_resources(node, host, st_from, origin, core_route_stanza) then
- core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- TODO send last activity
+ core_route_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"})); -- TODO send last activity
end
- else
+ elseif not err then
core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}));
end
elseif stanza.attr.type == "subscribe" then
diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua
index a1b7acff..aa953310 100644
--- a/plugins/mod_privacy.lua
+++ b/plugins/mod_privacy.lua
@@ -1,31 +1,466 @@
-- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
+-- Copyright (C) 2009-2010 Matthew Wild
+-- Copyright (C) 2009-2010 Waqas Hussain
+-- Copyright (C) 2009 Thilo Cestonaro
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
+module:add_feature("jabber:iq:privacy");
+local prosody = prosody;
local st = require "util.stanza";
local datamanager = require "util.datamanager";
+local bare_sessions, full_sessions = bare_sessions, full_sessions;
+local util_Jid = require "util.jid";
+local jid_bare = util_Jid.bare;
+local jid_split, jid_join = util_Jid.split, util_Jid.join;
+local load_roster = require "core.rostermanager".load_roster;
+local to_number = tonumber;
+
+function isListUsed(origin, name, privacy_lists)
+ local user = bare_sessions[origin.username.."@"..origin.host];
+ if user then
+ for resource, session in pairs(user.sessions) do
+ if resource ~= origin.resource then
+ if session.activePrivacyList == name then
+ return true;
+ elseif session.activePrivacyList == nil and privacy_lists.default == name then
+ return true;
+ end
+ end
+ end
+ end
+end
+
+function isAnotherSessionUsingDefaultList(origin)
+ local user = bare_sessions[origin.username.."@"..origin.host];
+ if user then
+ for resource, session in pairs(user.sessions) do
+ if resource ~= origin.resource and session.activePrivacyList == nil then
+ return true;
+ end
+ end
+ end
+end
+
+function sendUnavailable(origin, to, from)
+--[[ example unavailable presence stanza
+<presence from="node@host/resource" type="unavailable" to="node@host" >
+ <status>Logged out</status>
+</presence>
+]]--
+ local presence = st.presence({from=from, type="unavailable"});
+ presence:tag("status"):text("Logged out");
+
+ local node, host = jid_bare(to);
+ local bare = node .. "@" .. host;
+
+ local user = bare_sessions[bare];
+ if user then
+ for resource, session in pairs(user.sessions) do
+ presence.attr.to = session.full_jid;
+ module:log("debug", "send unavailable to: %s; from: %s", tostring(presence.attr.to), tostring(presence.attr.from));
+ origin.send(presence);
+ end
+ end
+end
+
+function declineList(privacy_lists, origin, stanza, which)
+ if which == "default" then
+ if isAnotherSessionUsingDefaultList(origin) then
+ return { "cancel", "conflict", "Another session is online and using the default list."};
+ end
+ privacy_lists.default = nil;
+ origin.send(st.reply(stanza));
+ elseif which == "active" then
+ origin.activePrivacyList = nil;
+ origin.send(st.reply(stanza));
+ else
+ return {"modify", "bad-request", "Neither default nor active list specifed to decline."};
+ end
+ return true;
+end
+
+function activateList(privacy_lists, origin, stanza, which, name)
+ local list = privacy_lists.lists[name];
+
+ if which == "default" and list then
+ if isAnotherSessionUsingDefaultList(origin) then
+ return {"cancel", "conflict", "Another session is online and using the default list."};
+ end
+ privacy_lists.default = name;
+ origin.send(st.reply(stanza));
+ elseif which == "active" and list then
+ origin.activePrivacyList = name;
+ origin.send(st.reply(stanza));
+ elseif not list then
+ return {"cancel", "item-not-found", "No such list: "..name};
+ else
+ return {"modify", "bad-request", "No list chosen to be active or default."};
+ end
+ return true;
+end
+
+function deleteList(privacy_lists, origin, stanza, name)
+ local list = privacy_lists.lists[name];
+
+ if list then
+ if isListUsed(origin, name, privacy_lists) then
+ return {"cancel", "conflict", "Another session is online and using the list which should be deleted."};
+ end
+ if privacy_lists.default == name then
+ privacy_lists.default = nil;
+ end
+ if origin.activePrivacyList == name then
+ origin.activePrivacyList = nil;
+ end
+ privacy_lists.lists[name] = nil;
+ origin.send(st.reply(stanza));
+ return true;
+ end
+ return {"modify", "bad-request", "Not existing list specifed to be deleted."};
+end
+
+function createOrReplaceList (privacy_lists, origin, stanza, name, entries, roster)
+ local bare_jid = origin.username.."@"..origin.host;
+
+ if privacy_lists.lists == nil then
+ privacy_lists.lists = {};
+ end
+
+ local list = {};
+ privacy_lists.lists[name] = list;
+
+ local orderCheck = {};
+ list.name = name;
+ list.items = {};
+
+ for _,item in ipairs(entries) do
+ if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then
+ return {"modify", "bad-request", "Order attribute not valid."};
+ end
+
+ if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then
+ return {"modify", "bad-request", "Type attribute not valid."};
+ end
+
+ local tmp = {};
+ orderCheck[item.attr.order] = true;
+
+ tmp["type"] = item.attr.type;
+ tmp["value"] = item.attr.value;
+ tmp["action"] = item.attr.action;
+ tmp["order"] = to_number(item.attr.order);
+ tmp["presence-in"] = false;
+ tmp["presence-out"] = false;
+ tmp["message"] = false;
+ tmp["iq"] = false;
+
+ if #item.tags > 0 then
+ for _,tag in ipairs(item.tags) do
+ tmp[tag.name] = true;
+ end
+ end
+
+ if tmp.type == "subscription" then
+ if tmp.value ~= "both" and
+ tmp.value ~= "to" and
+ tmp.value ~= "from" and
+ tmp.value ~= "none" then
+ return {"cancel", "bad-request", "Subscription value must be both, to, from or none."};
+ end
+ end
+
+ if tmp.action ~= "deny" and tmp.action ~= "allow" then
+ return {"cancel", "bad-request", "Action must be either deny or allow."};
+ end
+ list.items[#list.items + 1] = tmp;
+ end
+
+ table.sort(list, function(a, b) return a.order < b.order; end);
+
+ origin.send(st.reply(stanza));
+ if bare_sessions[bare_jid] ~= nil then
+ local iq = st.iq ( { type = "set", id="push1" } );
+ iq:tag ("query", { xmlns = "jabber:iq:privacy" } );
+ iq:tag ("list", { name = list.name } ):up();
+ iq:up();
+ for resource, session in pairs(bare_sessions[bare_jid].sessions) do
+ iq.attr.to = bare_jid.."/"..resource
+ session.send(iq);
+ end
+ else
+ return {"cancel", "bad-request", "internal error."};
+ end
+ return true;
+end
+
+function getList(privacy_lists, origin, stanza, name)
+ local reply = st.reply(stanza);
+ reply:tag("query", {xmlns="jabber:iq:privacy"});
+
+ if name == nil then
+ if privacy_lists.lists then
+ if origin.ActivePrivacyList then
+ reply:tag("active", {name=origin.activePrivacyList}):up();
+ end
+ if privacy_lists.default then
+ reply:tag("default", {name=privacy_lists.default}):up();
+ end
+ for name,list in pairs(privacy_lists.lists) do
+ reply:tag("list", {name=name}):up();
+ end
+ end
+ else
+ local list = privacy_lists.lists[name];
+ if list then
+ reply = reply:tag("list", {name=list.name});
+ for _,item in ipairs(list.items) do
+ reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order});
+ if item["message"] then reply:tag("message"):up(); end
+ if item["iq"] then reply:tag("iq"):up(); end
+ if item["presence-in"] then reply:tag("presence-in"):up(); end
+ if item["presence-out"] then reply:tag("presence-out"):up(); end
+ reply:up();
+ end
+ else
+ return {"cancel", "item-not-found", "Unknown list specified."};
+ end
+ end
+
+ origin.send(reply);
+ return true;
+end
module:hook("iq/bare/jabber:iq:privacy:query", function(data)
local origin, stanza = data.origin, data.stanza;
- if not stanza.attr.to then -- only service requests to own bare JID
+ if stanza.attr.to == nil then -- only service requests to own bare JID
local query = stanza.tags[1]; -- the query element
- local privacy_lists = datamanager.load(origin.username, origin.host, "privacy") or {};
+ local valid = false;
+ local privacy_lists = datamanager.load(origin.username, origin.host, "privacy") or { lists = {} };
+
+ if privacy_lists.lists[1] then -- Code to migrate from old privacy lists format, remove in 0.8
+ module:log("info", "Upgrading format of stored privacy lists for %s@%s", origin.username, origin.host);
+ local lists = privacy_lists.lists;
+ for idx, list in ipairs(lists) do
+ lists[list.name] = list;
+ lists[idx] = nil;
+ end
+ end
+
if stanza.attr.type == "set" then
- -- TODO
+ if #query.tags == 1 then -- the <query/> element MUST NOT include more than one child element
+ for _,tag in ipairs(query.tags) do
+ if tag.name == "active" or tag.name == "default" then
+ if tag.attr.name == nil then -- Client declines the use of active / default list
+ valid = declineList(privacy_lists, origin, stanza, tag.name);
+ else -- Client requests change of active / default list
+ valid = activateList(privacy_lists, origin, stanza, tag.name, tag.attr.name);
+ end
+ elseif tag.name == "list" and tag.attr.name then -- Client adds / edits a privacy list
+ if #tag.tags == 0 then -- Client removes a privacy list
+ valid = deleteList(privacy_lists, origin, stanza, tag.attr.name);
+ else -- Client edits a privacy list
+ valid = createOrReplaceList(privacy_lists, origin, stanza, tag.attr.name, tag.tags);
+ end
+ end
+ end
+ end
elseif stanza.attr.type == "get" then
- if #query.tags == 0 then -- Client requests names of privacy lists from server
- -- TODO
- elseif #query.tags == 1 and query.tags[1].name == "list" then -- Client requests a privacy list from server
- -- TODO
- else
- origin.send(st.error_reply(stanza, "modify", "bad-request"));
+ local name = nil;
+ local listsToRetrieve = 0;
+ if #query.tags >= 1 then
+ for _,tag in ipairs(query.tags) do
+ if tag.name == "list" then -- Client requests a privacy list from server
+ name = tag.attr.name;
+ listsToRetrieve = listsToRetrieve + 1;
+ end
+ end
+ end
+ if listsToRetrieve == 0 or listsToRetrieve == 1 then
+ valid = getList(privacy_lists, origin, stanza, name);
+ end
+ end
+
+ if valid ~= true then
+ valid = valid or { "cancel", "bad-request", "Couldn't understand request" };
+ if valid[1] == nil then
+ valid[1] = "cancel";
+ end
+ if valid[2] == nil then
+ valid[2] = "bad-request";
end
+ origin.send(st.error_reply(stanza, valid[1], valid[2], valid[3]));
+ else
+ datamanager.store(origin.username, origin.host, "privacy", privacy_lists);
end
+ return true;
end
end);
+
+function checkIfNeedToBeBlocked(e, session)
+ local origin, stanza = e.origin, e.stanza;
+ local privacy_lists = datamanager.load(session.username, session.host, "privacy") or {};
+ local bare_jid = session.username.."@"..session.host;
+ local to = stanza.attr.to or bare_jid;
+ local from = stanza.attr.from;
+
+ local is_to_user = bare_jid == jid_bare(to);
+ local is_from_user = bare_jid == jid_bare(from);
+
+ --module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
+
+ if privacy_lists.lists == nil or
+ not (session.activePrivacyList or privacy_lists.default)
+ then
+ return; -- Nothing to block, default is Allow all
+ end
+ if is_from_user and is_to_user then
+ --module:log("debug", "Not blocking communications between user's resources");
+ return; -- from one of a user's resource to another => HANDS OFF!
+ end
+
+ local item;
+ local listname = session.activePrivacyList;
+ if listname == nil then
+ listname = privacy_lists.default; -- no active list selected, use default list
+ end
+ local list = privacy_lists.lists[listname];
+ if not list then -- should never happen
+ module:log("warn", "given privacy list not found. name: %s for user %s", listname, bare_jid);
+ return;
+ end
+ for _,item in ipairs(list.items) do
+ local apply = false;
+ local block = false;
+ if (
+ (stanza.name == "message" and item.message) or
+ (stanza.name == "iq" and item.iq) or
+ (stanza.name == "presence" and is_to_user and item["presence-in"]) or
+ (stanza.name == "presence" and is_from_user and item["presence-out"]) or
+ (item.message == false and item.iq == false and item["presence-in"] == false and item["presence-out"] == false)
+ ) then
+ apply = true;
+ end
+ if apply then
+ local evilJid = {};
+ apply = false;
+ if is_to_user then
+ --module:log("debug", "evil jid is (from): %s", from);
+ evilJid.node, evilJid.host, evilJid.resource = jid_split(from);
+ else
+ --module:log("debug", "evil jid is (to): %s", to);
+ evilJid.node, evilJid.host, evilJid.resource = jid_split(to);
+ end
+ if item.type == "jid" and
+ (evilJid.node and evilJid.host and evilJid.resource and item.value == evilJid.node.."@"..evilJid.host.."/"..evilJid.resource) or
+ (evilJid.node and evilJid.host and item.value == evilJid.node.."@"..evilJid.host) or
+ (evilJid.host and evilJid.resource and item.value == evilJid.host.."/"..evilJid.resource) or
+ (evilJid.host and item.value == evilJid.host) then
+ apply = true;
+ block = (item.action == "deny");
+ elseif item.type == "group" then
+ local roster = load_roster(session.username, session.host);
+ local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
+ if roster_entry then
+ local groups = roster_entry.groups;
+ for group in pairs(groups) do
+ if group == item.value then
+ apply = true;
+ block = (item.action == "deny");
+ break;
+ end
+ end
+ end
+ elseif item.type == "subscription" then -- we need a valid bare evil jid
+ local roster = load_roster(session.username, session.host);
+ local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
+ if (not(roster_entry) and item.value == "none")
+ or (roster_entry and roster_entry.subscription == item.value) then
+ apply = true;
+ block = (item.action == "deny");
+ end
+ elseif item.type == nil then
+ apply = true;
+ block = (item.action == "deny");
+ end
+ end
+ if apply then
+ if block then
+ module:log("debug", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
+ if stanza.name == "message" then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ elseif stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ end
+ return true; -- stanza blocked !
+ else
+ --module:log("debug", "stanza explicitly allowed!")
+ return;
+ end
+ end
+ end
+end
+
+function preCheckIncoming(e)
+ local session;
+ if e.stanza.attr.to ~= nil then
+ local node, host, resource = jid_split(e.stanza.attr.to);
+ if node == nil or host == nil then
+ return;
+ end
+ if resource == nil then
+ local prio = 0;
+ local session_;
+ if bare_sessions[node.."@"..host] ~= nil then
+ for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do
+ if session_.priority ~= nil and session_.priority > prio then
+ session = session_;
+ prio = session_.priority;
+ end
+ end
+ end
+ else
+ session = full_sessions[node.."@"..host.."/"..resource];
+ end
+ if session ~= nil then
+ return checkIfNeedToBeBlocked(e, session);
+ else
+ --module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource));
+ end
+ end
+end
+
+function preCheckOutgoing(e)
+ local session = e.origin;
+ if e.stanza.attr.from == nil then
+ e.stanza.attr.from = session.username .. "@" .. session.host;
+ if session.resource ~= nil then
+ e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource;
+ end
+ end
+ return checkIfNeedToBeBlocked(e, session);
+end
+
+module:hook("pre-message/full", preCheckOutgoing, 500);
+module:hook("pre-message/bare", preCheckOutgoing, 500);
+module:hook("pre-message/host", preCheckOutgoing, 500);
+module:hook("pre-iq/full", preCheckOutgoing, 500);
+module:hook("pre-iq/bare", preCheckOutgoing, 500);
+module:hook("pre-iq/host", preCheckOutgoing, 500);
+module:hook("pre-presence/full", preCheckOutgoing, 500);
+module:hook("pre-presence/bare", preCheckOutgoing, 500);
+module:hook("pre-presence/host", preCheckOutgoing, 500);
+
+module:hook("message/full", preCheckIncoming, 500);
+module:hook("message/bare", preCheckIncoming, 500);
+module:hook("message/host", preCheckIncoming, 500);
+module:hook("iq/full", preCheckIncoming, 500);
+module:hook("iq/bare", preCheckIncoming, 500);
+module:hook("iq/host", preCheckIncoming, 500);
+module:hook("presence/full", preCheckIncoming, 500);
+module:hook("presence/bare", preCheckIncoming, 500);
+module:hook("presence/host", preCheckIncoming, 500);
diff --git a/plugins/mod_private.lua b/plugins/mod_private.lua
index 859bf45a..abf1ec03 100644
--- a/plugins/mod_private.lua
+++ b/plugins/mod_private.lua
@@ -26,7 +26,11 @@ module:add_iq_handler("c2s", "jabber:iq:private",
if #query.tags == 1 then
local tag = query.tags[1];
local key = tag.name..":"..tag.attr.xmlns;
- local data = datamanager.load(node, host, "private");
+ local data, err = datamanager.load(node, host, "private");
+ if err then
+ session.send(st.error_reply(stanza, "wait", "internal-server-error"));
+ return true;
+ end
if stanza.attr.type == "get" then
if data and data[key] then
session.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"}):add_child(st.deserialize(data[key])));
diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua
new file mode 100644
index 00000000..190d30be
--- /dev/null
+++ b/plugins/mod_proxy65.lua
@@ -0,0 +1,279 @@
+-- Copyright (C) 2009 Thilo Cestonaro
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+--[[
+* to restart the proxy in the console: e.g.
+module:unload("proxy65");
+> server.removeserver(<proxy65_port>);
+module:load("proxy65", <proxy65_jid>);
+]]--
+
+if module:get_host_type() ~= "component" then
+ error("proxy65 should be loaded as a component, please see http://prosody.im/doc/components", 0);
+end
+
+local jid_split, jid_join = require "util.jid".split, require "util.jid".join;
+local st = require "util.stanza";
+local componentmanager = require "core.componentmanager";
+local config_get = require "core.configmanager".get;
+local connlisteners = require "net.connlisteners";
+local sha1 = require "util.hashes".sha1;
+local server = require "net.server";
+
+local host, name = module:get_host(), "SOCKS5 Bytestreams Service";
+local sessions, transfers, component, replies_cache = {}, {}, nil, {};
+
+local proxy_port = config_get(host, "core", "proxy65_port") or 5000;
+local proxy_interface = config_get(host, "core", "proxy65_interface") or "*";
+local proxy_address = config_get(host, "core", "proxy65_address") or (proxy_interface ~= "*" and proxy_interface) or host;
+local proxy_acl = config_get(host, "core", "proxy65_acl");
+local max_buffer_size = 4096;
+
+local connlistener = { default_port = proxy_port, default_interface = proxy_interface, default_mode = "*a" };
+
+function connlistener.onincoming(conn, data)
+ local session = sessions[conn] or {};
+
+ if session.setup == nil and data ~= nil and data:sub(1):byte() == 0x05 and data:len() > 2 then
+ local nmethods = data:sub(2):byte();
+ local methods = data:sub(3);
+ local supported = false;
+ for i=1, nmethods, 1 do
+ if(methods:sub(i):byte() == 0x00) then -- 0x00 == method: NO AUTH
+ supported = true;
+ break;
+ end
+ end
+ if(supported) then
+ module:log("debug", "new session found ... ")
+ session.setup = true;
+ sessions[conn] = session;
+ conn:write(string.char(5, 0));
+ end
+ return;
+ end
+ if session.setup then
+ if session.sha ~= nil and transfers[session.sha] ~= nil then
+ local sha = session.sha;
+ if transfers[sha].activated == true and transfers[sha].target ~= nil then
+ if transfers[sha].initiator == conn then
+ transfers[sha].target:write(data);
+ else
+ transfers[sha].initiator:write(data);
+ end
+ return;
+ end
+ end
+ if data ~= nil and data:len() == 0x2F and -- 40 == length of SHA1 HASH, and 7 other bytes => 47 => 0x2F
+ data:sub(1):byte() == 0x05 and -- SOCKS5 has 5 in first byte
+ data:sub(2):byte() == 0x01 and -- CMD must be 1
+ data:sub(3):byte() == 0x00 and -- RSV must be 0
+ data:sub(4):byte() == 0x03 and -- ATYP must be 3
+ data:sub(5):byte() == 40 and -- SHA1 HASH length must be 40 (0x28)
+ data:sub(-2):byte() == 0x00 and -- PORT must be 0, size 2 byte
+ data:sub(-1):byte() == 0x00
+ then
+ local sha = data:sub(6, 45); -- second param is not count! it's the ending index (included!)
+ if transfers[sha] == nil then
+ transfers[sha] = {};
+ transfers[sha].activated = false;
+ transfers[sha].target = conn;
+ session.sha = sha;
+ module:log("debug", "target connected ... ");
+ elseif transfers[sha].target ~= nil then
+ transfers[sha].initiator = conn;
+ session.sha = sha;
+ module:log("debug", "initiator connected ... ");
+ server.link(conn, transfers[sha].target, max_buffer_size);
+ server.link(transfers[sha].target, conn, max_buffer_size);
+ end
+ conn:write(string.char(5, 0, 0, 3, sha:len()) .. sha .. string.char(0, 0)); -- VER, REP, RSV, ATYP, BND.ADDR (sha), BND.PORT (2 Byte)
+ conn:lock_read(true)
+ else
+ module:log("warn", "Neither data transfer nor initial connect of a participator of a transfer.")
+ conn:close();
+ end
+ else
+ if data ~= nil then
+ module:log("warn", "unknown connection with no authentication data -> closing it");
+ conn:close();
+ end
+ end
+end
+
+function connlistener.ondisconnect(conn, err)
+ local session = sessions[conn];
+ if session then
+ if session.sha and transfers[session.sha] then
+ local initiator, target = transfers[session.sha].initiator, transfers[session.sha].target;
+ if initiator == conn and target ~= nil then
+ target:close();
+ elseif target == conn and initiator ~= nil then
+ initiator:close();
+ end
+ transfers[session.sha] = nil;
+ end
+ -- Clean up any session-related stuff here
+ sessions[conn] = nil;
+ end
+end
+
+local function get_disco_info(stanza)
+ local reply = replies_cache.disco_info;
+ if reply == nil then
+ reply = st.iq({type='result', from=host}):query("http://jabber.org/protocol/disco#info")
+ :tag("identity", {category='proxy', type='bytestreams', name=name}):up()
+ :tag("feature", {var="http://jabber.org/protocol/bytestreams"});
+ replies_cache.disco_info = reply;
+ end
+
+ reply.attr.id = stanza.attr.id;
+ reply.attr.to = stanza.attr.from;
+ return reply;
+end
+
+local function get_disco_items(stanza)
+ local reply = replies_cache.disco_items;
+ if reply == nil then
+ reply = st.iq({type='result', from=host}):query("http://jabber.org/protocol/disco#items");
+ replies_cache.disco_items = reply;
+ end
+
+ reply.attr.id = stanza.attr.id;
+ reply.attr.to = stanza.attr.from;
+ return reply;
+end
+
+local function get_stream_host(origin, stanza)
+ local reply = replies_cache.stream_host;
+ local err_reply = replies_cache.stream_host_err;
+ local sid = stanza.tags[1].attr.sid;
+ local allow = false;
+ local jid_node, jid_host, jid_resource = jid_split(stanza.attr.from);
+
+ if stanza.attr.from == nil then
+ jid_node = origin.username;
+ jid_host = origin.host;
+ jid_resource = origin.resource;
+ end
+
+ if proxy_acl and #proxy_acl > 0 then
+ if host ~= nil then -- at least a domain is needed.
+ for _, acl in ipairs(proxy_acl) do
+ local acl_node, acl_host, acl_resource = jid_split(acl);
+ if ((acl_node ~= nil and acl_node == jid_node) or acl_node == nil) and
+ ((acl_host ~= nil and acl_host == jid_host) or acl_host == nil) and
+ ((acl_resource ~= nil and acl_resource == jid_resource) or acl_resource == nil) then
+ allow = true;
+ end
+ end
+ end
+ else
+ allow = true;
+ end
+ if allow == true then
+ if reply == nil then
+ reply = st.iq({type="result", from=host})
+ :query("http://jabber.org/protocol/bytestreams")
+ :tag("streamhost", {jid=host, host=proxy_address, port=proxy_port});
+ replies_cache.stream_host = reply;
+ end
+ else
+ module:log("warn", "Denying use of proxy for %s", tostring(jid_join(jid_node, jid_host, jid_resource)));
+ if err_reply == nil then
+ err_reply = st.iq({type="error", from=host})
+ :query("http://jabber.org/protocol/bytestreams")
+ :tag("error", {code='403', type='auth'})
+ :tag("forbidden", {xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'});
+ replies_cache.stream_host_err = err_reply;
+ end
+ reply = err_reply;
+ end
+ reply.attr.id = stanza.attr.id;
+ reply.attr.to = stanza.attr.from;
+ reply.tags[1].attr.sid = sid;
+ return reply;
+end
+
+module.unload = function()
+ componentmanager.deregister_component(host);
+ connlisteners.deregister(module.host .. ':proxy65');
+end
+
+local function set_activation(stanza)
+ local from, to, sid, reply = nil;
+ from = stanza.attr.from;
+ if stanza.tags[1] ~= nil and tostring(stanza.tags[1].name) == "query" then
+ if stanza.tags[1].attr ~= nil then
+ sid = stanza.tags[1].attr.sid;
+ end
+ if stanza.tags[1].tags[1] ~= nil and tostring(stanza.tags[1].tags[1].name) == "activate" then
+ to = stanza.tags[1].tags[1][1];
+ end
+ end
+ if from ~= nil and to ~= nil and sid ~= nil then
+ reply = st.iq({type="result", from=host, to=from});
+ reply.attr.id = stanza.attr.id;
+ end
+ return reply, from, to, sid;
+end
+
+function handle_to_domain(origin, stanza)
+ local to_node, to_host, to_resource = jid_split(stanza.attr.to);
+ if to_node == nil then
+ 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));
+ return true;
+ elseif xmlns == "http://jabber.org/protocol/disco#items" then
+ origin.send(get_disco_items(stanza));
+ return true;
+ elseif xmlns == "http://jabber.org/protocol/bytestreams" then
+ origin.send(get_stream_host(origin, stanza));
+ return true;
+ else
+ origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
+ return true;
+ end
+ elseif stanza.name == "iq" and type == "set" then
+ module:log("debug", "Received activation request from %s", stanza.attr.from);
+ local reply, from, to, sid = set_activation(stanza);
+ if reply ~= nil and from ~= nil and to ~= nil and sid ~= nil then
+ local sha = sha1(sid .. from .. to, true);
+ if transfers[sha] == nil then
+ module:log("error", "transfers[sha]: nil");
+ elseif(transfers[sha] ~= nil and transfers[sha].initiator ~= nil and transfers[sha].target ~= nil) then
+ origin.send(reply);
+ transfers[sha].activated = true;
+ transfers[sha].target:lock_read(false);
+ transfers[sha].initiator:lock_read(false);
+ else
+ module:log("debug", "Both parties were not yet connected");
+ local message = "Neither party is connected to the proxy";
+ if transfers[sha].initiator then
+ message = "The recipient is not connected to the proxy";
+ elseif transfers[sha].target then
+ message = "The sender (you) is not connected to the proxy";
+ end
+ origin.send(st.error_reply(stanza, "cancel", "not-allowed", message));
+ end
+ else
+ module:log("error", "activation failed: sid: %s, initiator: %s, target: %s", tostring(sid), tostring(from), tostring(to));
+ end
+ end
+ end
+ return;
+end
+
+if not connlisteners.register(module.host .. ':proxy65', connlistener) then
+ module:log("error", "mod_proxy65: Could not establish a connection listener. Check your configuration please.");
+ module:log("error", "Possibly two proxy65 components are configured to share the same port.");
+end
+
+connlisteners.start(module.host .. ':proxy65');
+component = componentmanager.register_component(host, handle_to_domain);
diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua
index 2a25d1d0..2818e336 100644
--- a/plugins/mod_register.lua
+++ b/plugins/mod_register.lua
@@ -12,6 +12,7 @@ local st = require "util.stanza";
local datamanager = require "util.datamanager";
local usermanager_user_exists = require "core.usermanager".user_exists;
local usermanager_create_user = require "core.usermanager".create_user;
+local usermanager_set_password = require "core.usermanager".set_password;
local datamanager_store = require "util.datamanager".store;
local os_time = os.time;
local nodeprep = require "util.encodings".stringprep.nodeprep;
@@ -34,7 +35,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza)
local username, host = session.username, session.host;
--session.send(st.error_reply(stanza, "cancel", "not-allowed"));
--return;
- usermanager_create_user(username, nil, host); -- Disable account
+ --usermanager_set_password(username, host, nil); -- Disable account
-- FIXME the disabling currently allows a different user to recreate the account
-- we should add an in-memory account block mode when we have threading
session.send(st.reply(stanza));
@@ -45,7 +46,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza)
-- TODO datamanager should be able to delete all user data itself
datamanager.store(username, host, "vcard", nil);
datamanager.store(username, host, "private", nil);
- datamanager.store(username, host, "offline", nil);
+ datamanager.list_store(username, host, "offline", nil);
local bare = username.."@"..host;
for jid, item in pairs(roster) do
if jid and jid ~= "pending" then
@@ -58,6 +59,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza)
end
end
datamanager.store(username, host, "roster", nil);
+ datamanager.store(username, host, "privacy", nil);
datamanager.store(username, host, "accounts", nil); -- delete accounts datastore at the end
module:log("info", "User removed their account: %s@%s", username, host);
module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session });
@@ -69,7 +71,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza)
username = nodeprep(table.concat(username));
password = table.concat(password);
if username == session.username then
- if usermanager_create_user(username, password, session.host) then -- password change -- TODO is this the right way?
+ if usermanager_set_password(username, session.host, password) then
session.send(st.reply(stanza));
else
-- TODO unable to write file, file may be locked, etc, what's the correct error?
diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua
index 755d263d..ddf02f2f 100644
--- a/plugins/mod_roster.lua
+++ b/plugins/mod_roster.lua
@@ -23,12 +23,12 @@ local core_post_stanza = core_post_stanza;
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)
- if session.username then
- features:add_child(rosterver_stream_feature);
- end
- end);
+module:hook("stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if origin.username then
+ features:add_child(rosterver_stream_feature);
+ end
+end);
module:add_iq_handler("c2s", "jabber:iq:roster",
function (session, stanza)
@@ -36,9 +36,10 @@ module:add_iq_handler("c2s", "jabber:iq:roster",
if stanza.attr.type == "get" then
local roster = st.reply(stanza);
- local ver = stanza.tags[1].attr.ver
+ local client_ver = tonumber(stanza.tags[1].attr.ver);
+ local server_ver = tonumber(session.roster[false].version or 1);
- if (not ver) or tonumber(ver) ~= (session.roster[false].version or 1) then
+ if not (client_ver and server_ver) or client_ver ~= server_ver then
roster:query("jabber:iq:roster");
-- Client does not support versioning, or has stale roster
for jid in pairs(session.roster) do
@@ -55,7 +56,7 @@ module:add_iq_handler("c2s", "jabber:iq:roster",
roster:up(); -- move out from item
end
end
- roster.tags[1].attr.ver = tostring(session.roster[false].version or "1");
+ roster.tags[1].attr.ver = server_ver;
end
session.send(roster);
session.interested = true; -- resource is interested in roster updates
diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua
index 85481ad7..d407e5da 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -21,11 +21,18 @@ local usermanager_user_exists = require "core.usermanager".user_exists;
local usermanager_get_password = require "core.usermanager".get_password;
local t_concat, t_insert = table.concat, table.insert;
local tostring = tostring;
-local jid_split = require "util.jid".split
+local 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", "c2s_require_encryption") or 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 sasl_backend = module:get_option("sasl_backend") or "builtin";
+
+-- Cyrus config options
+local require_provisioning = module:get_option("cyrus_require_provisioning") or false;
+local cyrus_service_realm = module:get_option("cyrus_service_realm");
+local cyrus_service_name = module:get_option("cyrus_service_name");
+local cyrus_application_name = module:get_option("cyrus_application_name");
local log = module._log;
@@ -33,7 +40,52 @@ local xmlns_sasl ='urn:ietf:params:xml:ns:xmpp-sasl';
local xmlns_bind ='urn:ietf:params:xml:ns:xmpp-bind';
local xmlns_stanzas ='urn:ietf:params:xml:ns:xmpp-stanzas';
-local new_sasl = require "util.sasl".new;
+local new_sasl;
+if sasl_backend == "builtin" then
+ new_sasl = require "util.sasl".new;
+elseif sasl_backend == "cyrus" then
+ prosody.unlock_globals(); --FIXME: Figure out why this is needed and
+ -- why cyrussasl isn't caught by the sandbox
+ local ok, cyrus = pcall(require, "util.sasl_cyrus");
+ prosody.lock_globals();
+ if ok then
+ local cyrus_new = cyrus.new;
+ new_sasl = function(realm)
+ return cyrus_new(
+ cyrus_service_realm or realm,
+ cyrus_service_name or "xmpp",
+ cyrus_application_name or "prosody"
+ );
+ end
+ else
+ module:log("error", "Failed to load Cyrus SASL because: %s", cyrus);
+ error("Failed to load Cyrus SASL");
+ end
+else
+ module:log("error", "Unknown SASL backend: %s", sasl_backend);
+ error("Unknown SASL backend");
+end
+
+local default_authentication_profile = {
+ plain = function(username, realm)
+ local prepped_username = nodeprep(username);
+ if not prepped_username then
+ log("debug", "NODEprep failed on username: %s", username);
+ return "", nil;
+ end
+ local password = usermanager_get_password(prepped_username, realm);
+ if not password then
+ return "", nil;
+ end
+ return password, true;
+ end
+};
+
+local anonymous_authentication_profile = {
+ anonymous = function(username, realm)
+ return true; -- for normal usage you should always return true here
+ end
+};
local function build_reply(status, ret, err_msg)
local reply = st.stanza(status, {xmlns = xmlns_sasl});
@@ -52,53 +104,29 @@ local function build_reply(status, ret, err_msg)
return reply;
end
-local function handle_status(session, status)
+local function handle_status(session, status, ret, err_msg)
if status == "failure" then
- session.sasl_handler = nil;
+ session.sasl_handler = session.sasl_handler:clean_clone();
elseif status == "success" then
local username = nodeprep(session.sasl_handler.username);
- 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
- sm_make_authenticated(session, username);
- session:reset_stream();
- end
-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
- local 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);
+ if not(require_provisioning) or usermanager_user_exists(username, session.host) then
+ local aret, err = sm_make_authenticated(session, session.sasl_handler.username);
+ if aret then
+ session.sasl_handler = nil;
+ session:reset_stream();
+ else
+ module:log("warn", "SASL succeeded but username was invalid");
+ session.sasl_handler = session.sasl_handler:clean_clone();
+ return "failure", "not-authorized", "User authenticated successfully, but username was invalid";
end
- return func, md5(node..":"..realm..":"..password);
else
- return func, nil;
+ module:log("warn", "SASL succeeded but we don't have an account provisioned for %s", username);
+ session.sasl_handler = session.sasl_handler:clean_clone();
+ return "failure", "not-authorized", "User authenticated successfully, but not provisioned for XMPP";
end
end
+ return status, ret, err_msg;
end
local function sasl_handler(session, stanza)
@@ -111,8 +139,8 @@ 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, credentials_callback);
- if not session.sasl_handler then
+ local valid_mechanism = session.sasl_handler:select(stanza.attr.mechanism);
+ if not valid_mechanism then
return session.send(build_reply("failure", "invalid-mechanism"));
end
if secure_auth_only and not session.secure then
@@ -131,8 +159,8 @@ local function sasl_handler(session, stanza)
return;
end
end
- local status, ret, err_msg = session.sasl_handler:feed(text);
- handle_status(session, status);
+ local status, ret, err_msg = session.sasl_handler:process(text);
+ status, ret, err_msg = handle_status(session, status, ret, err_msg);
local s = build_reply(status, ret, err_msg);
log("debug", "sasl reply: %s", tostring(s));
session.send(s);
@@ -145,54 +173,55 @@ 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)
- if not session.username then
- if secure_auth_only and not session.secure then
- return;
- 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
- local 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
- features:tag("bind", bind_attr):tag("required"):up():up();
- features:tag("session", xmpp_session_attr):tag("optional"):up():up();
- end
- end);
-
-module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind",
- function (session, stanza)
- log("debug", "Client requesting a resource bind");
- local resource;
- if stanza.attr.type == "set" then
- local bind = stanza.tags[1];
- if bind and bind.attr.xmlns == xmlns_bind then
- resource = bind:child_with_name("resource");
- if resource then
- resource = resource[1];
- end
- end
+module:hook("stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if not origin.username then
+ if secure_auth_only and not origin.secure then
+ return;
+ end
+ local realm = module:get_option("sasl_realm") or origin.host;
+ if module:get_option("anonymous_login") then
+ origin.sasl_handler = new_sasl(realm, anonymous_authentication_profile);
+ else
+ origin.sasl_handler = new_sasl(realm, default_authentication_profile);
+ if not (module:get_option("allow_unencrypted_plain_auth")) and not origin.secure then
+ origin.sasl_handler:forbidden({"PLAIN"});
end
- local success, err_type, err, err_msg = sm_bind_resource(session, resource);
- if not success then
- session.send(st.error_reply(stanza, err_type, err, err_msg));
- else
- session.send(st.reply(stanza)
- :tag("bind", { xmlns = xmlns_bind})
- :tag("jid"):text(session.full_jid));
+ end
+ features:tag("mechanisms", mechanisms_attr);
+ for k, v in pairs(origin.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):tag("optional"):up():up();
+ end
+end);
+
+module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind", function(session, stanza)
+ log("debug", "Client requesting a resource bind");
+ local resource;
+ if stanza.attr.type == "set" then
+ local bind = stanza.tags[1];
+ if bind and bind.attr.xmlns == xmlns_bind then
+ resource = bind:child_with_name("resource");
+ if resource then
+ resource = resource[1];
end
- end);
+ end
+ end
+ local success, err_type, err, err_msg = sm_bind_resource(session, resource);
+ if not success then
+ session.send(st.error_reply(stanza, err_type, err, err_msg));
+ else
+ session.send(st.reply(stanza)
+ :tag("bind", { xmlns = xmlns_bind})
+ :tag("jid"):text(session.full_jid));
+ end
+end);
-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));
- end);
+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));
+end);
diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua
index f68552fa..8b96aa15 100644
--- a/plugins/mod_tls.lua
+++ b/plugins/mod_tls.lua
@@ -8,105 +8,82 @@
local st = require "util.stanza";
-local xmlns_stream = 'http://etherx.jabber.org/streams';
-local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls';
-
local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
local secure_s2s_only = module:get_option("s2s_require_encryption");
+local allow_s2s_tls = module:get_option("s2s_allow_encryption") ~= false;
-local host = hosts[module.host];
-
+local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls';
local starttls_attr = { xmlns = xmlns_starttls };
+local starttls_proceed = st.stanza("proceed", starttls_attr);
+local starttls_failure = st.stanza("failure", starttls_attr);
+local c2s_feature = st.stanza("starttls", starttls_attr);
+local s2s_feature = st.stanza("starttls", starttls_attr);
+if secure_auth_only then c2s_feature:tag("required"):up(); end
+if secure_s2s_only then s2s_feature:tag("required"):up(); end
---- Client-to-server TLS handling
-module:add_handler("c2s_unauthed", "starttls", xmlns_starttls,
- function (session, stanza)
- if session.conn.starttls and host.ssl_ctx_in then
- session.send(st.stanza("proceed", starttls_attr));
- 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;
- else
- session.log("warn", "Attempt to start TLS, but TLS is not available on this connection");
- (session.sends2s or session.send)(st.stanza("failure", starttls_attr));
- session:close();
- end
- end);
+local global_ssl_ctx = prosody.global_ssl_ctx;
-module:add_event_hook("stream-features",
- function (session, features)
- if session.conn.starttls then
- features:tag("starttls", starttls_attr);
- if secure_auth_only then
- features:tag("required"):up():up();
- else
- features:up();
- end
- end
- end);
----
+local host = hosts[module.host];
--- Stop here if the user doesn't want to allow s2s encryption
-if module:get_option("s2s_allow_encryption") == false then
- return;
+local function can_do_tls(session)
+ if session.type == "c2s_unauthed" then
+ return session.conn.starttls and host.ssl_ctx_in;
+ elseif session.type == "s2sin_unauthed" and allow_s2s_tls then
+ return session.conn.starttls and host.ssl_ctx_in;
+ elseif session.direction == "outgoing" and allow_s2s_tls then
+ return session.conn.starttls and host.ssl_ctx;
+ end
+ return false;
end
---- Server-to-server TLS handling
-module:add_handler("s2sin_unauthed", "starttls", xmlns_starttls,
- function (session, stanza)
- if session.conn.starttls and host.ssl_ctx_in then
- session.sends2s(st.stanza("proceed", starttls_attr));
- 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
- session.log("warn", "Attempt to start TLS, but TLS is not available on this s2s connection");
- (session.sends2s or session.send)(st.stanza("failure", starttls_attr));
- session:close();
- end
- end);
-
+-- Hook <starttls/>
+module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event)
+ local origin = event.origin;
+ if can_do_tls(origin) then
+ (origin.sends2s or origin.send)(starttls_proceed);
+ origin:reset_stream();
+ local host = origin.to_host or origin.host;
+ local ssl_ctx = host and hosts[host].ssl_ctx_in or global_ssl_ctx;
+ origin.conn:starttls(ssl_ctx);
+ origin.log("info", "TLS negotiation started for %s...", origin.type);
+ origin.secure = false;
+ else
+ origin.log("warn", "Attempt to start TLS, but TLS is not available on this %s connection", origin.type);
+ (origin.sends2s or origin.send)(starttls_failure);
+ origin:close();
+ end
+ return true;
+end);
-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);
- if secure_s2s_only then
- features:tag("required"):up():up();
- else
- features:up();
- end
- end
- end);
+-- Advertize stream feature
+module:hook("stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if can_do_tls(origin) then
+ features:add_child(c2s_feature);
+ end
+end);
+module:hook("s2s-stream-features", function(event)
+ local origin, features = event.origin, event.features;
+ if can_do_tls(origin) then
+ features:add_child(s2s_feature);
+ end
+end);
-- For s2sout connections, start TLS if we can
-module:hook_stanza(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("http://etherx.jabber.org/streams", "features", function (session, stanza)
+ module:log("debug", "Received features element");
+ if can_do_tls(session) and stanza:child_with_ns(xmlns_starttls) then
+ module:log("%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;
- local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx;
- session.conn.set_sslctx(ssl_ctx);
- session:reset_stream();
- session.conn.starttls(true);
- session.secure = false;
- return true;
- end);
+module:hook_stanza(xmlns_starttls, "proceed", function (session, stanza)
+ module:log("debug", "Proceeding with TLS on s2sout...");
+ session:reset_stream();
+ local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx;
+ session.conn:starttls(ssl_ctx, true);
+ session.secure = false;
+ return true;
+end);
diff --git a/plugins/mod_xmlrpc.lua b/plugins/mod_xmlrpc.lua
deleted file mode 100644
index 92f296c5..00000000
--- a/plugins/mod_xmlrpc.lua
+++ /dev/null
@@ -1,128 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-module.host = "*" -- Global module
-
-local httpserver = require "net.httpserver";
-local st = require "util.stanza";
-local pcall = pcall;
-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;
-
-local translate_request = require "util.xmlrpc".translate_request;
-local create_response = require "util.xmlrpc".create_response;
-local create_error_response = require "util.xmlrpc".create_error_response;
-
-local entity_map = setmetatable({
- ["amp"] = "&";
- ["gt"] = ">";
- ["lt"] = "<";
- ["apos"] = "'";
- ["quot"] = "\"";
-}, {__index = function(_, s)
- if s:sub(1,1) == "#" then
- if s:sub(2,2) == "x" then
- return string.char(tonumber(s:sub(3), 16));
- else
- return string.char(tonumber(s:sub(2)));
- end
- end
- end
-});
-local function xml_unescape(str)
- return (str:gsub("&(.-);", entity_map));
-end
-local function parse_xml(xml)
- local stanza = st.stanza("root");
- local regexp = "<([^>]*)>([^<]*)";
- for elem, text in xml:gmatch(regexp) do
- --print("[<"..elem..">|"..text.."]");
- if elem:sub(1,1) == "!" or elem:sub(1,1) == "?" then -- neglect comments and processing-instructions
- elseif elem:sub(1,1) == "/" then -- end tag
- elem = elem:sub(2);
- stanza:up(); -- TODO check for start-end tag name match
- elseif elem:sub(-1,-1) == "/" then -- empty tag
- elem = elem:sub(1,-2);
- stanza:tag(elem):up();
- else -- start tag
- stanza:tag(elem);
- end
- if #text ~= 0 then -- text
- stanza:text(xml_unescape(text));
- end
- end
- return stanza.tags[1];
-end
-
-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");
- if success then
- return result;
- end
- return create_error_response(500, "Error in creating response: "..result);
- end
- 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
- 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
-module:add_iq_handler({"c2s", "s2sin"}, "jabber:iq:rpc", handle_xmpp_request);
-module:add_feature("jabber:iq:rpc");
--- TODO add <identity category='automation' type='rpc'/> to disco replies
-
-local default_headers = { ['Content-Type'] = 'text/xml' };
-local unauthorized_response = { status = '401 UNAUTHORIZED', headers = {['Content-Type']='text/html', ['WWW-Authenticate']='Basic realm="WallyWorld"'}; body = "<html><body>Authentication required</body></html>"; };
-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) then
- return unauthorized_response;
- end
- -- parse request
- local stanza = body and parse_xml(body);
- if (not stanza) or request.method ~= "POST" then
- return "<html><body>You really don't look like an XML-RPC client to me... what do you want?</body></html>";
- end
- -- execute request
- local success, method, args = pcall(translate_request, stanza);
- if success then
- 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
-httpserver.new{ port = 9000, base = "xmlrpc", handler = handle_http_request }
diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua
index 680b902a..de23aebb 100644
--- a/plugins/muc/mod_muc.lua
+++ b/plugins/muc/mod_muc.lua
@@ -16,7 +16,6 @@ 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;
diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua
index 75289cc1..18c80325 100644
--- a/plugins/muc/muc.lib.lua
+++ b/plugins/muc/muc.lib.lua
@@ -59,19 +59,12 @@ local kickable_error_conditions = {
["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";
+ local _, condition = stanza:get_error();
+ return condition or "malformed error";
end
+
local function is_kickable_error(stanza)
local cond = get_error_condition(stanza);
return kickable_error_conditions[cond] and cond;
@@ -89,17 +82,6 @@ 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;
@@ -183,12 +165,12 @@ function room_mt:send_history(to)
end
end
-local function room_get_disco_info(self, stanza)
+function room_mt:get_disco_info(stanza)
return st.reply(stanza):query("http://jabber.org/protocol/disco#info")
:tag("identity", {category="conference", type="text"}):up()
:tag("feature", {var="http://jabber.org/protocol/muc"});
end
-local function room_get_disco_items(self, stanza)
+function room_mt:get_disco_items(stanza)
local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
for room_jid in pairs(self._occupants) do
reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up();
@@ -206,6 +188,16 @@ function room_mt:set_subject(current_nick, subject)
return true;
end
+local function build_unavailable_presence_from_error(stanza)
+ local type, condition, text = stanza:get_error();
+ local error_message = "Kicked: "..condition:gsub("%-", " ");
+ if text then
+ error_message = error_message..": "..text;
+ end
+ return st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to})
+ :tag('status'):text(error_message);
+end
+
function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
local from, to = stanza.attr.from, stanza.attr.to;
local room = jid_bare(to);
@@ -219,8 +211,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
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
+ self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza));
end
elseif type == "unavailable" then -- unavailable
if current_nick then
@@ -328,6 +319,11 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
:tag("item", {affiliation=affiliation or "none", role=role or "none"}):up()
:tag("status", {code='110'}));
end
+ if self._data.whois == 'anyone' then -- non-anonymous?
+ self:_route_stanza(st.stanza("message", {from=to, to=from, type='groupchat'})
+ :tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
+ :tag("status", {code='100'}));
+ end
self:send_history(from);
else -- banned
local reply = st.error_reply(stanza, "auth", "forbidden"):up();
@@ -367,8 +363,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
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
+ self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable
else -- private stanza
local o_data = self._occupants[to];
if o_data then
@@ -389,51 +384,112 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
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()
+function room_mt:send_form(origin, stanza)
+ 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()
+ :tag("field", {type='list-single', label='Who May Discover Real JIDs?', var='muc#roomconfig_whois'})
+ :tag("value"):text(self._data.whois or 'moderators'):up()
+ :tag("option", {label = 'Moderators Only'})
+ :tag("value"):text('moderators'):up()
:up()
- :tag("field", {type='boolean', label='Make Room Publicly Searchable?', var='muc#roomconfig_publicroom'})
- :tag("value"):text(self._data.hidden and "0" or "1"):up()
+ :tag("option", {label = 'Anyone'})
+ :tag("value"):text('anyone'):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
+ :up()
+ );
+end
+
+local valid_whois = {
+ moderators = true,
+ anyone = true,
+}
+
+function room_mt:process_form(origin, stanza)
+ local query = stanza.tags[1];
+ local form;
+ for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end
+ if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end
+ if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end
+ if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); 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
- if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; 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 dirty = false
- 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 persistent = fields['muc#roomconfig_persistentroom'];
+ if persistent == "0" or persistent == "false" then persistent = nil; elseif persistent == "1" or persistent == "true" then persistent = true;
+ else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ dirty = dirty or (self._data.persistent ~= persistent)
+ self._data.persistent = persistent;
+ 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;
+ local public = fields['muc#roomconfig_publicroom'];
+ if public == "0" or public == "false" then public = nil; elseif public == "1" or public == "true" then public = true;
+ else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
+ dirty = dirty or (self._data.hidden ~= (not public and true or nil))
+ self._data.hidden = not public and true or nil;
- if self.save then self:save(true); end
- origin.send(st.reply(stanza));
+ local whois = fields['muc#roomconfig_whois'];
+ if not valid_whois[whois] then
+ origin.send(st.error_reply(stanza, 'cancel', 'bad-request'));
+ return;
end
+ local whois_changed = self._data.whois ~= whois
+ self._data.whois = whois
+ module:log('debug', 'whois=%s', tostring(whois))
+
+ if self.save then self:save(true); end
+ origin.send(st.reply(stanza));
+
+ if dirty or whois_changed then
+ local msg = st.message({type='groupchat', from=self.jid})
+ :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up()
+
+ if dirty then
+ msg.tags[1]:tag('status', {code = '104'})
+ end
+ if whois_changed then
+ local code = (whois == 'moderators') and 173 or 172
+ msg.tags[1]:tag('status', {code = code})
+ end
+
+ self:broadcast_message(msg, false)
+ end
+end
+
+function room_mt:destroy(newjid, reason, password)
+ local pr = st.presence({type = "unavailable"})
+ :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
+ :tag("item", { affiliation='none', role='none' }):up()
+ :tag("destroy", {jid=newjid})
+ if reason then pr:tag("reason"):text(reason):up(); end
+ if password then pr:tag("password"):text(password):up(); end
+ for nick, occupant in pairs(self._occupants) do
+ pr.attr.from = nick;
+ for jid in pairs(occupant.sessions) do
+ pr.attr.to = jid;
+ self:_route_stanza(pr);
+ self._jid_nick[jid] = nil;
+ end
+ self._occupants[nick] = nil;
+ end
+ self._data.persistent = nil;
+ if self.save then self:save(true); end
end
function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
@@ -441,9 +497,9 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
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));
+ origin.send(self:get_disco_info(stanza));
elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" then
- origin.send(room_get_disco_items(self, stanza));
+ origin.send(self:get_disco_items(stanza));
elseif xmlns == "http://jabber.org/protocol/muc#admin" then
local actor = stanza.attr.from;
local affiliation = self:get_affiliation(actor);
@@ -519,7 +575,30 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
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);
+ if self:get_affiliation(stanza.attr.from) ~= "owner" then
+ origin.send(st.error_reply(stanza, "auth", "forbidden"));
+ elseif stanza.attr.type == "get" then
+ self:send_form(origin, stanza);
+ elseif stanza.attr.type == "set" then
+ local child = stanza.tags[1].tags[1];
+ if not child then
+ origin.send(st.error_reply(stanza, "auth", "bad-request"));
+ elseif child.name == "destroy" then
+ local newjid = child.attr.jid;
+ local reason, password;
+ for _,tag in ipairs(child.tags) do
+ if tag.name == "reason" then
+ reason = #tag.tags == 0 and tag[1];
+ elseif tag.name == "password" then
+ password = #tag.tags == 0 and tag[1];
+ end
+ end
+ self:destroy(newjid, reason, password);
+ origin.send(st.reply(stanza));
+ else
+ self:process_form(origin, stanza);
+ end
+ end
elseif type == "set" or type == "get" then
origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
end
@@ -551,8 +630,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
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
+ self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable
elseif stanza.name == "presence" then -- hack - some buggy clients send presence updates to the room rather than their nick
local to = stanza.attr.to;
local current_nick = self._jid_nick[stanza.attr.from];
@@ -707,13 +785,11 @@ function room_mt:_route_stanza(stanza)
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
+ if self._data.whois == 'anyone' then
+ muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user");
+ else
+ if to_occupant.role == "moderator" or jid_bare(to_occupant.jid) == jid_bare(from_occupant.jid) then
+ muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user");
end
end
end
@@ -746,7 +822,9 @@ function _M.new_room(jid)
jid = jid;
_jid_nick = {};
_occupants = {};
- _data = {};
+ _data = {
+ whois = 'moderators',
+ };
_affiliations = {};
}, room_mt);
end