diff options
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/mod_actions_http.lua | 86 | ||||
-rw-r--r-- | plugins/mod_bosh.lua | 51 | ||||
-rw-r--r-- | plugins/mod_component.lua | 13 | ||||
-rw-r--r-- | plugins/mod_compression.lua | 210 | ||||
-rw-r--r-- | plugins/mod_console.lua | 16 | ||||
-rw-r--r-- | plugins/mod_disco.lua | 53 | ||||
-rw-r--r-- | plugins/mod_httpserver.lua | 13 | ||||
-rw-r--r-- | plugins/mod_iq.lua | 12 | ||||
-rw-r--r-- | plugins/mod_legacyauth.lua | 7 | ||||
-rw-r--r-- | plugins/mod_pep.lua | 18 | ||||
-rw-r--r-- | plugins/mod_posix.lua | 12 | ||||
-rw-r--r-- | plugins/mod_presence.lua | 41 | ||||
-rw-r--r-- | plugins/mod_privacy.lua | 465 | ||||
-rw-r--r-- | plugins/mod_proxy65.lua | 286 | ||||
-rw-r--r-- | plugins/mod_roster.lua | 12 | ||||
-rw-r--r-- | plugins/mod_saslauth.lua | 192 | ||||
-rw-r--r-- | plugins/mod_tls.lua | 151 | ||||
-rw-r--r-- | plugins/mod_xmlrpc.lua | 128 | ||||
-rw-r--r-- | plugins/muc/mod_muc.lua | 1 | ||||
-rw-r--r-- | plugins/muc/muc.lib.lua | 234 |
20 files changed, 1407 insertions, 594 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 5de79eff..2cb3100e 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 @@ -186,14 +206,15 @@ function stream_callbacks.streamopened(request, attr) -- Send creation response local features = st.stanza("stream:features"); + hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); fire_event("stream-features", session, features); --xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh' - local response = st.stanza("body", { xmlns = xmlns_bosh, - inactivity = tostring(BOSH_DEFAULT_INACTIVITY), polling = tostring(BOSH_DEFAULT_POLLING), requests = tostring(BOSH_DEFAULT_REQUESTS), hold = tostring(session.bosh_hold), maxpause = "120", - sid = sid, authid = sid, ver = '1.6', from = session.host, secure = 'true', ["xmpp:version"] = "1.0", + local response = st.stanza("body", { xmlns = xmlns_bosh, + inactivity = tostring(BOSH_DEFAULT_INACTIVITY), polling = tostring(BOSH_DEFAULT_POLLING), requests = tostring(BOSH_DEFAULT_REQUESTS), hold = tostring(session.bosh_hold), maxpause = "120", + sid = sid, authid = sid, ver = '1.6', from = session.host, secure = 'true', ["xmpp:version"] = "1.0", ["xmlns:xmpp"] = "urn:xmpp:xbosh", ["xmlns:stream"] = "http://etherx.jabber.org/streams" }):add_child(features); request:send{ headers = default_headers, body = tostring(response) }; - + request.sid = sid; return; end @@ -237,6 +258,7 @@ function stream_callbacks.streamopened(request, attr) if session.notopen then local features = st.stanza("stream:features"); + hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); fire_event("stream-features", session, features); session.send(features); session.notopen = nil; @@ -254,7 +276,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(); + session.ip = request.handler:ip(); core_process_stanza(session, stanza); end end @@ -298,7 +320,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 4201bb19..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"); @@ -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 87ff6be7..3ef5a1f3 100644 --- a/plugins/mod_compression.lua +++ b/plugins/mod_compression.lua @@ -12,6 +12,7 @@ local tostring = tostring; local xmlns_compression_feature = "http://jabber.org/features/compress" local xmlns_compression_protocol = "http://jabber.org/protocol/compress" +local xmlns_stream = "http://etherx.jabber.org/streams"; local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up(); local compression_level = module:get_option("compression_level"); @@ -26,22 +27,149 @@ if not compression_level or compression_level < 1 or compression_level > 9 then return; end -module:add_event_hook("stream-features", - function (session, features) +module:hook("stream-features", function(event) + local origin, features = event.origin, event.features; + if not origin.compressed then + -- FIXME only advertise compression support when TLS layer has no compression enabled + features:add_child(compression_stream_feature); + end +end); + +module:hook("s2s-stream-features", function(event) + local origin, features = event.origin, event.features; + -- FIXME only advertise compression support when TLS layer has no compression enabled + if not origin.compressed then + features:add_child(compression_stream_feature); + end +end); + +-- Hook to activate compression if remote server supports it. +module:hook_stanza(xmlns_stream, "features", + function (session, stanza) if not session.compressed then - -- FIXME only advertise compression support when TLS layer has no compression enabled - features:add_child(compression_stream_feature); + -- does remote server support compression? + local comp_st = stanza:child_with_name("compression"); + if comp_st then + -- do we support the mechanism + for a in comp_st:children() do + local algorithm = a[1] + if algorithm == "zlib" then + session.sends2s(st.stanza("compress", {xmlns=xmlns_compression_protocol}):tag("method"):text("zlib")) + session.log("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", "%s", tostring(deflate_stream)); + return + end + return deflate_stream +end + +-- returns either nil or a fully functional ready to use inflate stream +local function get_inflate_stream(session) + local status, inflate_stream = pcall(zlib.inflate); + if status == false then + local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); + (session.sends2s or session.send)(error_st); + session.log("error", "Failed to create zlib.inflate filter."); + module:log("error", "%s", tostring(inflate_stream)); + return + end + return inflate_stream +end + +-- setup compression for a stream +local function setup_compression(session, deflate_stream) + local old_send = (session.sends2s or session.send); + + local new_send = function(t) + --TODO: Better code injection in the sending process + 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", "%s", tostring(compressed)); + return; + end + session.conn:write(compressed); + end; + + if session.sends2s then session.sends2s = new_send + elseif session.send then session.send = new_send end +end + +-- setup decompression for a stream +local function setup_decompression(session, inflate_stream) + local old_data = session.data + session.data = function(conn, data) + local status, decompressed, eof = pcall(inflate_stream, data); + if status == false then + session:close({ + condition = "undefined-condition"; + text = decompressed; + extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); + }); + module:log("warn", "%s", tostring(decompressed)); + return; end + old_data(conn, decompressed); + end; +end + +module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compression_protocol, + function(session ,stanza) + session.log("debug", "Activating compression...") + -- create deflate and inflate streams + local deflate_stream = get_deflate_stream(session); + if not deflate_stream then return end + + local inflate_stream = get_inflate_stream(session); + if not inflate_stream then return end + + -- setup compression for session.w + setup_compression(session, deflate_stream); + + -- setup decompression for session.data + setup_decompression(session, inflate_stream); + local session_reset_stream = session.reset_stream; + session.reset_stream = function(session) + session_reset_stream(session); + setup_decompression(session, inflate_stream); + return true; + end; + session:reset_stream(); + local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams", + ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host}; + session.sends2s("<?xml version='1.0'?>"); + session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag()); + session.compressed = true; end ); --- TODO Support compression on S2S level too. -module:add_handler({"c2s_unauthed", "c2s"}, "compress", xmlns_compression_protocol, +module:add_handler({"c2s_unauthed", "c2s", "s2sin_unauthed", "s2sin"}, "compress", xmlns_compression_protocol, function(session, stanza) -- fail if we are already compressed if session.compressed then local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); - session.send(error_st); + (session.sends2s or session.send)(error_st); session.log("warn", "Tried to establish another compression layer."); return; end @@ -50,78 +178,38 @@ module:add_handler({"c2s_unauthed", "c2s"}, "compress", xmlns_compression_protoc local method = stanza:child_with_name("method"); method = method and (method[1] or ""); if method == "zlib" then + session.log("debug", "%s compression selected.", tostring(method)); + -- create deflate and inflate streams - local status, deflate_stream = pcall(zlib.deflate, compression_level); - if status == false then - local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); - session.send(error_st); - session.log("error", "Failed to create zlib.deflate filter."); - module:log("error", "%s", tostring(deflate_stream)); - return - end + local deflate_stream = get_deflate_stream(session); + if not deflate_stream then return end - local status, inflate_stream = pcall(zlib.inflate); - if status == false then - local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); - session.send(error_st); - session.log("error", "Failed to create zlib.inflate filter."); - module:log("error", "%s", tostring(inflate_stream)); - return - end + local inflate_stream = get_inflate_stream(session); + if not inflate_stream then return end - session.log("info", method.." compression selected."); - session.send(st.stanza("compressed", {xmlns=xmlns_compression_protocol})); + (session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol})); session:reset_stream(); - - -- setup compression for session.w - local old_send = session.send; - session.send = function(t) - local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync'); - if status == false then - session:close({ - condition = "undefined-condition"; - text = compressed; - extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); - }); - module:log("warn", "%s", tostring(compressed)); - return; - end - old_send(compressed); - end; + -- setup compression for session.w + setup_compression(session, deflate_stream); -- setup decompression for session.data - local function setup_decompression(session) - local old_data = session.data - session.data = function(conn, data) - local status, decompressed, eof = pcall(inflate_stream, data); - if status == false then - session:close({ - condition = "undefined-condition"; - text = decompressed; - extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); - }); - module:log("warn", "%s", tostring(decompressed)); - return; - end - old_data(conn, decompressed); - end; - end - setup_decompression(session); + setup_decompression(session, inflate_stream); local session_reset_stream = session.reset_stream; session.reset_stream = function(session) session_reset_stream(session); - setup_decompression(session); + setup_decompression(session, inflate_stream); return true; end; session.compressed = true; elseif method then session.log("info", "%s compression selected, but we don't support it.", tostring(method)); local error_st = st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("unsupported-method"); - session.send(error_st); + (session.sends2s or session.send)(error_st); else - session.send(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed")); + (session.sends2s or session.send)(st.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed")); end end ); + diff --git a/plugins/mod_console.lua b/plugins/mod_console.lua index d8362a07..05ff3274 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 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 c12f1c05..07c7f315 100644 --- a/plugins/mod_httpserver.lua +++ b/plugins/mod_httpserver.lua @@ -76,6 +76,13 @@ local function handle_default_request(method, body, request) return serve_file(path); end -local ports = config.get(module.host, "core", "http_ports") or { 5280 }; -httpserver.set_default_handler(handle_default_request); -httpserver.new_from_config(ports, handle_file_request, { base = "files" }); +local function setup() + local ports = config.get(module.host, "core", "http_ports") or { 5280 }; + httpserver.set_default_handler(handle_default_request); + httpserver.new_from_config(ports, handle_file_request, { base = "files" }); +end +if prosody.start_time then -- already started + setup(); +else + prosody.events.add_handler("server-started", setup); +end diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua index 5be04533..0e1dadfc 100644 --- a/plugins/mod_iq.lua +++ b/plugins/mod_iq.lua @@ -53,6 +53,18 @@ module:hook("iq/bare", function(data) end end); +module:hook("iq/self", function(data) + -- IQ to bare JID recieved + local origin, stanza = data.origin, data.stanza; + + if stanza.attr.type == "get" or stanza.attr.type == "set" then + return module:fire_event("iq/self/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data); + else + module:fire_event("iq/self/"..stanza.attr.id, data); + return true; + end +end); + module:hook("iq/host", function(data) -- IQ to a local host recieved local origin, stanza = data.origin, data.stanza; diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index c678dce1..9837920b 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -19,11 +19,12 @@ local nodeprep = require "util.encodings".stringprep.nodeprep; local resourceprep = require "util.encodings".stringprep.resourceprep; module:add_feature("jabber:iq:auth"); -module:add_event_hook("stream-features", function (session, features) - if secure_auth_only and not session.secure then +module:hook("stream-features", function(event) + local origin, features = event.origin, event.features; + if secure_auth_only and not origin.secure then -- Sorry, not offering to insecure streams! return; - elseif not session.username then + elseif not origin.username then features:tag("auth", {xmlns='http://jabber.org/features/iq-auth'}):up(); end end); diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index ef03ab0f..c42876b8 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -277,3 +277,21 @@ module:hook("iq/bare/disco", function(event) end end end); + +module:hook("account-disco-info", function(event) + local stanza = event.stanza; + stanza:tag('identity', {category='pubsub', type='pep'}):up(); + stanza:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up(); +end); + +module:hook("account-disco-items", function(event) + local session, stanza = event.session, event.stanza; + local bare = session.username..'@'..session.host; + local user_data = data[bare]; + + if user_data then + for node, _ in pairs(user_data) do + stanza:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes + end + end +end); diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua index ed0dbd87..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 @@ -26,6 +26,9 @@ local prosody = _G.prosody; module.host = "*"; -- we're a global module +local umask = module:get_option("umask") or "027"; +pposix.umask(umask); + -- Allow switching away from root, some people like strange ports. module:add_event_hook("server-started", function () local uid = module:get_option("setuid"); @@ -159,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 a39d9c19..648c78b3 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -38,23 +38,42 @@ function core_route_stanza(origin, stanza) _core_route_stanza(origin, stanza); end -local function select_top_resources(user) - local priority = 0; - local recipients = {}; - for _, session in pairs(user.sessions) do -- find resource with greatest priority - if session.presence then - -- TODO check active privacy list for session +local select_top_resources; +local bare_message_delivery_policy = module:get_option("bare_message_delivery_policy") or "priority"; +if bare_message_delivery_policy == "broadcast" then + function select_top_resources(user) + local recipients = {}; + for _, session in pairs(user.sessions) do -- find resources with non-negative priority local p = session.priority; - if p > priority then - priority = p; - recipients = {session}; - elseif p == priority then + if p and p >= 0 then t_insert(recipients, session); end end + return recipients; + end +else + if bare_message_delivery_policy ~= "priority" then + module:log("warn", "Invalid value for config option bare_message_delivery_policy"); + end + function select_top_resources(user) + local priority = 0; + local recipients = {}; + for _, session in pairs(user.sessions) do -- find resource with greatest priority + if session.presence then + -- TODO check active privacy list for session + local p = session.priority; + if p > priority then + priority = p; + recipients = {session}; + elseif p == priority then + t_insert(recipients, session); + end + end + end + return recipients; end - return recipients; end + local function recalc_resource_map(user) if user then user.top_resources = select_top_resources(user); diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua index 8c319bde..d3043d69 100644 --- a/plugins/mod_privacy.lua +++ b/plugins/mod_privacy.lua @@ -1,31 +1,476 @@ -- 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 isListUsed(origin, name, privacy_lists) + local user = bare_sessions[origin.username.."@"..origin.host]; + if user then + for resource, session in pairs(user.sessions) do + if resource ~= origin.resource then + if session.activePrivacyList == name then + return true; + elseif session.activePrivacyList == nil and privacy_lists.default == name then + return true; + end + end + end + end +end + +function isAnotherSessionUsingDefaultList(origin) + local user = bare_sessions[origin.username.."@"..origin.host]; + if user then + for resource, session in pairs(user.sessions) do + if resource ~= origin.resource and session.activePrivacyList == nil then + return true; + end + end + end +end + +function sendUnavailable(origin, to, from) +--[[ example unavailable presence stanza +<presence from="node@host/resource" type="unavailable" to="node@host" > + <status>Logged out</status> +</presence> +]]-- + local presence = st.presence({from=from, type="unavailable"}); + presence:tag("status"):text("Logged out"); + + local node, host = jid_bare(to); + local bare = node .. "@" .. host; + + local user = bare_sessions[bare]; + if user then + for resource, session in pairs(user.sessions) do + presence.attr.to = session.full_jid; + module:log("debug", "send unavailable to: %s; from: %s", tostring(presence.attr.to), tostring(presence.attr.from)); + origin.send(presence); + end + end +end + +function declineList(privacy_lists, origin, stanza, which) + if which == "default" then + if isAnotherSessionUsingDefaultList(origin) then + return { "cancel", "conflict", "Another session is online and using the default list."}; + end + privacy_lists.default = nil; + origin.send(st.reply(stanza)); + elseif which == "active" then + origin.activePrivacyList = nil; + origin.send(st.reply(stanza)); + else + return {"modify", "bad-request", "Neither default nor active list specifed to decline."}; + end + return true; +end + +function activateList(privacy_lists, origin, stanza, which, name) + local list = privacy_lists.lists[name]; + + if which == "default" and list then + if isAnotherSessionUsingDefaultList(origin) then + return {"cancel", "conflict", "Another session is online and using the default list."}; + end + privacy_lists.default = name; + origin.send(st.reply(stanza)); + elseif which == "active" and list then + origin.activePrivacyList = name; + origin.send(st.reply(stanza)); + 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 list = privacy_lists.lists[name]; + + if list then + if isListUsed(origin, name, privacy_lists) then + return {"cancel", "conflict", "Another session is online and using the list which should be deleted."}; + end + if privacy_lists.default == name then + privacy_lists.default = nil; + end + if origin.activePrivacyList == name then + origin.activePrivacyList = nil; + end + privacy_lists.lists[name] = nil; + origin.send(st.reply(stanza)); + return true; + end + return {"modify", "bad-request", "Not existing list specifed to be deleted."}; +end + +function createOrReplaceList (privacy_lists, origin, stanza, name, entries, roster) + local bare_jid = origin.username.."@"..origin.host; + + if privacy_lists.lists == nil then + privacy_lists.lists = {}; + end + + local list = {}; + privacy_lists.lists[name] = list; + + local orderCheck = {}; + list.name = name; + list.items = {}; + + for _,item in ipairs(entries) do + if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then + return {"modify", "bad-request", "Order attribute not valid."}; + end + + if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then + return {"modify", "bad-request", "Type attribute not valid."}; + end + + local tmp = {}; + orderCheck[item.attr.order] = true; + + tmp["type"] = item.attr.type; + tmp["value"] = item.attr.value; + tmp["action"] = item.attr.action; + tmp["order"] = to_number(item.attr.order); + tmp["presence-in"] = false; + tmp["presence-out"] = false; + tmp["message"] = false; + tmp["iq"] = false; + + if #item.tags > 0 then + for _,tag in ipairs(item.tags) do + tmp[tag.name] = true; + end + end + + if tmp.type == "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 + list.items[#list.items + 1] = tmp; + end + + table.sort(list, function(a, b) return a.order < b.order; end); + + origin.send(st.reply(stanza)); + if bare_sessions[bare_jid] ~= nil then + local iq = st.iq ( { type = "set", id="push1" } ); + iq:tag ("query", { xmlns = "jabber:iq:privacy" } ); + iq:tag ("list", { name = list.name } ):up(); + iq:up(); + for resource, session in pairs(bare_sessions[bare_jid].sessions) do + iq.attr.to = bare_jid.."/"..resource + session.send(iq); + end + else + return {"cancel", "bad-request", "internal error."}; + end + return true; +end + +function getList(privacy_lists, origin, stanza, name) + local reply = st.reply(stanza); + reply:tag("query", {xmlns="jabber:iq:privacy"}); + + if name == nil then + if privacy_lists.lists then + if origin.ActivePrivacyList then + reply:tag("active", {name=origin.activePrivacyList}):up(); + end + if privacy_lists.default then + reply:tag("default", {name=privacy_lists.default}):up(); + end + for name,list in pairs(privacy_lists.lists) do + reply:tag("list", {name=name}):up(); + end + end + else + local list = privacy_lists.lists[name]; + if list then + reply = reply:tag("list", {name=list.name}); + for _,item in ipairs(list.items) do + reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order}); + if item["message"] then reply:tag("message"):up(); end + if item["iq"] then reply:tag("iq"):up(); end + if item["presence-in"] then reply:tag("presence-in"):up(); end + if item["presence-out"] then reply:tag("presence-out"):up(); end + reply:up(); + end + else + return {"cancel", "item-not-found", "Unknown list specified."}; + end + end + + origin.send(reply); + return true; +end module:hook("iq/bare/jabber:iq:privacy:query", function(data) local origin, stanza = data.origin, data.stanza; - if not stanza.attr.to then -- only service requests to own bare JID + if stanza.attr.to == nil then -- only service requests to own bare JID local query = stanza.tags[1]; -- the query element - local privacy_lists = datamanager.load(origin.username, origin.host, "privacy") or {}; + local valid = false; + local privacy_lists = datamanager.load(origin.username, origin.host, "privacy") or { lists = {} }; + + if privacy_lists.lists[1] then -- Code to migrate from old privacy lists format, remove in 0.8 + module:log("info", "Upgrading format of stored privacy lists for %s@%s", origin.username, origin.host); + local lists = privacy_lists.lists; + for idx, list in ipairs(lists) do + lists[list.name] = list; + lists[idx] = nil; + end + end + if stanza.attr.type == "set" then - -- TODO + if #query.tags == 1 then -- the <query/> element MUST NOT include more than one child element + for _,tag in ipairs(query.tags) do + if tag.name == "active" or tag.name == "default" then + if tag.attr.name == nil then -- Client declines the use of active / default list + valid = declineList(privacy_lists, origin, stanza, tag.name); + else -- Client requests change of active / default list + valid = activateList(privacy_lists, origin, stanza, tag.name, tag.attr.name); + end + elseif tag.name == "list" and tag.attr.name then -- Client adds / edits a privacy list + if #tag.tags == 0 then -- Client removes a privacy list + valid = deleteList(privacy_lists, origin, stanza, tag.attr.name); + else -- Client edits a privacy list + valid = createOrReplaceList(privacy_lists, origin, stanza, tag.attr.name, tag.tags); + end + end + end + end elseif stanza.attr.type == "get" then - if #query.tags == 0 then -- Client requests names of privacy lists from server - -- TODO - elseif #query.tags == 1 and query.tags[1].name == "list" then -- Client requests a privacy list from server - -- TODO - else - origin.send(st.error_reply(stanza, "modify", "bad-request")); + local name = nil; + local listsToRetrieve = 0; + if #query.tags >= 1 then + for _,tag in ipairs(query.tags) do + if tag.name == "list" then -- Client requests a privacy list from server + name = tag.attr.name; + listsToRetrieve = listsToRetrieve + 1; + end + end + end + if listsToRetrieve == 0 or listsToRetrieve == 1 then + valid = getList(privacy_lists, origin, stanza, name); + end + end + + if valid ~= true then + valid = valid or { "cancel", "bad-request", "Couldn't understand request" }; + if valid[1] == nil then + valid[1] = "cancel"; end + if valid[2] == nil then + valid[2] = "bad-request"; + end + origin.send(st.error_reply(stanza, valid[1], valid[2], valid[3])); + else + datamanager.store(origin.username, origin.host, "privacy", privacy_lists); end + return true; end end); + +function checkIfNeedToBeBlocked(e, session) + local origin, stanza = e.origin, e.stanza; + local privacy_lists = datamanager.load(session.username, session.host, "privacy") or {}; + local bare_jid = session.username.."@"..session.host; + local to = stanza.attr.to; + local from = stanza.attr.from; + + local is_to_user = bare_jid == jid_bare(to); + local is_from_user = bare_jid == jid_bare(from); + + module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from)); + + if privacy_lists.lists == nil or + not (session.activePrivacyList or privacy_lists.default) + then + return; -- Nothing to block, default is Allow all + end + if is_from_user and is_to_user then + module:log("debug", "Not blocking communications between user's resources"); + return; -- from one of a user's resource to another => HANDS OFF! + end + + local item; + local listname = session.activePrivacyList; + if listname == nil then + listname = privacy_lists.default; -- no active list selected, use default list + end + local list = privacy_lists.lists[listname]; + if not list then + module:log("debug", "given privacy list not found. name: %s", listname); + return; + end + for _,item in ipairs(list.items) do + local apply = false; + local block = false; + if ( + (stanza.name == "message" and item.message) or + (stanza.name == "iq" and item.iq) or + (stanza.name == "presence" and is_to_user and item["presence-in"]) or + (stanza.name == "presence" and is_from_user and item["presence-out"]) or + (item.message == false and item.iq == false and item["presence-in"] == false and item["presence-out"] == false) + ) then + apply = true; + end + if apply then + local evilJid = {}; + apply = false; + if is_to_user then + module:log("debug", "evil jid is (from): %s", from); + evilJid.node, evilJid.host, evilJid.resource = jid_split(from); + else + module:log("debug", "evil jid is (to): %s", to); + evilJid.node, evilJid.host, evilJid.resource = jid_split(to); + end + if item.type == "jid" and + (evilJid.node and evilJid.host and evilJid.resource and item.value == evilJid.node.."@"..evilJid.host.."/"..evilJid.resource) or + (evilJid.node and evilJid.host and item.value == evilJid.node.."@"..evilJid.host) or + (evilJid.host and evilJid.resource and item.value == evilJid.host.."/"..evilJid.resource) or + (evilJid.host and item.value == evilJid.host) then + apply = true; + block = (item.action == "deny"); + elseif item.type == "group" then + local roster = load_roster(session.username, session.host); + local 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..5c9ae329 --- /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_roster.lua b/plugins/mod_roster.lua index 52c61a26..4362dca2 100644 --- a/plugins/mod_roster.lua +++ b/plugins/mod_roster.lua @@ -23,12 +23,12 @@ local core_post_stanza = core_post_stanza; module:add_feature("jabber:iq:roster"); local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"}):tag("optional"):up(); -module:add_event_hook("stream-features", - function (session, features) - if session.username then - features:add_child(rosterver_stream_feature); - end - end); +module:hook("stream-features", function(event) + local origin, features = event.origin, event.features; + if origin.username then + features:add_child(rosterver_stream_feature); + end +end); module:add_iq_handler("c2s", "jabber:iq:roster", function (session, stanza) diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index 2aee2be0..0cae5833 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -1,7 +1,7 @@ -- 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. -- @@ -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,7 +34,48 @@ 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 + prosody.unlock_globals(); --FIXME: Figure out why this is needed and + -- why cyrussasl isn't caught by the sandbox + local ok, cyrus = pcall(require, "util.sasl_cyrus"); + prosody.lock_globals(); + if ok then + local cyrus_new = cyrus.new; + new_sasl = function(realm) + return cyrus_new(realm, module:get_option("cyrus_service_name") or "xmpp"); + end + else + sasl_backend = "builtin"; + module:log("warn", "Failed to load Cyrus SASL, falling back to builtin auth mechanisms"); + module:log("debug", "Failed to load Cyrus because: %s", cyrus); + end +end +if not new_sasl then + if sasl_backend ~= "builtin" then module:log("warn", "Unknown SASL backend %s", sasl_backend); end; + new_sasl = require "util.sasl".new; +end + +local default_authentication_profile = { + plain = function(username, realm) + local prepped_username = nodeprep(username); + if not prepped_username then + log("debug", "NODEprep failed on username: %s", username); + return "", nil; + end + local password = usermanager_get_password(prepped_username, realm); + if not password then + return "", nil; + end + return password, true; + end +}; + +local anonymous_authentication_profile = { + anonymous = function(username, realm) + return true; -- for normal usage you should always return true here + end +}; local function build_reply(status, ret, err_msg) local reply = st.stanza(status, {xmlns = xmlns_sasl}); @@ -54,50 +96,18 @@ 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); - session.sasl_handler = nil; if not username then -- TODO move this to sessionmanager module:log("warn", "SASL succeeded but we didn't get a username!"); session.sasl_handler = nil; session:reset_stream(); return; - end - sm_make_authenticated(session, username); - session:reset_stream(); - end -end - -local function credentials_callback(mechanism, ...) - if mechanism == "PLAIN" then - local username, hostname, password = ...; - username = nodeprep(username); - if not username then - return false; - end - local response = usermanager_validate_credentials(hostname, username, password, mechanism); - if response == nil then - return false; - else - return response; - end - elseif mechanism == "DIGEST-MD5" then - local function func(x) return x; end - local node, domain, realm, decoder = ...; - local prepped_node = nodeprep(node); - if not prepped_node then - return func, nil; - end - local password = usermanager_get_password(prepped_node, domain); - if password then - if decoder then - node, realm, password = decoder(node), decoder(realm), decoder(password); - end - return func, md5(node..":"..realm..":"..password); - else - return func, nil; end + sm_make_authenticated(session, session.sasl_handler.username); + session.sasl_handler = nil; + session:reset_stream(); end end @@ -111,8 +121,8 @@ local function sasl_handler(session, stanza) elseif stanza.attr.mechanism == "ANONYMOUS" then return session.send(build_reply("failure", "mechanism-too-weak")); end - session.sasl_handler = new_sasl(stanza.attr.mechanism, session.host, credentials_callback); - if not session.sasl_handler then + local valid_mechanism = session.sasl_handler:select(stanza.attr.mechanism); + if not valid_mechanism then return session.send(build_reply("failure", "invalid-mechanism")); end if secure_auth_only and not session.secure then @@ -131,7 +141,7 @@ local function sasl_handler(session, stanza) return; end end - local status, ret, err_msg = session.sasl_handler:feed(text); + local status, ret, err_msg = session.sasl_handler:process(text); handle_status(session, status); local s = build_reply(status, ret, err_msg); log("debug", "sasl reply: %s", tostring(s)); @@ -145,54 +155,54 @@ module:add_handler("c2s_unauthed", "response", xmlns_sasl, sasl_handler); local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' }; local bind_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-bind' }; local xmpp_session_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-session' }; -module:add_event_hook("stream-features", - function (session, features) - if not session.username then - if secure_auth_only and not session.secure then - return; - end - features:tag("mechanisms", mechanisms_attr); - -- TODO: Provide PLAIN only if TLS is active, this is a SHOULD from the introduction of RFC 4616. This behavior could be overridden via configuration but will issuing a warning or so. - if config.get(session.host or "*", "core", "anonymous_login") then - features:tag("mechanism"):text("ANONYMOUS"):up(); - else - local mechanisms = usermanager_get_supported_methods(session.host or "*"); - for k, v in pairs(mechanisms) do - features:tag("mechanism"):text(k):up(); - end - end - features:up(); - else - features:tag("bind", bind_attr):tag("required"):up():up(); - features:tag("session", xmpp_session_attr):tag("optional"):up():up(); - end - end); - -module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind", - function (session, stanza) - log("debug", "Client requesting a resource bind"); - local resource; - if stanza.attr.type == "set" then - local bind = stanza.tags[1]; - if bind and bind.attr.xmlns == xmlns_bind then - resource = bind:child_with_name("resource"); - if resource then - resource = resource[1]; - end - end +module:hook("stream-features", function(event) + local origin, features = event.origin, event.features; + if not origin.username then + if secure_auth_only and not origin.secure then + return; + end + if module:get_option("anonymous_login") then + origin.sasl_handler = new_sasl(origin.host, anonymous_authentication_profile); + else + origin.sasl_handler = new_sasl(origin.host, default_authentication_profile); + if not (module:get_option("allow_unencrypted_plain_auth")) and not origin.secure then + origin.sasl_handler:forbidden({"PLAIN"}); end - local success, err_type, err, err_msg = sm_bind_resource(session, resource); - if not success then - session.send(st.error_reply(stanza, err_type, err, err_msg)); - else - session.send(st.reply(stanza) - :tag("bind", { xmlns = xmlns_bind}) - :tag("jid"):text(session.full_jid)); + end + features:tag("mechanisms", mechanisms_attr); + for k, v in pairs(origin.sasl_handler:mechanisms()) do + features:tag("mechanism"):text(v):up(); + end + features:up(); + else + features:tag("bind", bind_attr):tag("required"):up():up(); + features:tag("session", xmpp_session_attr):tag("optional"):up():up(); + end +end); + +module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-bind", function(session, stanza) + log("debug", "Client requesting a resource bind"); + local resource; + if stanza.attr.type == "set" then + local bind = stanza.tags[1]; + if bind and bind.attr.xmlns == xmlns_bind then + resource = bind:child_with_name("resource"); + if resource then + resource = resource[1]; end - end); + end + end + local success, err_type, err, err_msg = sm_bind_resource(session, resource); + if not success then + session.send(st.error_reply(stanza, err_type, err, err_msg)); + else + session.send(st.reply(stanza) + :tag("bind", { xmlns = xmlns_bind}) + :tag("jid"):text(session.full_jid)); + end +end); -module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-session", - function (session, stanza) - log("debug", "Client requesting a session"); - session.send(st.reply(stanza)); - end); +module:add_iq_handler("c2s", "urn:ietf:params:xml:ns:xmpp-session", function(session, stanza) + log("debug", "Client requesting a session"); + session.send(st.reply(stanza)); +end); diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index 67555b15..7aee2921 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -8,96 +8,81 @@ local st = require "util.stanza"; -local xmlns_stream = 'http://etherx.jabber.org/streams'; -local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls'; - local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption"); local secure_s2s_only = module:get_option("s2s_require_encryption"); -local host = hosts[module.host]; +local xmlns_starttls = 'urn:ietf:params:xml:ns:xmpp-tls'; +local starttls_attr = { xmlns = xmlns_starttls }; +local starttls_proceed = st.stanza("proceed", starttls_attr); +local starttls_failure = st.stanza("failure", starttls_attr); +local c2s_feature = st.stanza("starttls", starttls_attr); +local s2s_feature = st.stanza("starttls", starttls_attr); +if secure_auth_only then c2s_feature:tag("required"):up(); end +if secure_s2s_only then s2s_feature:tag("required"):up(); end + +local global_ssl_ctx = prosody.global_ssl_ctx; -module:add_handler("c2s_unauthed", "starttls", xmlns_starttls, - function (session, stanza) - if session.conn.starttls and host.ssl_ctx_in then - session.send(st.stanza("proceed", { xmlns = xmlns_starttls })); - session:reset_stream(); - if session.host and hosts[session.host].ssl_ctx_in then - session.conn.set_sslctx(hosts[session.host].ssl_ctx_in); - end - session.conn.starttls(); - session.log("info", "TLS negotiation started..."); - session.secure = false; - else - session.log("warn", "Attempt to start TLS, but TLS is not available on this connection"); - (session.sends2s or session.send)(st.stanza("failure", { xmlns = xmlns_starttls })); - session:close(); - end - end); - -module:add_handler("s2sin_unauthed", "starttls", xmlns_starttls, - function (session, stanza) - if session.conn.starttls and host.ssl_ctx_in then - session.sends2s(st.stanza("proceed", { xmlns = xmlns_starttls })); - session:reset_stream(); - if session.to_host and hosts[session.to_host].ssl_ctx_in then - session.conn.set_sslctx(hosts[session.to_host].ssl_ctx_in); - end - session.conn.starttls(); - session.log("info", "TLS negotiation started for incoming s2s..."); - session.secure = false; - else - session.log("warn", "Attempt to start TLS, but TLS is not available on this s2s connection"); - (session.sends2s or session.send)(st.stanza("failure", { xmlns = xmlns_starttls })); - session:close(); - end - end); +local host = hosts[module.host]; +local function can_do_tls(session) + if session.type == "c2s_unauthed" then + return session.conn.starttls and host.ssl_ctx_in; + elseif session.type == "s2sin_unauthed" then + return session.conn.starttls and host.ssl_ctx_in; + elseif session.direction == "outgoing" then + return session.conn.starttls and host.ssl_ctx; + end + return false; +end -local starttls_attr = { xmlns = xmlns_starttls }; -module:add_event_hook("stream-features", - function (session, features) - if session.conn.starttls then - features:tag("starttls", starttls_attr); - if secure_auth_only then - features:tag("required"):up():up(); - else - features:up(); - end - end - end); +-- Hook <starttls/> +module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event) + local origin = event.origin; + if can_do_tls(origin) then + (origin.sends2s or origin.send)(starttls_proceed); + origin:reset_stream(); + local host = origin.to_host or origin.host; + local ssl_ctx = host and hosts[host].ssl_ctx_in or global_ssl_ctx; + origin.conn:starttls(ssl_ctx); + origin.log("info", "TLS negotiation started for %s...", origin.type); + origin.secure = false; + else + origin.log("warn", "Attempt to start TLS, but TLS is not available on this %s connection", origin.type); + (origin.sends2s or origin.send)(starttls_failure); + origin:close(); + end + return true; +end); -module:hook("s2s-stream-features", - function (data) - local session, features = data.session, data.features; - if session.to_host and session.conn.starttls then - features:tag("starttls", starttls_attr); - if secure_s2s_only then - features:tag("required"):up():up(); - else - features:up(); - end - end - end); +-- Advertize stream feature +module:hook("stream-features", function(event) + local origin, features = event.origin, event.features; + if can_do_tls(origin) then + features:add_child(c2s_feature); + end +end); +module:hook("s2s-stream-features", function(event) + local origin, features = event.origin, event.features; + if can_do_tls(origin) then + features:add_child(s2s_feature); + end +end); -- For s2sout connections, start TLS if we can -module:hook_stanza(xmlns_stream, "features", - function (session, stanza) - module:log("debug", "Received features element"); - if session.conn.starttls and stanza:child_with_ns(xmlns_starttls) then - module:log("%s is offering TLS, taking up the offer...", session.to_host); - session.sends2s("<starttls xmlns='"..xmlns_starttls.."'/>"); - return true; - end - end, 500); +module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza) + module:log("debug", "Received features element"); + if can_do_tls(session) and stanza:child_with_ns(xmlns_starttls) then + module:log("%s is offering TLS, taking up the offer...", session.to_host); + session.sends2s("<starttls xmlns='"..xmlns_starttls.."'/>"); + return true; + end +end, 500); -module:hook_stanza(xmlns_starttls, "proceed", - function (session, stanza) - module:log("debug", "Proceeding with TLS on s2sout..."); - local format, to_host, from_host = string.format, session.to_host, session.from_host; - local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx; - session.conn.set_sslctx(ssl_ctx); - session:reset_stream(); - session.conn.starttls(true); - session.secure = false; - return true; - end); +module:hook_stanza(xmlns_starttls, "proceed", function (session, stanza) + module:log("debug", "Proceeding with TLS on s2sout..."); + session:reset_stream(); + local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx; + session.conn:starttls(ssl_ctx, true); + session.secure = false; + return true; +end); diff --git a/plugins/mod_xmlrpc.lua b/plugins/mod_xmlrpc.lua deleted file mode 100644 index 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 60687886..e6f459ae 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -59,19 +59,12 @@ local kickable_error_conditions = { ["service-unavailable"] = true; ["malformed error"] = true; }; + local function get_error_condition(stanza) - for _, tag in ipairs(stanza.tags) do - if tag.name == "error" and (not(tag.attr.xmlns) or tag.attr.xmlns == "jabber:client") then - for _, cond in ipairs(tag.tags) do - if cond.attr.xmlns == "urn:ietf:params:xml:ns:xmpp-stanzas" then - return cond.name; - end - end - return "malformed error"; - end - end - return "malformed error"; + local _, condition = stanza:get_error(); + return condition or "malformed error"; end + local function is_kickable_error(stanza) local cond = get_error_condition(stanza); return kickable_error_conditions[cond] and cond; @@ -89,17 +82,6 @@ local function getTag(stanza, path) return getUsingPath(stanza, path); end local function getText(stanza, path) return getUsingPath(stanza, path, true); end ----------- ---[[function get_room_disco_info(room, stanza) - return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info") - :tag("identity", {category='conference', type='text', name=room._data["name"]):up() - :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply -end -function get_room_disco_items(room, stanza) - return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); -end -- TODO allow non-private rooms]] - --- - local room_mt = {}; room_mt.__index = room_mt; @@ -183,12 +165,12 @@ function room_mt:send_history(to) end end -local function room_get_disco_info(self, stanza) +function room_mt:get_disco_info(stanza) return st.reply(stanza):query("http://jabber.org/protocol/disco#info") :tag("identity", {category="conference", type="text"}):up() :tag("feature", {var="http://jabber.org/protocol/muc"}); end -local function room_get_disco_items(self, stanza) +function room_mt:get_disco_items(stanza) local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); for room_jid in pairs(self._occupants) do reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up(); @@ -206,6 +188,16 @@ function room_mt:set_subject(current_nick, subject) return true; end +local function build_unavailable_presence_from_error(stanza) + local type, condition, text = stanza:get_error(); + local error_message = "Kicked: "..condition:gsub("%-", " "); + if text then + error_message = error_message..": "..text; + end + return st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to}) + :tag('status'):text(error_message); +end + function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc local from, to = stanza.attr.from, stanza.attr.to; local room = jid_bare(to); @@ -219,8 +211,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc if type == "error" then -- error, kick em out! if current_nick then log("debug", "kicking %s from %s", current_nick, room); - self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}) - :tag('status'):text('Kicked: '..get_error_condition(stanza))); -- send unavailable + self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); end elseif type == "unavailable" then -- unavailable if current_nick then @@ -367,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 @@ -389,51 +379,112 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc end end -function room_mt:handle_form(origin, stanza) - if self:get_affiliation(stanza.attr.from) ~= "owner" then origin.send(st.error_reply(stanza, "auth", "forbidden")); return; end - if stanza.attr.type == "get" then - local title = "Configuration for "..self.jid; - origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") - :tag("x", {xmlns='jabber:x:data', type='form'}) - :tag("title"):text(title):up() - :tag("instructions"):text(title):up() - :tag("field", {type='hidden', var='FORM_TYPE'}):tag("value"):text("http://jabber.org/protocol/muc#roomconfig"):up():up() - :tag("field", {type='boolean', label='Make Room Persistent?', var='muc#roomconfig_persistentroom'}) - :tag("value"):text(self._data.persistent and "1" or "0"):up() +function room_mt:send_form(origin, stanza) + local title = "Configuration for "..self.jid; + origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") + :tag("x", {xmlns='jabber:x:data', type='form'}) + :tag("title"):text(title):up() + :tag("instructions"):text(title):up() + :tag("field", {type='hidden', var='FORM_TYPE'}):tag("value"):text("http://jabber.org/protocol/muc#roomconfig"):up():up() + :tag("field", {type='boolean', label='Make Room Persistent?', var='muc#roomconfig_persistentroom'}) + :tag("value"):text(self._data.persistent and "1" or "0"):up() + :up() + :tag("field", {type='boolean', label='Make Room Publicly Searchable?', var='muc#roomconfig_publicroom'}) + :tag("value"):text(self._data.hidden and "0" or "1"):up() + :up() + :tag("field", {type='list-single', label='Who May Discover Real JIDs?', var='muc#roomconfig_whois'}) + :tag("value"):text(self._data.whois or 'moderators'):up() + :tag("option", {label = 'Moderators Only'}) + :tag("value"):text('moderators'):up() :up() - :tag("field", {type='boolean', label='Make Room Publicly Searchable?', var='muc#roomconfig_publicroom'}) - :tag("value"):text(self._data.hidden and "0" or "1"):up() + :tag("option", {label = 'Anyone'}) + :tag("value"):text('anyone'):up() :up() - ); - elseif stanza.attr.type == "set" then - local query = stanza.tags[1]; - local form; - for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end - if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end - if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end - if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end - local fields = {}; - for _, field in pairs(form.tags) do - if field.name == "field" and field.attr.var and field.tags[1].name == "value" and #field.tags[1].tags == 0 then - fields[field.attr.var] = field.tags[1][1] or ""; - end + :up() + ); +end + +local valid_whois = { + moderators = true, + anyone = true, +} + +function room_mt:process_form(origin, stanza) + local query = stanza.tags[1]; + local form; + for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end + if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end + if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end + if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + local fields = {}; + for _, field in pairs(form.tags) do + if field.name == "field" and field.attr.var and field.tags[1].name == "value" and #field.tags[1].tags == 0 then + fields[field.attr.var] = field.tags[1][1] or ""; end - if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + end + if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + + local dirty = false + + local persistent = fields['muc#roomconfig_persistentroom']; + if persistent == "0" or persistent == "false" then persistent = nil; elseif persistent == "1" or persistent == "true" then persistent = true; + else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + dirty = dirty or (self._data.persistent ~= persistent) + self._data.persistent = persistent; + module:log("debug", "persistent=%s", tostring(persistent)); - local persistent = fields['muc#roomconfig_persistentroom']; - if persistent == "0" or persistent == "false" then persistent = nil; elseif persistent == "1" or persistent == "true" then persistent = true; - else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end - 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 public = fields['muc#roomconfig_publicroom']; - if public == "0" or public == "false" then public = nil; elseif public == "1" or public == "true" then public = true; - else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end - self._data.hidden = not public and true or nil; + local whois = fields['muc#roomconfig_whois']; + if not valid_whois[whois] then + origin.send(st.error_reply(stanza, 'cancel', 'bad-request')); + return; + end + local whois_changed = self._data.whois ~= whois + self._data.whois = whois + module:log('debug', 'whois=%s', tostring(whois)) + + if self.save then self:save(true); end + origin.send(st.reply(stanza)); + + if dirty or whois_changed then + local msg = st.message({type='groupchat', from=self.jid}) + :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up() + + if dirty then + msg.tags[1]:tag('status', {code = '104'}) + end + if whois_changed then + local code = (whois == 'moderators') and 173 or 172 + msg.tags[1]:tag('status', {code = code}) + end + + self:broadcast_message(msg, false) + end +end - if self.save then self:save(true); end - origin.send(st.reply(stanza)); +function room_mt:destroy(newjid, reason, password) + local pr = st.presence({type = "unavailable"}) + :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) + :tag("item", { affiliation='none', role='none' }):up() + :tag("destroy", {jid=newjid}) + if reason then pr:tag("reason"):text(reason):up(); end + if password then pr:tag("password"):text(password):up(); end + for nick, occupant in pairs(self._occupants) do + pr.attr.from = nick; + for jid in pairs(occupant.sessions) do + pr.attr.to = jid; + self:_route_stanza(pr); + self._jid_nick[jid] = nil; + end + self._occupants[nick] = nil; end + self._data.persistent = nil; + if self.save then self:save(true); end end function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc @@ -441,9 +492,9 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; if stanza.name == "iq" then if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" then - origin.send(room_get_disco_info(self, stanza)); + origin.send(self:get_disco_info(stanza)); elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" then - origin.send(room_get_disco_items(self, stanza)); + origin.send(self:get_disco_items(stanza)); elseif xmlns == "http://jabber.org/protocol/muc#admin" then local actor = stanza.attr.from; local affiliation = self:get_affiliation(actor); @@ -519,7 +570,30 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha origin.send(st.error_reply(stanza, "cancel", "bad-request")); end elseif xmlns == "http://jabber.org/protocol/muc#owner" and (type == "get" or type == "set") and stanza.tags[1].name == "query" then - self:handle_form(origin, stanza); + if self:get_affiliation(stanza.attr.from) ~= "owner" then + origin.send(st.error_reply(stanza, "auth", "forbidden")); + elseif stanza.attr.type == "get" then + self:send_form(origin, stanza); + elseif stanza.attr.type == "set" then + local child = stanza.tags[1].tags[1]; + if not child then + origin.send(st.error_reply(stanza, "auth", "bad-request")); + elseif child.name == "destroy" then + local newjid = child.attr.jid; + local reason, password; + for _,tag in ipairs(child.tags) do + if tag.name == "reason" then + reason = #tag.tags == 0 and tag[1]; + elseif tag.name == "password" then + password = #tag.tags == 0 and tag[1]; + end + end + self:destroy(newjid, reason, password); + origin.send(st.reply(stanza)); + else + self:process_form(origin, stanza); + end + end elseif type == "set" or type == "get" then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end @@ -551,8 +625,7 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha elseif stanza.name == "message" and type == "error" and is_kickable_error(stanza) then local current_nick = self._jid_nick[stanza.attr.from]; log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid); - self:handle_to_occupant(origin, st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to}) - :tag('status'):text('Kicked: '..get_error_condition(stanza))); -- send unavailable + self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable elseif stanza.name == "presence" then -- hack - some buggy clients send presence updates to the room rather than their nick local to = stanza.attr.to; local current_nick = self._jid_nick[stanza.attr.from]; @@ -707,13 +780,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 @@ -728,6 +799,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 @@ -746,7 +820,9 @@ function _M.new_room(jid) jid = jid; _jid_nick = {}; _occupants = {}; - _data = {}; + _data = { + whois = 'moderators', + }; _affiliations = {}; }, room_mt); end |