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_bosh.lua52
-rw-r--r--plugins/mod_component.lua15
-rw-r--r--plugins/mod_compression.lua210
-rw-r--r--plugins/mod_console.lua18
-rw-r--r--plugins/mod_disco.lua53
-rw-r--r--plugins/mod_httpserver.lua37
-rw-r--r--plugins/mod_pep.lua111
-rw-r--r--plugins/mod_posix.lua49
-rw-r--r--plugins/mod_presence.lua30
-rw-r--r--plugins/mod_privacy.lua527
-rw-r--r--plugins/mod_proxy65.lua286
-rw-r--r--plugins/mod_register.lua20
-rw-r--r--plugins/mod_roster.lua16
-rw-r--r--plugins/mod_saslauth.lua150
-rw-r--r--plugins/mod_tls.lua18
-rw-r--r--plugins/mod_vcard.lua2
-rw-r--r--plugins/mod_xmlrpc.lua128
-rw-r--r--plugins/muc/mod_muc.lua1
-rw-r--r--plugins/muc/muc.lib.lua274
20 files changed, 1533 insertions, 550 deletions
diff --git a/plugins/mod_actions_http.lua b/plugins/mod_actions_http.lua
deleted file mode 100644
index c6069793..00000000
--- a/plugins/mod_actions_http.lua
+++ /dev/null
@@ -1,86 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-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_bosh.lua b/plugins/mod_bosh.lua
index af13bde9..f25e7670 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 = xmlns_bosh };
+local stream_callbacks = { stream_ns = "http://jabber.org/protocol/httpbind", stream_tag = "body", default_ns = xmlns_bosh };
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;
@@ -34,6 +34,22 @@ local BOSH_DEFAULT_MAXPAUSE = tonumber(module:get_option("bosh_max_pause")) or 3
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;
@@ -61,9 +77,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
@@ -152,7 +172,7 @@ function stream_callbacks.streamopened(request, attr)
local r, send_buffer = session.requests, session.send_buffer;
local response = { headers = default_headers }
function session.send(s)
- log("debug", "Sending BOSH data: %s", tostring(s));
+ --log("debug", "Sending BOSH data: %s", tostring(s));
local oldest_request = r[1];
while oldest_request and oldest_request.destroyed do
t_remove(r, 1);
@@ -160,7 +180,7 @@ function stream_callbacks.streamopened(request, attr)
oldest_request = r[1];
end
if oldest_request then
- log("debug", "We have an open request, so using that to send with");
+ log("debug", "We have an open request, so sending on that");
response.body = t_concat{"<body xmlns='http://jabber.org/protocol/httpbind' sid='", sid, "' xmlns:stream = 'http://etherx.jabber.org/streams'>", tostring(s), "</body>" };
oldest_request:send(response);
--log("debug", "Sent");
@@ -188,12 +208,12 @@ function stream_callbacks.streamopened(request, attr)
local features = st.stanza("stream: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
@@ -254,6 +274,7 @@ function stream_callbacks.handlestanza(request, stanza)
if stanza.attr.xmlns == xmlns_bosh then
stanza.attr.xmlns = "jabber:client";
end
+ session.ip = request.handler:ip();
core_process_stanza(session, stanza);
end
end
@@ -297,7 +318,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 69a42eaf..d9783b0c 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");
@@ -44,7 +33,7 @@ function handle_component_auth(session, stanza)
local secret = config.get(session.user, "core", "component_secret");
if not secret then
- (session.log or log)("warn", "Component attempted to identify as %s, but component_password is not set", session.user);
+ (session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.user);
session:close("not-authorized");
return;
end
@@ -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 f1cae737..638b8e13 100644
--- a/plugins/mod_compression.lua
+++ b/plugins/mod_compression.lua
@@ -8,16 +8,16 @@
local st = require "util.stanza";
local zlib = require "zlib";
local pcall = pcall;
-
local xmlns_compression_feature = "http://jabber.org/features/compress"
local xmlns_compression_protocol = "http://jabber.org/protocol/compress"
+local 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");
-
-- if not defined assume admin wants best compression
if compression_level == nil then compression_level = 9 end;
+
compression_level = tonumber(compression_level);
if not compression_level or compression_level < 1 or compression_level > 9 then
module:log("warn", "Invalid compression level in config: %s", tostring(compression_level));
@@ -34,89 +34,179 @@ module:add_event_hook("stream-features",
end
);
--- TODO Support compression on S2S level too.
-module:add_handler({"c2s_unauthed", "c2s"}, "compress", xmlns_compression_protocol,
+module:hook("s2s-stream-features",
+ function (data)
+ local session, features = data.session, data.features;
+ -- FIXME only advertise compression support when TLS layer has no compression enabled
+ if not session.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
+ -- 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("info", "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", 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.deflate filter.");
+ module:log("error", 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
+ session.log(t)
+ local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
+ if status == false then
+ session:close({
+ condition = "undefined-condition";
+ text = compressed;
+ extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
+ });
+ module:log("warn", compressed);
+ return;
+ end
+ 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", 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
+);
+
+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("unsupported-method");
- session.send(error_st);
- session:log("warn", "Tried to establish another compression layer.");
+ (session.sends2s or session.send)(error_st);
+ session.log("warn", "Tried to establish another compression layer.");
end
-- checking if the compression method is supported
local method = stanza:child_with_name("method")[1];
if method == "zlib" then
- session.log("info", method.." compression selected.");
- session.send(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
- session:reset_stream();
+ session.log("debug", method.." compression selected.");
-- create deflate and inflate streams
- local status, deflate_stream = pcall(zlib.deflate, compression_level);
- if status == false then
- local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed");
- session.send(error_st);
- session:log("error", "Failed to create zlib.deflate filter.");
- module:log("error", deflate_stream);
- return
- end
+ local 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.deflate filter.");
- module:log("error", inflate_stream);
- return
- end
+ local inflate_stream = get_inflate_stream(session);
+ if not inflate_stream then return end
- -- setup compression for session.w
- local old_send = session.send;
+ (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
+ session:reset_stream();
- session.send = function(t)
- local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync');
- if status == false then
- session:close({
- condition = "undefined-condition";
- text = compressed;
- extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed");
- });
- module:log("warn", compressed);
- return;
- end
- old_send(compressed);
- end;
+ -- setup 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", 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;
else
- session.log("info", method.." compression selected. But we don't support it.");
+ session.log("warn", method.." compression selected. But we don't support it.");
local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method");
- session.send(error_st);
+ (session.sends2s or session.send)(error_st);
end
end
);
+
diff --git a/plugins/mod_console.lua b/plugins/mod_console.lua
index 5a092298..6d387b0e 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,7 +53,7 @@ end
local sessions = {};
-function console_listener.listener(conn, data)
+function console_listener.onincoming(conn, data)
local session = sessions[conn];
if not session then
@@ -126,7 +126,7 @@ function console_listener.listener(conn, 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();
@@ -148,7 +148,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
@@ -158,7 +158,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
@@ -478,7 +478,7 @@ function def_env.s2s:show(match_jid)
for remotehost, session in pairs(host_session.s2sout) do
if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then
count_out = count_out + 1;
- print(" "..host.." -> "..remotehost..(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
@@ -515,7 +515,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
@@ -650,3 +650,5 @@ if option and option ~= "short" and option ~= "full" and option ~= "graphic" the
end
end
end
+
+prosody.net_activate_ports("console", "console", {5582}, "tcp");
diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua
index 06b29f0e..f7e51b83 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_httpserver.lua b/plugins/mod_httpserver.lua
index 545d4faf..07c7f315 100644
--- a/plugins/mod_httpserver.lua
+++ b/plugins/mod_httpserver.lua
@@ -15,8 +15,20 @@ local t_concat = table.concat;
local http_base = config.get("*", "core", "http_path") or "www_files";
local response_400 = { status = "400 Bad Request", body = "<h1>Bad Request</h1>Sorry, we didn't understand your request :(" };
+local response_403 = { status = "403 Forbidden", body = "<h1>Forbidden</h1>You don't have permission to view the contents of this directory :(" };
local response_404 = { status = "404 Not Found", body = "<h1>Page Not Found</h1>Sorry, we couldn't find what you were looking for :(" };
+-- TODO: Should we read this from /etc/mime.types if it exists? (startup time...?)
+local mime_map = {
+ html = "text/html";
+ htm = "text/html";
+ xml = "text/xml";
+ xsl = "text/xml";
+ txt = "text/plain; charset=utf-8";
+ js = "text/javascript";
+ css = "text/css";
+};
+
local function preprocess_path(path)
if path:sub(1,1) ~= "/" then
path = "/"..path;
@@ -36,11 +48,19 @@ local function preprocess_path(path)
end
function serve_file(path)
- local f, err = open(http_base..path, "r");
+ local f, err = open(http_base..path, "rb");
if not f then return response_404; end
local data = f:read("*a");
f:close();
- return data;
+ if not data then
+ return response_403;
+ end
+ local ext = path:match("%.([^.]*)$");
+ local mime = mime_map[ext]; -- Content-Type should be nil when not known
+ return {
+ headers = { ["Content-Type"] = mime; };
+ body = data;
+ };
end
local function handle_file_request(method, body, request)
@@ -56,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_pep.lua b/plugins/mod_pep.lua
index bfe22867..c42876b8 100644
--- a/plugins/mod_pep.lua
+++ b/plugins/mod_pep.lua
@@ -37,9 +37,16 @@ end
module:add_identity("pubsub", "pep", "Prosody");
module:add_feature("http://jabber.org/protocol/pubsub#publish");
-local function publish(session, node, item)
+local function subscription_presence(user_bare, recipient)
+ local recipient_bare = jid_bare(recipient);
+ if (recipient_bare == user_bare) then return true end
+ local item = load_roster(jid_split(user_bare))[recipient_bare];
+ return item and (item.subscription == 'from' or item.subscription == 'both');
+end
+
+local function publish(session, node, id, item)
item.attr.xmlns = nil;
- local disable = #item.tags ~= 1 or #item.tags[1].tags == 0;
+ local disable = #item.tags ~= 1 or #item.tags[1] == 0;
if #item.tags == 0 then item.name = "retract"; end
local bare = session.username..'@'..session.host;
local stanza = st.message({from=bare, type='headline'})
@@ -58,9 +65,9 @@ local function publish(session, node, item)
end
else
if not user_data then user_data = {}; data[bare] = user_data; end
- user_data[node] = stanza;
+ user_data[node] = {id or "1", item};
end
-
+
-- broadcast
for recipient, notify in pairs(recipients[bare] or NULL) do
if notify[node] then
@@ -74,10 +81,14 @@ local function publish_all(user, recipient, session)
local notify = recipients[user] and recipients[user][recipient];
if d and notify then
for node in pairs(notify) do
- local message = d[node];
- if message then
- message.attr.to = recipient;
- session.send(message);
+ if d[node] then
+ local id, item = unpack(d[node]);
+ session.send(st.message({from=user, to=recipient, type='headline'})
+ :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'})
+ :tag('items', {node=node})
+ :add_child(item)
+ :up()
+ :up());
end
end
end
@@ -106,11 +117,9 @@ end
module:hook("presence/bare", function(event)
-- inbound presence to bare JID recieved
local origin, stanza = event.origin, event.stanza;
-
local user = stanza.attr.to or (origin.username..'@'..origin.host);
- local bare = jid_bare(stanza.attr.from);
- local item = load_roster(jid_split(user))[bare];
- if not stanza.attr.to or (item and (item.subscription == 'from' or item.subscription == 'both')) then
+
+ if not stanza.attr.to or subscription_presence(user, stanza.attr.from) then
local recipient = stanza.attr.from;
local current = recipients[user] and recipients[user][recipient];
local hash = get_caps_hash_from_presence(stanza, current);
@@ -135,19 +144,63 @@ end, 10);
module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event)
local session, stanza = event.origin, event.stanza;
+ local payload = stanza.tags[1];
+
if stanza.attr.type == 'set' and (not stanza.attr.to or jid_bare(stanza.attr.from) == stanza.attr.to) then
- local payload = stanza.tags[1];
- if payload.name == 'pubsub' then -- <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ payload = payload.tags[1];
+ if payload and (payload.name == 'publish' or payload.name == 'retract') and payload.attr.node then -- <publish node='http://jabber.org/protocol/tune'>
+ local node = payload.attr.node;
payload = payload.tags[1];
- if payload and (payload.name == 'publish' or payload.name == 'retract') and payload.attr.node then -- <publish node='http://jabber.org/protocol/tune'>
- local node = payload.attr.node;
- payload = payload.tags[1];
- if payload and payload.name == "item" then -- <item>
- session.send(st.reply(stanza));
- publish(session, node, st.clone(payload));
+ if payload and payload.name == "item" then -- <item>
+ local id = payload.attr.id;
+ session.send(st.reply(stanza));
+ publish(session, node, id, st.clone(payload));
+ return true;
+ end
+ end
+ elseif stanza.attr.type == 'get' then
+ local user = stanza.attr.to and jid_bare(stanza.attr.to) or session.username..'@'..session.host;
+ if subscription_presence(user, stanza.attr.from) then
+ local user_data = data[user];
+ local node, requested_id;
+ payload = payload.tags[1];
+ if payload and payload.name == 'items' then
+ node = payload.attr.node;
+ local item = payload.tags[1];
+ if item and item.name == "item" then
+ requested_id = item.attr.id;
+ end
+ end
+ if node and user_data and user_data[node] then -- Send the last item
+ local id, item = unpack(user_data[node]);
+ if not requested_id or id == requested_id then
+ local stanza = st.reply(stanza)
+ :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'})
+ :tag('items', {node=node})
+ :add_child(item)
+ :up()
+ :up();
+ session.send(stanza);
+ return true;
+ else -- requested item doesn't exist
+ local stanza = st.reply(stanza)
+ :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'})
+ :tag('items', {node=node})
+ :up();
+ session.send(stanza);
return true;
end
+ elseif node then -- node doesn't exist
+ session.send(st.error_reply(stanza, 'cancel', 'item-not-found'));
+ return true;
+ else --invalid request
+ session.send(st.error_reply(stanza, 'modify', 'bad-request'));
+ return true;
end
+ else --no presence subscription
+ session.send(st.error_reply(stanza, 'auth', 'not-authorized')
+ :tag('presence-subscription-required', {xmlns='http://jabber.org/protocol/pubsub#errors'}));
+ return true;
end
end
end);
@@ -224,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 b75b9610..55d52ccd 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
@@ -19,10 +19,16 @@ end
local logger_set = require "util.logger".setwriter;
+local lfs = require "lfs";
+local stat = lfs.attributes;
+
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");
@@ -59,28 +65,38 @@ module:add_event_hook("server-starting", function ()
end
end);
-local pidfile_written;
+local pidfile;
+local pidfile_handle;
local function remove_pidfile()
- if pidfile_written then
- os.remove(pidfile_written);
- pidfile_written = nil;
+ if pidfile_handle then
+ pidfile_handle:close();
+ os.remove(pidfile);
+ pidfile, pidfile_handle = nil, nil;
end
end
local function write_pidfile()
- if pidfile_written then
+ if pidfile_handle then
remove_pidfile();
end
- local pidfile = module:get_option("pidfile");
+ pidfile = module:get_option("pidfile");
if pidfile then
- local pf, err = io.open(pidfile, "w+");
- if not pf then
- module:log("error", "Couldn't write pidfile; %s", err);
+ local mode = stat(pidfile) and "r+" or "w+";
+ pidfile_handle, err = io.open(pidfile, mode);
+ if not pidfile_handle then
+ module:log("error", "Couldn't write pidfile at %s; %s", pidfile, err);
+ prosody.shutdown("Couldn't write pidfile");
else
- pf:write(tostring(pposix.getpid()));
- pf:close();
- pidfile_written = pidfile;
+ if not lfs.lock(pidfile_handle, "w") then -- Exclusive lock
+ local other_pid = pidfile_handle:read("*a");
+ module:log("error", "Another Prosody instance seems to be running with PID %s, quitting", other_pid);
+ pidfile_handle = nil;
+ prosody.shutdown("Prosody already running");
+ else
+ pidfile_handle:write(tostring(pposix.getpid()));
+ pidfile_handle:flush();
+ end
end
end
end
@@ -146,4 +162,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 f83e017b..c28dd338 100644
--- a/plugins/mod_presence.lua
+++ b/plugins/mod_presence.lua
@@ -76,6 +76,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza)
end
end
if stanza.attr.type == nil and not origin.presence then -- initial presence
+ origin.presence = stanza; -- FIXME repeated later
local probe = st.presence({from = origin.full_jid, type = "probe"});
for jid, item in pairs(roster) do -- probe all contacts we are subscribed to
if item.subscription == "both" or item.subscription == "to" then
@@ -200,9 +201,6 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_
rostermanager.roster_push(node, host, to_bare);
end
core_route_stanza(origin, stanza);
- -- COMPAT: Some legacy clients keep displaying unsubscribed contacts as online unless an unavailable presence is sent:
- send_presence_of_available_resources(node, host, to_bare, origin, core_route_stanza,
- st.presence({ type="unavailable", from=from_bare, to=to_bare, id=stanza.attr.id }));
end
stanza.attr.from, stanza.attr.to = st_from, st_to;
end
@@ -220,19 +218,20 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b
if stanza.attr.type == "probe" then
if rostermanager.is_contact_subscribed(node, host, from_bare) then
if 0 == send_presence_of_available_resources(node, host, st_from, origin, core_route_stanza) then
- -- TODO send last recieved unavailable presence (or we MAY do nothing, which is fine too)
+ core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- TODO send last activity
end
else
- core_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="unsubscribed"}));
+ core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}));
end
elseif stanza.attr.type == "subscribe" then
if rostermanager.is_contact_subscribed(node, host, from_bare) then
- core_route_stanza(origin, st.presence({from=to_bare, to=from_bare, type="subscribed"})); -- already subscribed
+ core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"})); -- already subscribed
-- Sending presence is not clearly stated in the RFC, but it seems appropriate
if 0 == send_presence_of_available_resources(node, host, from_bare, origin, core_route_stanza) then
- -- TODO send last recieved unavailable presence (or we MAY do nothing, which is fine too)
+ core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- TODO send last activity
end
else
+ core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- acknowledging receipt
if not rostermanager.is_contact_pending_in(node, host, from_bare) then
if rostermanager.set_contact_pending_in(node, host, from_bare) then
sessionmanager.send_to_available_resources(node, host, stanza);
@@ -241,14 +240,17 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b
end
elseif stanza.attr.type == "unsubscribe" then
if rostermanager.process_inbound_unsubscribe(node, host, from_bare) then
+ sessionmanager.send_to_interested_resources(node, host, stanza);
rostermanager.roster_push(node, host, from_bare);
end
elseif stanza.attr.type == "subscribed" then
if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then
+ sessionmanager.send_to_interested_resources(node, host, stanza);
rostermanager.roster_push(node, host, from_bare);
end
elseif stanza.attr.type == "unsubscribed" then
if rostermanager.process_inbound_subscription_cancellation(node, host, from_bare) then
+ sessionmanager.send_to_interested_resources(node, host, stanza);
rostermanager.roster_push(node, host, from_bare);
end
end -- discard any other type
@@ -325,6 +327,20 @@ module:hook("presence/full", function(data)
end -- resource not online, discard
return true;
end);
+module:hook("presence/host", function(data)
+ -- inbound presence to the host
+ local origin, stanza = data.origin, data.stanza;
+
+ local from_bare = jid_bare(stanza.attr.from);
+ local t = stanza.attr.type;
+ if t == "probe" then
+ core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
+ elseif t == "subscribe" then
+ core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" }));
+ core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
+ end
+ return true;
+end);
module:hook("resource-unbind", function(event)
local session, err = event.session, event.error;
diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua
index 8c319bde..ab1eb870 100644
--- a/plugins/mod_privacy.lua
+++ b/plugins/mod_privacy.lua
@@ -1,31 +1,540 @@
-- Prosody IM
-- Copyright (C) 2008-2009 Matthew Wild
-- Copyright (C) 2008-2009 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.
--
-
+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 = util_Jid.split;
+local load_roster = require "core.rostermanager".load_roster;
+local to_number = tonumber;
+
+function findNamedList(privacy_lists, name)
+ if privacy_lists.lists then
+ for i=1,#privacy_lists.lists do
+ if privacy_lists.lists[i].name == name then
+ return i;
+ end
+ end
+ end
+end
+
+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 sendNeededUnavailablePersences(origin, listnameOrItem) -- TODO implement it correctly!
+ if type(listnameOrItem) == "string" then
+ local listname = listnameOrItem;
+ for _,list in ipairs(privacy_lists.lists) do
+ if list.name == listname then
+ for _,item in ipairs(list.items) do
+ sendNeededUnavailablePersences(origin, item);
+ end
+ end
+ end
+ elseif type(listnameOrItem) == "table" then
+ module:log("debug", "got an item, check whether to send unavailable presence stanza or not");
+ local item = listnameOrItem;
+
+ if item["presence-out"] == true then
+ if item.type == "jid" then
+ sendUnavailable(origin, item.value, origin.full_jid);
+ elseif item.type == "group" then
+ elseif item.type == "subscription" then
+ elseif item.type == nil then
+ end
+ elseif item["presence-in"] == true then
+ if item.type == "jid" then
+ sendUnavailable(origin, origin.full_jid, item.value);
+ elseif item.type == "group" then
+ elseif item.type == "subscription" then
+ elseif item.type == nil then
+ end
+ end
+ else
+ module:log("debug", "got unknown type: %s", type(listnameOrItem));
+ 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 idx = findNamedList(privacy_lists, name);
+
+ if privacy_lists.default == nil then
+ privacy_lists.default = "";
+ end
+ if origin.activePrivacyList == nil then
+ origin.activePrivacyList = "";
+ end
+
+ if which == "default" and idx ~= nil 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));
+--[[
+ if origin.activePrivacyList == nil then
+ sendNeededUnavailablePersences(origin, name);
+ end
+]]--
+ elseif which == "active" and idx ~= nil then
+ origin.activePrivacyList = name;
+ origin.send(st.reply(stanza));
+ -- sendNeededUnavailablePersences(origin, name);
+ else
+ return {"modify", "bad-request", "Either not active or default given or unknown list name specified."};
+ end
+ return true;
+end
+
+function deleteList(privacy_lists, origin, stanza, name)
+ local idx = findNamedList(privacy_lists, name);
+
+ if idx ~= nil 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 = "";
+ end
+ if origin.activePrivacyList == name then
+ origin.activePrivacyList = "";
+ end
+ table.remove(privacy_lists.lists, idx);
+ 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 idx = findNamedList(privacy_lists, name);
+ local bare_jid = origin.username.."@"..origin.host;
+
+ if privacy_lists.lists == nil then
+ privacy_lists.lists = {};
+ end
+
+ if idx == nil then
+ idx = #privacy_lists.lists + 1;
+ end
+
+ local orderCheck = {};
+ local list = {};
+ 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 == "group" then
+ local found = false;
+ local roster = load_roster(origin.username, origin.host);
+ for jid,item in pairs(roster) do
+ if item.groups ~= nil then
+ for group in pairs(item.groups) do
+ if group == tmp.value then
+ found = true;
+ break;
+ end
+ end
+ if found == true then
+ break;
+ end
+ end
+ end
+ if found == false then
+ return {"cancel", "item-not-found", "Specifed roster group not existing."};
+ end
+ elseif 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
+
+--[[
+ if (privacy_lists.default == name and origin.activePrivacyList == nil) or origin.activePrivacyList == name then
+ module:log("debug", "calling sendNeededUnavailablePresences!");
+ -- item is valid and list is active, so send needed unavailable stanzas
+ sendNeededUnavailablePersences(origin, tmp);
+ end
+]]--
+ list.items[#list.items + 1] = tmp;
+ end
+
+ table.sort(list, function(a, b) return a.order < b.order; end);
+
+ privacy_lists.lists[idx] = list;
+ 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
+ reply:tag("active", {name=origin.activePrivacyList or ""}):up();
+ reply:tag("default", {name=privacy_lists.default or ""}):up();
+ if privacy_lists.lists then
+ for _,list in ipairs(privacy_lists.lists) do
+ reply:tag("list", {name=list.name}):up();
+ end
+ end
+ else
+ local idx = findNamedList(privacy_lists, name);
+ if idx ~= nil then
+ local list = privacy_lists.lists[idx];
+ 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 valid = false;
local privacy_lists = datamanager.load(origin.username, origin.host, "privacy") or {};
+
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
+ if valid[0] == nil then
+ valid[0] = "cancel";
+ end
+ if valid[1] == nil then
+ valid[1] = "bad-request";
+ end
+ origin.send(st.error_reply(stanza, valid[0], valid[1], valid[2]));
+ 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;
+ local from = stanza.attr.from;
+
+ local to_user = bare_jid == jid_bare(to);
+ local 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
+ (session.activePrivacyList == nil or session.activePrivacyList == "") and
+ (privacy_lists.default == nil or privacy_lists.default == "")
+ then
+ return; -- Nothing to block, default is Allow all
+ end
+ if from_user and 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 idx;
+ local list;
+ local item;
+ local listname = session.activePrivacyList;
+ if listname == nil or listname == "" then
+ listname = privacy_lists.default; -- no active list selected, use default list
+ end
+ idx = findNamedList(privacy_lists, listname);
+ if idx == nil then
+ module:log("debug", "given privacy listname not found. name: %s", listname);
+ return;
+ end
+ list = privacy_lists.lists[idx];
+ if list == nil then
+ module:log("debug", "privacy list index wrong. index: %d", idx);
+ 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 to_user and item["presence-in"]) or
+ (stanza.name == "presence" and 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 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 groups = roster[evilJid.node .. "@" .. evilJid.host].groups;
+ for group in pairs(groups) do
+ if group == item.value then
+ apply = true;
+ block = (item.action == "deny");
+ break;
+ end
+ end
+ elseif item.type == "subscription" and evilJid.node ~= nil and evilJid.host ~= nil then -- we need a valid bare evil jid
+ local roster = load_roster(session.username, session.host);
+ if roster[evilJid.node .. "@" .. evilJid.host].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_proxy65.lua b/plugins/mod_proxy65.lua
new file mode 100644
index 00000000..2cfbe7b6
--- /dev/null
+++ b/plugins/mod_proxy65.lua
@@ -0,0 +1,286 @@
+-- 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 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 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 ... ");
+ throttle_sending(conn, transfers[sha].target);
+ throttle_sending(transfers[sha].target, conn);
+ 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;
+ end
+ elseif stanza.name == "iq" and type == "set" then
+ 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);
+ 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);
+local sender_lock_threshold = 4096;
+function throttle_sending(sender, receiver)
+ sender:pattern(sender_lock_threshold);
+ local sender_locked;
+ local _sendbuffer = receiver.sendbuffer;
+ function receiver.sendbuffer()
+ _sendbuffer();
+ if sender_locked and receiver.bufferlen() < sender_lock_threshold then
+ sender:lock_read(false); -- Unlock now
+ sender_locked = nil;
+ end
+ end
+
+ local _readbuffer = sender.readbuffer;
+ function sender.readbuffer()
+ _readbuffer();
+ if not sender_locked and receiver.bufferlen() >= sender_lock_threshold then
+ sender_locked = true;
+ sender:lock_read(true);
+ end
+ end
+end
diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua
index 22724130..be1be0ae 100644
--- a/plugins/mod_register.lua
+++ b/plugins/mod_register.lua
@@ -43,21 +43,21 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza)
session:close({condition = "not-authorized", text = "Account deleted"});
end
-- TODO datamanager should be able to delete all user data itself
- datamanager.store(username, host, "roster", nil);
datamanager.store(username, host, "vcard", nil);
datamanager.store(username, host, "private", nil);
datamanager.store(username, host, "offline", nil);
- --local bare = username.."@"..host;
+ local bare = username.."@"..host;
for jid, item in pairs(roster) do
- if jid ~= "pending" then
- if item.subscription == "both" or item.subscription == "to" then
- -- TODO unsubscribe
+ if jid and jid ~= "pending" then
+ if item.subscription == "both" or item.subscription == "from" or (roster.pending and roster.pending[jid]) then
+ core_post_stanza(hosts[host], st.presence({type="unsubscribed", from=bare, to=jid}));
end
- if item.subscription == "both" or item.subscription == "from" then
- -- TODO unsubscribe
+ if item.subscription == "both" or item.subscription == "to" or item.ask then
+ core_post_stanza(hosts[host], st.presence({type="unsubscribe", from=bare, to=jid}));
end
end
end
+ datamanager.store(username, host, "roster", 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 });
@@ -117,7 +117,9 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s
local password = query:child_with_name("password");
if username and password then
-- Check that the user is not blacklisted or registering too often
- if blacklisted_ips[session.ip] or (whitelist_only and not whitelisted_ips[session.ip]) then
+ if not session.ip then
+ module:log("debug", "User's IP not known; can't apply blacklist/whitelist");
+ elseif blacklisted_ips[session.ip] or (whitelist_only and not whitelisted_ips[session.ip]) then
session.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not allowed to register an account."));
return;
elseif min_seconds_between_registrations and not whitelisted_ips[session.ip] then
@@ -139,7 +141,7 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:register", function (session, s
username = nodeprep(table.concat(username));
password = table.concat(password);
local host = module.host;
- if not username then
+ if not username or username == "" then
session.send(st.error_reply(stanza, "modify", "not-acceptable", "The requested username is invalid."));
elseif usermanager_user_exists(username, host) then
session.send(st.error_reply(stanza, "cancel", "conflict", "The requested username already exists."));
diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua
index 7ca22aa1..52c61a26 100644
--- a/plugins/mod_roster.lua
+++ b/plugins/mod_roster.lua
@@ -74,18 +74,20 @@ module:add_iq_handler("c2s", "jabber:iq:roster",
if not resource and host then
if jid ~= from_node.."@"..from_host then
if item.attr.subscription == "remove" then
- local r_item = session.roster[jid];
+ local roster = session.roster;
+ local r_item = roster[jid];
if r_item then
+ local to_bare = node and (node.."@"..host) or host; -- bare JID
+ if r_item.subscription == "both" or r_item.subscription == "from" or (roster.pending and roster.pending[jid]) then
+ core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=to_bare}));
+ end
+ if r_item.subscription == "both" or r_item.subscription == "to" or r_item.ask then
+ core_post_stanza(session, st.presence({type="unsubscribe", from=session.full_jid, to=to_bare}));
+ end
local success, err_type, err_cond, err_msg = rm_remove_from_roster(session, jid);
if success then
session.send(st.reply(stanza));
rm_roster_push(from_node, from_host, jid);
- local to_bare = node and (node.."@"..host) or host; -- bare JID
- if r_item.subscription == "both" or r_item.subscription == "from" then
- core_post_stanza(session, st.presence({type="unsubscribed", from=session.full_jid, to=to_bare}));
- elseif r_item.subscription == "both" or r_item.subscription == "to" then
- core_post_stanza(session, st.presence({type="unsubscribe", from=session.full_jid, to=to_bare}));
- end
else
session.send(st.error_reply(stanza, err_type, err_cond, err_msg));
end
diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua
index 04e33b29..75ee9f04 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -21,11 +21,12 @@ 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";
local log = module._log;
@@ -33,28 +34,37 @@ 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 == "cyrus" then
+ local cyrus_new = require "util.sasl_cyrus".new;
+ new_sasl = function(realm)
+ return cyrus_new(realm, module:get_option("cyrus_service_name") or "xmpp");
+ end
+else
+ if sasl_backend ~= "builtin" then module:log("warn", "Unknown SASL backend %s", sasl_backend); end;
+ new_sasl = require "util.sasl".new;
+end
-default_authentication_profile = {
+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;
+ local prepped_username = nodeprep(username);
+ if not prepped_username then
+ log("debug", "NODEprep failed on username: %s", username);
+ return "", nil;
+ end
+ local password = usermanager_get_password(prepped_username, realm);
+ if not password then
+ return "", nil;
end
+ return password, true;
+ end
};
-anonymous_authentication_profile = {
+local anonymous_authentication_profile = {
anonymous = function(username, realm)
- return true; -- for normal usage you should always return true here
- end
-}
+ 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});
@@ -75,7 +85,7 @@ end
local function handle_status(session, status)
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);
if not username then -- TODO move this to sessionmanager
@@ -104,13 +114,16 @@ local function sasl_handler(session, stanza)
if not valid_mechanism then
return session.send(build_reply("failure", "invalid-mechanism"));
end
+ if secure_auth_only and not session.secure then
+ return session.send(build_reply("failure", "encryption-required"));
+ end
elseif not session.sasl_handler then
return; -- FIXME ignoring out of order stanzas because ejabberd does
end
local text = stanza[1];
if text then
text = base64.decode(text);
- log("debug", "%s", text);
+ log("debug", "%s", text:gsub("[%z\001-\008\011\012\014-\031]", " "));
if not text then
session.sasl_handler = nil;
session.send(build_reply("failure", "incorrect-encoding"));
@@ -131,56 +144,53 @@ 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
- if module:get_option("anonymous_login") then
- session.sasl_handler = new_sasl(session.host, anonymous_authentication_profile);
- else
- session.sasl_handler = new_sasl(session.host, default_authentication_profile);
- if not (module:get_option("allow_unencrypted_plain_auth")) and not session.secure then
- session.sasl_handler:forbidden({"PLAIN"});
- end
- end
- features:tag("mechanisms", mechanisms_attr);
- for k, v in pairs(session.sasl_handler:mechanisms()) do
- features:tag("mechanism"):text(v):up();
- end
- features:up();
- else
- features:tag("bind", bind_attr):tag("required"):up():up();
- features:tag("session", xmpp_session_attr):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:add_event_hook("stream-features", function(session, features)
+ if not session.username then
+ if secure_auth_only and not session.secure then
+ return;
+ end
+ if module:get_option("anonymous_login") then
+ session.sasl_handler = new_sasl(session.host, anonymous_authentication_profile);
+ else
+ session.sasl_handler = new_sasl(session.host, default_authentication_profile);
+ if not (module:get_option("allow_unencrypted_plain_auth")) and not session.secure then
+ session.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(session.sasl_handler:mechanisms()) do
+ features:tag("mechanism"):text(v):up();
+ end
+ features:up();
+ else
+ features:tag("bind", bind_attr):tag("required"):up():up();
+ features:tag("session", xmpp_session_attr):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 8a450803..73b5ae09 100644
--- a/plugins/mod_tls.lua
+++ b/plugins/mod_tls.lua
@@ -14,15 +14,15 @@ 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 global_ssl_ctx = prosody.global_ssl_ctx;
+
module:add_handler("c2s_unauthed", "starttls", xmlns_starttls,
function (session, stanza)
if session.conn.starttls then
session.send(st.stanza("proceed", { xmlns = xmlns_starttls }));
session:reset_stream();
- if session.host and hosts[session.host].ssl_ctx_in then
- session.conn.set_sslctx(hosts[session.host].ssl_ctx_in);
- end
- session.conn.starttls();
+ local ssl_ctx = session.host and hosts[session.host].ssl_ctx_in or global_ssl_ctx;
+ session.conn:starttls(ssl_ctx);
session.log("info", "TLS negotiation started...");
session.secure = false;
else
@@ -36,10 +36,8 @@ module:add_handler("s2sin_unauthed", "starttls", xmlns_starttls,
if session.conn.starttls then
session.sends2s(st.stanza("proceed", { xmlns = xmlns_starttls }));
session:reset_stream();
- if session.to_host and hosts[session.to_host].ssl_ctx_in then
- session.conn.set_sslctx(hosts[session.to_host].ssl_ctx_in);
- end
- session.conn.starttls();
+ local ssl_ctx = session.to_host and hosts[session.to_host].ssl_ctx_in or global_ssl_ctx;
+ session.conn:starttls(ssl_ctx);
session.log("info", "TLS negotiation started for incoming s2s...");
session.secure = false;
else
@@ -89,9 +87,9 @@ module:hook_stanza(xmlns_stream, "features",
module:hook_stanza(xmlns_starttls, "proceed",
function (session, stanza)
module:log("debug", "Proceeding with TLS on s2sout...");
- local format, to_host, from_host = string.format, session.to_host, session.from_host;
session:reset_stream();
- session.conn.starttls(true);
+ 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_vcard.lua b/plugins/mod_vcard.lua
index 0efc1638..6bf82ee7 100644
--- a/plugins/mod_vcard.lua
+++ b/plugins/mod_vcard.lua
@@ -51,7 +51,7 @@ if module:get_option("vcard_compatibility") then
module:hook("iq/full", function(data)
local stanza = data.stanza;
local payload = stanza.tags[1];
- if stanza.attr.type == "get" or stanza.attr.type == "set" and payload.name == "vCard" and payload.attr.xmlns == "vcard-temp" then
+ if stanza.attr.type == "get" and payload.name == "vCard" and payload.attr.xmlns == "vcard-temp" then
return handle_vcard(data);
end
end, 1);
diff --git a/plugins/mod_xmlrpc.lua b/plugins/mod_xmlrpc.lua
deleted file mode 100644
index 7165386a..00000000
--- a/plugins/mod_xmlrpc.lua
+++ /dev/null
@@ -1,128 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2009 Matthew Wild
--- Copyright (C) 2008-2009 Waqas Hussain
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-
-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 856f3cba..d23e2474 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 3a185e17..ad45bbfd 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;
@@ -128,19 +110,21 @@ function room_mt:broadcast_presence(stanza, sid, code, nick)
end
end
function room_mt:broadcast_message(stanza, historic)
+ local to = stanza.attr.to;
for occupant, o_data in pairs(self._occupants) do
for jid in pairs(o_data.sessions) do
stanza.attr.to = jid;
self:_route_stanza(stanza);
end
end
+ stanza.attr.to = to;
if historic then -- add to history
local history = self._data['history'];
if not history then history = {}; self._data['history'] = history; end
- -- stanza = st.clone(stanza);
+ stanza = st.clone(stanza);
stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203
stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
- t_insert(history, st.clone(st.preserialize(stanza)));
+ t_insert(history, st.preserialize(stanza));
while #history > history_length do t_remove(history, 1) end
end
end
@@ -181,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();
@@ -204,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);
@@ -217,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
@@ -365,8 +358,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
@@ -387,61 +379,122 @@ 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
+ 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
+ dirty = dirty or (self._data.hidden ~= (not public and true or nil))
+ self._data.hidden = not public and true or nil;
+
+ 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));
- 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));
+ 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()
- local public = fields['muc#roomconfig_publicroom'];
- if public == "0" or public == "false" then public = nil; elseif public == "1" or public == "true" then public = true;
- else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end
- self._data.hidden = not public and true or nil;
+ if 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
- if self.save then self:save(true); end
- origin.send(st.reply(stanza));
+ 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
local type = stanza.attr.type;
local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns;
if stanza.name == "iq" then
if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" then
- origin.send(room_get_disco_info(self, stanza));
+ 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);
@@ -492,9 +545,14 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
-- TODO allow admins and owners not in room? Provide read-only access to everyone who can see the participants anyway?
if _rol == "none" then _rol = nil; end
local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
- for nick, occupant in pairs(self._occupants) do
+ for occupant_jid, occupant in pairs(self._occupants) do
if occupant.role == _rol then
- reply:tag("item", {nick = nick, role = _rol or "none", affiliation = occupant.affiliation or "none", jid = occupant.jid}):up();
+ reply:tag("item", {
+ nick = select(3, jid_split(occupant_jid)),
+ role = _rol or "none",
+ affiliation = occupant.affiliation or "none",
+ jid = occupant.jid
+ }):up();
end
end
origin.send(reply);
@@ -509,7 +567,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
@@ -517,23 +598,31 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha
local from, to = stanza.attr.from, stanza.attr.to;
local room = jid_bare(to);
local current_nick = self._jid_nick[from];
- if not current_nick then -- not in room
+ local occupant = self._occupants[current_nick];
+ if not occupant then -- not in room
origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
+ elseif occupant.role == "visitor" then
+ origin.send(st.error_reply(stanza, "cancel", "forbidden"));
else
local from = stanza.attr.from;
stanza.attr.from = current_nick;
local subject = getText(stanza, {"subject"});
if subject then
- self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza
+ if occupant.role == "moderator" then
+ self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza
+ else
+ stanza.attr.from = from;
+ origin.send(st.error_reply(stanza, "cancel", "forbidden"));
+ end
else
self:broadcast_message(stanza, true);
end
+ stanza.attr.from = from;
end
elseif stanza.name == "message" and type == "error" and is_kickable_error(stanza) then
local current_nick = self._jid_nick[stanza.attr.from];
log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
- self:handle_to_occupant(origin, st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to})
- :tag('status'):text('Kicked: '..get_error_condition(stanza))); -- send unavailable
+ 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];
@@ -651,21 +740,21 @@ function room_mt:get_role(nick)
local session = self._occupants[nick];
return session and session.role or nil;
end
-function room_mt:set_role(actor, nick, role, callback, reason)
+function room_mt:set_role(actor, occupant_jid, role, callback, reason)
if role == "none" then role = nil; end
if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end
if self:get_affiliation(actor) ~= "owner" then return nil, "cancel", "not-allowed"; end
- local occupant = self._occupants[nick];
+ local occupant = self._occupants[occupant_jid];
if not occupant then return nil, "modify", "not-acceptable"; end
if occupant.affiliation == "owner" or occupant.affiliation == "admin" then return nil, "cancel", "not-allowed"; end
- local p = st.presence({from = nick})
+ local p = st.presence({from = occupant_jid})
:tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
- :tag("item", {affiliation=occupant.affiliation or "none", nick=nick, role=role or "none"})
+ :tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"})
:tag("reason"):text(reason or ""):up()
:up();
if not role then -- kick
p.attr.type = "unavailable";
- self._occupants[nick] = nil;
+ self._occupants[occupant_jid] = nil;
for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick
self._jid_nick[jid] = nil;
end
@@ -678,7 +767,7 @@ function room_mt:set_role(actor, nick, role, callback, reason)
self:_route_stanza(p);
end
if callback then callback(); end
- self:broadcast_except_nick(p, nick);
+ self:broadcast_except_nick(p, occupant_jid);
return true;
end
@@ -688,13 +777,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
@@ -709,6 +796,9 @@ function room_mt:_route_stanza(stanza)
end
end
end
+ if self._data.whois == 'anyone' then
+ muc_child:tag('status', { code = '100' });
+ end
end
self:route_stanza(stanza);
if muc_child then
@@ -727,7 +817,9 @@ function _M.new_room(jid)
jid = jid;
_jid_nick = {};
_occupants = {};
- _data = {};
+ _data = {
+ whois = 'moderators',
+ };
_affiliations = {};
}, room_mt);
end