# HG changeset patch # User Matthew Wild # Date 1363090225 0 # Node ID a9dfa7232d8821e0930c8c038eaeda08c4fd19a5 # Parent f88381a39c56a7c03e020e3c6a4947d89237dc4f# Parent 720b8268778eb38dec1391889defbe4f26a90b6d Merge diff -r f88381a39c56 -r a9dfa7232d88 mod_admin_web/admin_web/mod_admin_web.lua --- a/mod_admin_web/admin_web/mod_admin_web.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_admin_web/admin_web/mod_admin_web.lua Tue Mar 12 12:10:25 2013 +0000 @@ -22,27 +22,15 @@ local is_admin = require "core.usermanager".is_admin; local pubsub = require "util.pubsub"; local jid_bare = require "util.jid".bare; -local lfs = require "lfs"; -local open = io.open; -local stat = lfs.attributes; module:set_global(); local service = {}; -local http_base = module.path:gsub("/[^/]+$","") .. "/www_files/"; - local xmlns_adminsub = "http://prosody.im/adminsub"; local xmlns_c2s_session = "http://prosody.im/streams/c2s"; local xmlns_s2s_session = "http://prosody.im/streams/s2s"; -local mime_map = { - html = "text/html"; - xml = "text/xml"; - js = "text/javascript"; - css = "text/css"; -}; - local idmap = {}; function add_client(session, host) @@ -104,37 +92,14 @@ end end -function serve_file(event, path) - local full_path = http_base .. path; - - if stat(full_path, "mode") == "directory" then - if stat(full_path.."/index.html", "mode") == "file" then - return serve_file(event, path.."/index.html"); - end - return 403; - end - - local f, err = open(full_path, "rb"); - if not f then - return 404; - end - - local data = f:read("*a"); - f:close(); - if not data then - return 403; - end - - local ext = path:match("%.([^.]*)$"); - event.response.headers.content_type = mime_map[ext]; -- Content-Type should be nil when not known - return data; -end - function module.add_host(module) -- Dependencies module:depends("bosh"); module:depends("admin_adhoc"); module:depends("http"); + local serve_file = module:depends("http_files").serve { + path = module:get_directory() .. "/www_files"; + }; -- Setup HTTP server module:provides("http", { @@ -149,12 +114,14 @@ }); -- Setup adminsub service - local function simple_broadcast(node, jids, item) - item = st.clone(item); - item.attr.xmlns = nil; -- Clear the pubsub namespace + local function simple_broadcast(kind, node, jids, item) + if item then + item = st.clone(item); + item.attr.xmlns = nil; -- Clear the pubsub namespace + end local message = st.message({ from = module.host, type = "headline" }) :tag("event", { xmlns = xmlns_adminsub .. "#event" }) - :tag("items", { node = node }) + :tag(kind, { node = node }) :add_child(item); for jid in pairs(jids) do module:log("debug", "Sending notification to %s", jid); diff -r f88381a39c56 -r a9dfa7232d88 mod_admin_web/admin_web/www_files/js/main.js --- a/mod_admin_web/admin_web/www_files/js/main.js Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_admin_web/admin_web/www_files/js/main.js Tue Mar 12 12:10:25 2013 +0000 @@ -96,6 +96,7 @@ if (status == Strophe.Status.CONNECTING) { log('Strophe is connecting.'); } else if (status == Strophe.Status.CONNFAIL) { + alert('Connection failed (Wrong host?)'); log('Strophe failed to connect.'); showConnect(); } else if (status == Strophe.Status.DISCONNECTING) { @@ -104,6 +105,7 @@ log('Strophe is disconnected.'); showConnect(); } else if (status == Strophe.Status.AUTHFAIL) { + alert('Wrong username and/or password'); log('Authentication failed'); if (connection) { connection.disconnect(); @@ -120,7 +122,12 @@ return false; } for (i = 0; i < items.length; i++) { - $('#host').append(''); + var host = $(items[i]).text(); + if (host == Strophe.getDomainFromJid(connection.jid)) { + $('#host').append(''); + } else { + $('#host').append(''); + } } showDisconnect(); adminsubHost = $(items[0]).text(); diff -r f88381a39c56 -r a9dfa7232d88 mod_auth_external/mod_auth_external.lua --- a/mod_auth_external/mod_auth_external.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_auth_external/mod_auth_external.lua Tue Mar 12 12:10:25 2013 +0000 @@ -10,7 +10,6 @@ -- -local nodeprep = require "util.encodings".stringprep.nodeprep; --local process = require "process"; local lpc; pcall(function() lpc = require "lpc"; end); @@ -81,8 +80,6 @@ function do_query(kind, username, password) if not username then return nil, "not-acceptable"; end - username = nodeprep(username); - if not username then return nil, "jid-malformed"; end local query = (password and "%s:%s:%s:%s" or "%s:%s:%s"):format(kind, username, host, password); local len = #query @@ -132,12 +129,7 @@ function provider.get_sasl_handler() local testpass_authentication_profile = { plain_test = function(sasl, username, password, realm) - local prepped_username = nodeprep(username); - if not prepped_username then - log("debug", "NODEprep failed on username: %s", username); - return "", nil; - end - return usermanager.test_password(prepped_username, realm, password), true; + return usermanager.test_password(username, realm, password), true; end, }; return new_sasl(host, testpass_authentication_profile); diff -r f88381a39c56 -r a9dfa7232d88 mod_auth_internal_yubikey/mod_auth_internal_yubikey.lua --- a/mod_auth_internal_yubikey/mod_auth_internal_yubikey.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_auth_internal_yubikey/mod_auth_internal_yubikey.lua Tue Mar 12 12:10:25 2013 +0000 @@ -18,7 +18,6 @@ local config = require "core.configmanager"; local usermanager = require "core.usermanager"; local new_sasl = require "util.sasl".new; -local nodeprep = require "util.encodings".stringprep.nodeprep; local hosts = hosts; local prosody = _G.prosody; @@ -106,12 +105,6 @@ local realm = module:get_option("sasl_realm") or module.host; local getpass_authentication_profile = { plain_test = function(sasl, username, password, realm) - local prepped_username = nodeprep(username); - if not prepped_username then - log("debug", "NODEprep failed on username: %s", username); - return false, nil; - end - return usermanager.test_password(username, realm, password), true; end }; diff -r f88381a39c56 -r a9dfa7232d88 mod_auth_ldap/mod_auth_ldap.lua --- a/mod_auth_ldap/mod_auth_ldap.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_auth_ldap/mod_auth_ldap.lua Tue Mar 12 12:10:25 2013 +0000 @@ -1,6 +1,5 @@ local new_sasl = require "util.sasl".new; -local nodeprep = require "util.encodings".stringprep.nodeprep; local log = require "util.logger".init("auth_ldap"); local ldap_server = module:get_option("ldap_server") or "localhost"; @@ -42,12 +41,7 @@ function provider.get_sasl_handler() local testpass_authentication_profile = { plain_test = function(sasl, username, password, realm) - local prepped_username = nodeprep(username); - if not prepped_username then - log("debug", "NODEprep failed on username: %s", username); - return "", nil; - end - return provider.test_password(prepped_username, password), true; + return provider.test_password(username, password), true; end }; return new_sasl(module.host, testpass_authentication_profile); diff -r f88381a39c56 -r a9dfa7232d88 mod_auth_ldap2/mod_auth_ldap.lua --- a/mod_auth_ldap2/mod_auth_ldap.lua Thu Nov 22 18:59:10 2012 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,84 +0,0 @@ --- vim:sts=4 sw=4 - --- Prosody IM --- Copyright (C) 2008-2010 Matthew Wild --- Copyright (C) 2008-2010 Waqas Hussain --- Copyright (C) 2012 Rob Hoelz --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- --- http://code.google.com/p/prosody-modules/source/browse/mod_auth_ldap/mod_auth_ldap.lua --- adapted to use common LDAP store - -local ldap = module:require 'ldap'; -local new_sasl = require 'util.sasl'.new; -local nodeprep = require 'util.encodings'.stringprep.nodeprep; -local jsplit = require 'util.jid'.split; - -if not ldap then - return; -end - -local provider = {} - -function provider.test_password(username, password) - return ldap.bind(username, password); -end - -function provider.user_exists(username) - local params = ldap.getparams() - - local filter = ldap.filter.combine_and(params.user.filter, params.user.usernamefield .. '=' .. username); - - return ldap.singlematch { - base = params.user.basedn, - filter = filter, - }; -end - -function provider.get_password(username) - return nil, "Passwords unavailable for LDAP."; -end - -function provider.set_password(username, password) - return nil, "Passwords unavailable for LDAP."; -end - -function provider.create_user(username, password) - return nil, "Account creation/modification not available with LDAP."; -end - -function provider.get_sasl_handler() - local testpass_authentication_profile = { - plain_test = function(sasl, username, password, realm) - local prepped_username = nodeprep(username); - if not prepped_username then - module:log("debug", "NODEprep failed on username: %s", username); - return "", nil; - end - return provider.test_password(prepped_username, password), true; - end, - mechanisms = { PLAIN = true }, - }; - return new_sasl(module.host, testpass_authentication_profile); -end - -function provider.is_admin(jid) - local admin_config = ldap.getparams().admin; - - if not admin_config then - return; - end - - local ld = ldap:getconnection(); - local username = jsplit(jid); - local filter = ldap.filter.combine_and(admin_config.filter, admin_config.namefield .. '=' .. username); - - return ldap.singlematch { - base = admin_config.basedn, - filter = filter, - }; -end - -module:provides("auth", provider); diff -r f88381a39c56 -r a9dfa7232d88 mod_auth_ldap2/mod_auth_ldap2.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_auth_ldap2/mod_auth_ldap2.lua Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,78 @@ +-- vim:sts=4 sw=4 + +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2012 Rob Hoelz +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +-- http://code.google.com/p/prosody-modules/source/browse/mod_auth_ldap/mod_auth_ldap.lua +-- adapted to use common LDAP store + +local ldap = module:require 'ldap'; +local new_sasl = require 'util.sasl'.new; +local jsplit = require 'util.jid'.split; + +if not ldap then + return; +end + +local provider = {} + +function provider.test_password(username, password) + return ldap.bind(username, password); +end + +function provider.user_exists(username) + local params = ldap.getparams() + + local filter = ldap.filter.combine_and(params.user.filter, params.user.usernamefield .. '=' .. username); + + return ldap.singlematch { + base = params.user.basedn, + filter = filter, + }; +end + +function provider.get_password(username) + return nil, "Passwords unavailable for LDAP."; +end + +function provider.set_password(username, password) + return nil, "Passwords unavailable for LDAP."; +end + +function provider.create_user(username, password) + return nil, "Account creation/modification not available with LDAP."; +end + +function provider.get_sasl_handler() + local testpass_authentication_profile = { + plain_test = function(sasl, username, password, realm) + return provider.test_password(username, password), true; + end, + mechanisms = { PLAIN = true }, + }; + return new_sasl(module.host, testpass_authentication_profile); +end + +function provider.is_admin(jid) + local admin_config = ldap.getparams().admin; + + if not admin_config then + return; + end + + local ld = ldap:getconnection(); + local username = jsplit(jid); + local filter = ldap.filter.combine_and(admin_config.filter, admin_config.namefield .. '=' .. username); + + return ldap.singlematch { + base = admin_config.basedn, + filter = filter, + }; +end + +module:provides("auth", provider); diff -r f88381a39c56 -r a9dfa7232d88 mod_auth_sql/mod_auth_sql.lua --- a/mod_auth_sql/mod_auth_sql.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_auth_sql/mod_auth_sql.lua Tue Mar 12 12:10:25 2013 +0000 @@ -5,7 +5,6 @@ local log = require "util.logger".init("auth_sql"); local new_sasl = require "util.sasl".new; -local nodeprep = require "util.encodings".stringprep.nodeprep; local DBI = require "DBI" local connection; @@ -101,12 +100,7 @@ function provider.get_sasl_handler() local profile = { plain = function(sasl, username, realm) - local prepped_username = nodeprep(username); - if not prepped_username then - module:log("debug", "NODEprep failed on username: %s", username); - return "", nil; - end - local password = get_password(prepped_username); + local password = get_password(username); if not password then return "", nil; end return password, true; end diff -r f88381a39c56 -r a9dfa7232d88 mod_bidi/mod_bidi.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_bidi/mod_bidi.lua Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,129 @@ +-- Bidirectional Server-to-Server Connections +-- http://xmpp.org/extensions/xep-0288.html +-- Copyright (C) 2013 Kim Alvefur +-- +-- This file is MIT/X11 licensed. +-- +local s2smanager = require"core.s2smanager"; +local add_filter = require "util.filters".add_filter; +local st = require "util.stanza"; +local jid_split = require"util.jid".prepped_split; + +local xmlns_bidi_feature = "urn:xmpp:features:bidi" +local xmlns_bidi = "urn:xmpp:bidi"; +local noop = function () end +local core_process_stanza = prosody.core_process_stanza or core_process_stanza; +local traceback = debug.traceback; + +local function handleerr(err) log("error", "Traceback[s2s]: %s: %s", tostring(err), traceback()); end +local function handlestanza(session, stanza) + if stanza.attr.xmlns == "jabber:client" then --COMPAT: Prosody pre-0.6.2 may send jabber:client + stanza.attr.xmlns = nil; + end + -- stanza = session.filter("stanzas/in", stanza); + if stanza then + return xpcall(function () return core_process_stanza(session, stanza) end, handleerr); + end +end + +local function new_bidi(origin) + local bidi_session, remote_host; + origin.log("debug", "Creating bidirectional session wrapper"); + if origin.direction == "incoming" then -- then we create an "outgoing" bidirectional session + local conflicting_session = hosts[origin.to_host].s2sout[origin.from_host] + if conflicting_session then + conflicting_session.log("info", "We already have an outgoing connection to %s, closing it...", origin.from_host); + conflicting_session:close{ condition = "conflict", text = "Replaced by bidirectional stream" } + s2smanager.destroy_session(conflicting_session); + end + remote_host = origin.from_host; + bidi_session = s2smanager.new_outgoing(origin.to_host, origin.from_host) + else -- outgoing -- then we create an "incoming" bidirectional session + remote_host = origin.to_host; + bidi_session = s2smanager.new_incoming(origin.conn) + bidi_session.to_host = origin.from_host; + bidi_session.from_host = origin.to_host; + add_filter(origin, "stanzas/in", function(stanza) + if stanza.attr.xmlns ~= nil then return stanza end + local _, host = jid_split(stanza.attr.from); + if host ~= remote_host then return stanza end + handlestanza(bidi_session, stanza); + end, 1); + end + origin.bidi_session = bidi_session; + bidi_session.sends2s = origin.sends2s; + bidi_session.bounce_sendq = noop; + bidi_session.notopen = nil; + bidi_session.is_bidi = true; + bidi_session.bidi_session = false; + bidi_session.orig_session = origin; + bidi_session.secure = origin.secure; + bidi_session.cert_identity_status = origin.cert_identity_status; + bidi_session.cert_chain_status = origin.cert_chain_status; + bidi_session.close = function(...) + return origin.close(...); + end + + bidi_session.log("info", "Bidirectional session established"); + s2smanager.make_authenticated(bidi_session, remote_host); + return bidi_session; +end + +-- Incoming s2s +module:hook("s2s-stream-features", function(event) + local origin, features = event.origin, event.features; + if not origin.is_bidi and not hosts[module.host].s2sout[origin.from_host] then + module:log("debug", "Announcing support for bidirectional streams"); + features:tag("bidi", { xmlns = xmlns_bidi_feature }):up(); + end +end); + +module:hook("stanza/urn:xmpp:bidi:bidi", function(event) + local origin = event.session or event.origin; + if not origin.is_bidi and not origin.bidi_session then + module:log("debug", "%s requested bidirectional stream", origin.from_host); + origin.do_bidi = true; + return true; + end +end); + +-- Outgoing s2s +module:hook("stanza/http://etherx.jabber.org/streams:features", function(event) + local origin = event.session or event.origin; + if not ( origin.bidi_session or origin.is_bidi or origin.do_bidi) + and event.stanza:get_child("bidi", xmlns_bidi_feature) then + module:log("debug", "%s supports bidirectional streams", origin.to_host); + origin.sends2s(st.stanza("bidi", { xmlns = xmlns_bidi })); + origin.do_bidi = true; + end +end, 160); + +function enable_bidi(event) + local session = event.session; + if session.do_bidi and not ( session.is_bidi or session.bidi_session ) then + session.do_bidi = nil; + new_bidi(session); + end +end + +module:hook("s2sin-established", enable_bidi); +module:hook("s2sout-established", enable_bidi); + +function disable_bidi(event) + local session = event.session; + if session.bidi_session then + local bidi_session = session.bidi_session; + session.bidi_session, bidi_session.orig_session = nil, nil; + session.log("debug", "Tearing down bidirectional stream"); + s2smanager.destroy_session(bidi_session, event.reason); + elseif session.orig_session then + local orig_session = session.orig_session; + orig_session.bidi_session, session.orig_session = nil, nil; + orig_session.log("debug", "Tearing down bidirectional stream"); + s2smanager.destroy_session(orig_session, event.reason); + end +end + +module:hook("s2sin-destroyed", disable_bidi); +module:hook("s2sout-destroyed", disable_bidi); + diff -r f88381a39c56 -r a9dfa7232d88 mod_carbons/mod_carbons.lua --- a/mod_carbons/mod_carbons.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_carbons/mod_carbons.lua Tue Mar 12 12:10:25 2013 +0000 @@ -7,6 +7,7 @@ local jid_bare = require "util.jid".bare; local xmlns_carbons = "urn:xmpp:carbons:2"; local xmlns_carbons_old = "urn:xmpp:carbons:1"; +local xmlns_carbons_really_old = "urn:xmpp:carbons:0"; local xmlns_forward = "urn:xmpp:forward:0"; local full_sessions, bare_sessions = full_sessions, bare_sessions; @@ -27,6 +28,19 @@ module:hook("iq/self/"..xmlns_carbons_old..":disable", toggle_carbons); module:hook("iq/self/"..xmlns_carbons_old..":enable", toggle_carbons); +-- COMPAT :( +if module:get_option_boolean("carbons_v0") then + module:hook("iq/self/"..xmlns_carbons_really_old..":carbons", function(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "set" then + local state = stanza.tags[1].attr.mode; + origin.want_carbons = state == "enable" and xmlns_carbons_really_old; + origin.send(st.reply(stanza)); + return true; + end + end); +end + local function message_handler(event, c2s) local origin, stanza = event.origin, event.stanza; local orig_type = stanza.attr.type; @@ -76,7 +90,7 @@ local copy = st.clone(stanza); copy.attr.xmlns = "jabber:client"; local carbon = st.message{ from = bare_jid, type = orig_type, } - :tag(c2s and "sent" or "received", { xmlns = xmlns_carbons }):up() + :tag(c2s and "sent" or "received", { xmlns = xmlns_carbons }) :tag("forwarded", { xmlns = xmlns_forward }) :add_child(copy):reset(); @@ -86,6 +100,10 @@ :tag("forwarded", { xmlns = xmlns_forward }) :add_child(copy):reset(); + -- COMPAT + local carbon_really_old = st.clone(stanza) + :tag(c2s and "sent" or "received", { xmlns = xmlns_carbons_really_old }):up() + user_sessions = user_sessions and user_sessions.sessions; for _, session in pairs(user_sessions) do -- Carbons are sent to resources that have enabled it @@ -93,10 +111,14 @@ -- but not the resource that sent the message, or the one that it's directed to and session ~= target_session -- and isn't among the top resources that would receive the message per standard routing rules - and (c2s or session.priority ~= top_priority) then + and (c2s or session.priority ~= top_priority) + -- don't send v0 carbons (or copies) for c2s + and (not c2s or session.want_carbons ~= xmlns_carbons_really_old) then carbon.attr.to = session.full_jid; module:log("debug", "Sending carbon to %s", session.full_jid); - local carbon = session.want_carbons == xmlns_carbons_old and carbon_old or carbon; -- COMPAT + local carbon = session.want_carbons == xmlns_carbons_old and carbon_old -- COMPAT + or session.want_carbons == xmlns_carbons_really_old and carbon_really_old -- COMPAT + or carbon; session.send(carbon); end end @@ -107,6 +129,7 @@ end -- Stanzas sent by local clients +module:hook("pre-message/host", c2s_message_handler, 1); module:hook("pre-message/bare", c2s_message_handler, 1); module:hook("pre-message/full", c2s_message_handler, 1); -- Stanzas to local clients @@ -115,3 +138,6 @@ module:add_feature(xmlns_carbons); module:add_feature(xmlns_carbons_old); +if module:get_option_boolean("carbons_v0") then + module:add_feature(xmlns_carbons_really_old); +end diff -r f88381a39c56 -r a9dfa7232d88 mod_carbons_adhoc/mod_carbons_adhoc.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_carbons_adhoc/mod_carbons_adhoc.lua Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,42 @@ +-- Implement a Adhoc command which will show a user +-- the status of carbons generation in regard to his clients +-- +-- Copyright (C) 2012 Michael Holzt +-- +-- This file is MIT/X11 licensed. + +local st = require "util.stanza"; +local jid_bare = require "util.jid".bare; +local adhoc_new = module:require "adhoc".new; +local xmlns_carbons_v2 = "urn:xmpp:carbons:2"; +local xmlns_carbons_v1 = "urn:xmpp:carbons:1"; +local xmlns_carbons_v0 = "urn:xmpp:carbons:0"; + +local bare_sessions = bare_sessions; + +local function adhoc_status(self, data, state) + local result; + + local bare_jid = jid_bare(data.from); + local user_sessions = bare_sessions[bare_jid]; + + local result = ""; + + user_sessions = user_sessions and user_sessions.sessions; + for _, session in pairs(user_sessions) do + if session.full_jid then + result = result .. session.full_jid .. ": " .. + ( (session.want_carbons == xmlns_carbons_v2 and "v2" ) or + (session.want_carbons == xmlns_carbons_v1 and "v1" ) or + (session.want_carbons == xmlns_carbons_v0 and "v0" ) or + "none" ) .. "\n"; + end + end + + return { info = result, status = "completed" }; +end + +local status_desc = adhoc_new("Carbons: Get Status", + "mod_carbons_adhoc#status", adhoc_status); + +module:add_item("adhoc", status_desc); diff -r f88381a39c56 -r a9dfa7232d88 mod_carbons_copies/mod_carbons_copies.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_carbons_copies/mod_carbons_copies.lua Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,63 @@ +-- Send carbons v0 style copies of incoming messages to clients which +-- are not (yet) capable of Message Carbons (XEP-0280). +-- +-- This extension integrates with the mod_carbons plugin in such a way +-- that a client capable of Message Carbons will not get a v0 copy. +-- +-- This extension can be enabled for all users by default by setting +-- carbons_copies_default = true. +-- +-- Alternatively or additionally setting carbons_copies_adhoc = true +-- will allow the user to enable or disable copies through Adhoc +-- commands. +-- +-- Copyright (C) 2012 Michael Holzt +-- +-- This file is MIT/X11 licensed. + +local jid_split = require "util.jid".split; +local dm_load = require "util.datamanager".load; +local dm_store = require "util.datamanager".store; +local adhoc_new = module:require "adhoc".new; +local xmlns_carbons_v0 = "urn:xmpp:carbons:0"; +local storename = "mod_carbons_copies"; + +local function toggle_copies(data, on) + local username, hostname, resource = jid_split(data.from); + dm_store(username, hostname, storename, { enabled = on }); +end + +local function adhoc_enable_copies(self, data, state) + toggle_copies(data, true); + return { info = "Copies are enabled for you now.\nPlease restart/reconnect clients.", status = "completed" }; +end + +local function adhoc_disable_copies(self, data, state) + toggle_copies(data, false); + return { info = "Copies are disabled for you now.\nPlease restart/reconnect clients.", status = "completed" }; +end + +module:hook("resource-bind", function(event) + local session = event.session; + local username, hostname, resource = jid_split(session.full_jid); + + local store = dm_load(username, hostname, storename) or + { enabled = + module:get_option_boolean("carbons_copies_default") }; + + if store.enabled then + session.want_carbons = xmlns_carbons_v0; + module:log("debug", "%s enabling copies", session.full_jid); + end +end); + +-- Adhoc-Support +if module:get_option_boolean("carbons_copies_adhoc") then + local enable_desc = adhoc_new("Carbons: Enable Copies", + "mod_carbons_copies#enable", adhoc_enable_copies); + local disable_desc = adhoc_new("Carbons: Disable Copies", + "mod_carbons_copies#disable", adhoc_disable_copies); + + module:add_item("adhoc", enable_desc); + module:add_item("adhoc", disable_desc); +end diff -r f88381a39c56 -r a9dfa7232d88 mod_http_dir_listing/http_dir_listing/mod_http_dir_listing.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_http_dir_listing/http_dir_listing/mod_http_dir_listing.lua Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,58 @@ +-- Prosody IM +-- Copyright (C) 2012 Kim Alvefur +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +module:set_global(); +local server = require"net.http.server"; +local lfs = require "lfs"; +local stat = lfs.attributes; +local build_path = require"socket.url".build_path; +local base64_encode = require"util.encodings".base64.encode; +local tag = require"util.stanza".stanza; +local template = require"util.template"; + +local function get_resource(resource) + local fh = assert(module:load_resource(resource)); + local data = fh:read"*a"; + fh:close(); + return data; +end + +local dir_index_template = template(get_resource("resources/template.html")); +local style = get_resource("resources/style.css"):gsub("url%((.-)%)", function(url) + --module:log("debug", "Inlineing %s", url); + return "url(data:image/png;base64,"..base64_encode(get_resource("resources/"..url))..")"; +end); + +local function generate_directory_index(path, full_path) + local filelist = tag("ul", { class = "filelist" } ):text"\n"; + if path ~= "/" then + filelist:tag("li", { class = "parent directory" }) + :tag("a", { href = "..", rel = "up" }):text("Parent Directory"):up():up():text"\n" + end + for file in lfs.dir(full_path) do + if file:sub(1,1) ~= "." then + local attr = stat(full_path..file) or {}; + local path = { file }; + path.is_directory = attr.mode == "directory"; + filelist:tag("li", { class = attr.mode }) + :tag("a", { href = build_path(path) }):text(file):up() + :up():text"\n"; + end + end + return "\n"..tostring(dir_index_template.apply{ + path = path, + style = style, + filelist = filelist, + footer = "Prosody "..prosody.version, + }); +end + +module:hook_object_event(server, "directory-index", function (event) + local ok, data = pcall(generate_directory_index, event.path, event.full_path); + if ok then return data end + module:log("warn", data); +end); diff -r f88381a39c56 -r a9dfa7232d88 mod_http_dir_listing/http_dir_listing/resources/folder.png Binary file mod_http_dir_listing/http_dir_listing/resources/folder.png has changed diff -r f88381a39c56 -r a9dfa7232d88 mod_http_dir_listing/http_dir_listing/resources/style.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_http_dir_listing/http_dir_listing/resources/style.css Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,10 @@ + +body{background-color:#eeeeec;font-family:sans-serif;} +h1{font-size:xx-large;} +a:link,a:visited{color:#2e3436;text-decoration:none;} +a:link:hover,a:visited:hover{color:#3465a4;} +.filelist{background-color:white;padding:1em;list-style-position:inside;-moz-column-width:20em;-webkit-column-width:20em;-ms-column-width:20em;column-width:20em;} +.file{list-style-image:url(text-x-generic.png);} +.directory{list-style-image:url(folder.png);} +.parent{list-style-image:url(user-home.png);} +footer{margin-top:1ex;font-size:smaller;color:#babdb6;} diff -r f88381a39c56 -r a9dfa7232d88 mod_http_dir_listing/http_dir_listing/resources/template.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_http_dir_listing/http_dir_listing/resources/template.html Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,14 @@ + + + Index of {path} + + + + +

Index of {path}

+ + {filelist} + + + + diff -r f88381a39c56 -r a9dfa7232d88 mod_http_dir_listing/http_dir_listing/resources/text-x-generic.png Binary file mod_http_dir_listing/http_dir_listing/resources/text-x-generic.png has changed diff -r f88381a39c56 -r a9dfa7232d88 mod_http_dir_listing/http_dir_listing/resources/user-home.png Binary file mod_http_dir_listing/http_dir_listing/resources/user-home.png has changed diff -r f88381a39c56 -r a9dfa7232d88 mod_incidents_handling/incidents_handling/incidents_handling.lib.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_incidents_handling/incidents_handling/incidents_handling.lib.lua Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,405 @@ +-- This contains the auxiliary functions for the Incidents Handling module. +-- (C) 2012-2013, Marco Cirillo (LW.Org) + +local pairs, ipairs, os_date, string, table, tonumber = pairs, ipairs, os.date, string, table, tonumber + +local dataforms_new = require "util.dataforms".new +local st = require "util.stanza" + +local xmlns_inc = "urn:xmpp:incident:2" +local xmlns_iodef = "urn:ietf:params:xml:ns:iodef-1.0" +local my_host = nil + +-- // Util and Functions // + +local function ft_str() + local d = os_date("%FT%T%z"):gsub("^(.*)(%+%d+)", function(dt, z) + if z == "+0000" then return dt.."Z" else return dt..z end + end) + return d +end + +local function get_incident_layout(i_type) + local layout = { + title = (i_type == "report" and "Incident report form") or (i_type == "request" and "Request for assistance with incident form"), + instructions = "Started/Ended Time, Contacts, Sources and Targets of the attack are mandatory. See RFC 5070 for further format instructions.", + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/commands" }, + + { name = "name", type = "hidden", value = my_host }, + { name = "entity", type ="text-single", label = "Remote entity to query" }, + { name = "started", type = "text-single", label = "Incident Start Time" }, + { name = "ended", type = "text-single", label = "Incident Ended Time" }, + { name = "reported", type = "hidden", value = ft_str() }, + { name = "description", type = "text-single", label = "Description", + desc = "Description syntax is: " }, + { name = "contacts", type = "text-multi", label = "Contacts", + desc = "Contacts entries format is:
- separated by new lines" }, + { name = "related", type = "text-multi", label = "Related Incidents", + desc = "Related incidents entries format is: - separated by new lines" }, + { name = "impact", type = "text-single", label = "Impact Assessment", + desc = "Impact assessment format is: " }, + { name = "sources", type = "text-multi", label = "Attack Sources", + desc = "Attack sources format is:
" }, + { name = "targets", type = "text-multi", label = "Attack Targets", + desc = "Attack target format is:
" } + } + + if i_type == "request" then + table.insert(layout, { + name = "expectation", + type = "list-single", + label = "Expected action from remote entity", + value = { + { value = "nothing", label = "No action" }, + { value = "contact-sender", label = "Contact us, regarding the incident" }, + { value = "investigate", label = "Investigate the entities listed into the incident" }, + { value = "block-host", label = "Block the involved accounts" }, + { value = "other", label = "Other action, filling the description field is required" } + }}) + table.insert(layout, { name = "description", type = "text-single", label = "Description" }) + end + + return dataforms_new(layout) +end + +local function render_list(incidents) + local layout = { + title = "Stored Incidents List", + instructions = "You can select and view incident reports here, if a followup/response is possible it'll be noted in the step after selection.", + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/commands" }, + { + name = "ids", + type = "list-single", + label = "Stored Incidents", + value = {} + } + } + + -- Render stored incidents list + + for id in pairs(incidents) do + table.insert(layout[2].value, { value = id, label = id }) + end + + return dataforms_new(layout) +end + +local function insert_fixed(t, item) table.insert(t, { type = "fixed", value = item }) end + +local function render_single(incident) + local layout = { + title = string.format("Incident ID: %s - Friendly Name: %s", incident.data.id.text, incident.data.id.name), + instructions = incident.data.desc.text, + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/commands" } + } + + insert_fixed(layout, "Start Time: "..incident.data.start_time) + insert_fixed(layout, "End Time: "..incident.data.end_time) + insert_fixed(layout, "Report Time: "..incident.data.report_time) + + insert_fixed(layout, "Contacts --") + for _, contact in ipairs(incident.data.contacts) do + insert_fixed(layout, string.format("Role: %s Type: %s", contact.role, contact.type)) + if contact.jid then insert_fixed(layout, "--> JID: "..contact.jid..(contact.xmlns and ", XMLNS: "..contact.xmlns or "")) end + if contact.email then insert_fixed(layout, "--> E-Mail: "..contact.email) end + if contact.telephone then insert_fixed(layout, "--> Telephone: "..contact.telephone) end + if contact.postaladdr then insert_fixed(layout, "--> Postal Address: "..contact.postaladdr) end + end + + insert_fixed(layout, "Related Activity --") + for _, related in ipairs(incident.data.related) do + insert_fixed(layout, string.format("Name: %s ID: %s", related.name, related.text)) + end + + insert_fixed(layout, "Assessment --") + insert_fixed(layout, string.format("Language: %s Severity: %s Completion: %s Type: %s", + incident.data.assessment.lang, incident.data.assessment.severity, incident.data.assessment.completion, incident.data.assessment.type)) + + insert_fixed(layout, "Sources --") + for _, source in ipairs(incident.data.event_data.sources) do + insert_fixed(layout, string.format("Address: %s Counter: %s", source.address.text, source.counter.value)) + end + + insert_fixed(layout, "Targets --") + for _, target in ipairs(incident.data.event_data.targets) do + insert_fixed(layout, string.format("For NodeRole: %s", (target.noderole.cat == "ext-category" and target.noderole.ext) or targets.noderole.cat)) + for _, address in ipairs(target.addresses) do + insert_fixed(layout, string.format("---> Address: %s Type: %s", address.text, (address.cat == "ext-category" and address.ext) or address.cat)) + end + end + + if incident.data.expectation then + insert_fixed(layout, "Expected Action: "..incident.data.expectation.action) + if incident.data.expectation.desc then + insert_fixed(layout, "Expected Action Description: "..incident.data.expectation.desc) + end + end + + if incident.type == "request" and incident.status == "open" then + table.insert(layout, { name = "response-datetime", type = "hidden", value = ft_str() }) + table.insert(layout, { name = "response", type = "text-single", label = "Respond to the request" }) + end + + return dataforms_new(layout) +end + +local function get_type(var, typ) + if typ == "counter" then + local count_type, count_ext = var, nil + if count_type ~= "byte" or count_type ~= "packet" or count_type ~= "flow" or count_type ~= "session" or + count_type ~= "alert" or count_type ~= "message" or count_type ~= "event" or count_type ~= "host" or + count_type ~= "site" or count_type ~= "organization" then + count_ext = count_type + count_type = "ext-type" + end + return count_type, count_ext + elseif typ == "category" then + local cat, cat_ext = var, nil + if cat ~= "asn" or cat ~= "atm" or cat ~= "e-mail" or cat ~= "ipv4-addr" or + cat ~= "ipv4-net" or cat ~= "ipv4-net-mask" or cat ~= "ipv6-addr" or cat ~= "ipv6-net" or + cat ~= "ipv6-net-mask" or cat ~= "mac" then + cat_ext = cat + cat = "ext-category" + end + return cat, cat_ext + elseif type == "noderole" then + local noderole_ext = nil + if cat ~= "client" or cat ~= "server-internal" or cat ~= "server-public" or cat ~= "www" or + cat ~= "mail" or cat ~= "messaging" or cat ~= "streaming" or cat ~= "voice" or + cat ~= "file" or cat ~= "ftp" or cat ~= "p2p" or cat ~= "name" or + cat ~= "directory" or cat ~= "credential" or cat ~= "print" or cat ~= "application" or + cat ~= "database" or cat ~= "infra" or cat ~= "log" then + noderole_ext = true + end + return noderole_ext + end +end + +local function do_tag_mapping(tag, object) + if tag.name == "IncidentID" then + object.id = { text = tag:get_text(), name = tag.attr.name } + elseif tag.name == "StartTime" then + object.start_time = tag:get_text() + elseif tag.name == "EndTime" then + object.end_time = tag:get_text() + elseif tag.name == "ReportTime" then + object.report_time = tag:get_text() + elseif tag.name == "Description" then + object.desc = { text = tag:get_text(), lang = tag.attr["xml:lang"] } + elseif tag.name == "Contact" then + local jid = tag:get_child("AdditionalData").tags[1] + local email = tag:get_child("Email") + local telephone = tag:get_child("Telephone") + local postaladdr = tag:get_child("PostalAddress") + if not object.contacts then + object.contacts = {} + object.contacts[1] = { + role = tag.attr.role, + ext_role = (tag.attr["ext-role"] and true) or nil, + type = tag.attr.type, + ext_type = (tag.attr["ext-type"] and true) or nil, + xmlns = jid.attr.xmlns, + jid = jid:get_text(), + email = email, + telephone = telephone, + postaladdr = postaladdr + } + else + object.contacts[#object.contacts + 1] = { + role = tag.attr.role, + ext_role = (tag.attr["ext-role"] and true) or nil, + type = tag.attr.type, + ext_type = (tag.attr["ext-type"] and true) or nil, + xmlns = jid.attr.xmlns, + jid = jid:get_text(), + email = email, + telephone = telephone, + postaladdr = postaladdr + } + end + elseif tag.name == "RelatedActivity" then + object.related = {} + for _, t in ipairs(tag.tags) do + if tag.name == "IncidentID" then + object.related[#object.related + 1] = { text = t:get_text(), name = tag.attr.name } + end + end + elseif tag.name == "Assessment" then + local impact = tag:get_child("Impact") + object.assessment = { lang = impact.attr.lang, severity = impact.attr.severity, completion = impact.attr.completion, type = impact.attr.type } + elseif tag.name == "EventData" then + local source = tag:get_child("Flow").tags[1] + local target = tag:get_child("Flow").tags[2] + local expectation = tag:get_child("Flow").tags[3] + object.event_data = { sources = {}, targets = {} } + for _, t in ipairs(source.tags) do + local addr = t:get_child("Address") + local cntr = t:get_child("Counter") + object.event_data.sources[#object.event_data.sources + 1] = { + address = { cat = addr.attr.category, ext = addr.attr["ext-category"], text = addr:get_text() }, + counter = { type = cntr.attr.type, ext_type = cntr.attr["ext-type"], value = cntr:get_text() } + } + end + for _, entry in ipairs(target.tags) do + local noderole = { cat = entry:get_child("NodeRole").attr.category, ext = entry:get_child("NodeRole").attr["ext-category"] } + local current = #object.event_data.targets + 1 + object.event_data.targets[current] = { addresses = {}, noderole = noderole } + for _, tag in ipairs(entry.tags) do + object.event_data.targets[current].addresses[#object.event_data.targets[current].addresses + 1] = { text = tag:get_text(), cat = tag.attr.category, ext = tag.attr["ext-category"] } + end + end + if expectation then + object.event_data.expectation = { + action = expectation.attr.action, + desc = expectation:get_child("Description") and expectation:get_child("Description"):get_text() + } + end + elseif tag.name == "History" then + object.history = {} + for _, t in ipairs(tag.tags) do + object.history[#object.history + 1] = { + action = t.attr.action, + date = t:get_child("DateTime"):get_text(), + desc = t:get_chilld("Description"):get_text() + } + end + end +end + +local function stanza_parser(stanza) + local object = {} + + if stanza:get_child("report", xmlns_inc) then + local report = st.clone(stanza):get_child("report", xmlns_inc):get_child("Incident", xmlns_iodef) + for _, tag in ipairs(report.tags) do do_tag_mapping(tag, object) end + elseif stanza:get_child("request", xmlns_inc) then + local request = st.clone(stanza):get_child("request", xmlns_inc):get_child("Incident", xmlns_iodef) + for _, tag in ipairs(request.tags) do do_tag_mapping(tag, object) end + elseif stanza:get_child("response", xmlns_inc) then + local response = st.clone(stanza):get_child("response", xmlns_inc):get_child("Incident", xmlns_iodef) + for _, tag in ipairs(response.tags) do do_tag_mapping(tag, object) end + end + + return object +end + +local function stanza_construct(id) + if not id then return nil + else + local object = incidents[id].data + local s_type = incidents[id].type + local stanza = st.iq():tag(s_type or "report", { xmlns = xmlns_inc }) + stanza:tag("Incident", { xmlns = xmlns_iodef, purpose = incidents[id].purpose }) + :tag("IncidentID", { name = object.id.name }):text(object.id.text):up() + :tag("StartTime"):text(object.start_time):up() + :tag("EndTime"):text(object.end_time):up() + :tag("ReportTime"):text(object.report_time):up() + :tag("Description", { ["xml:lang"] = object.desc.lang }):text(object.desc.text):up():up(); + + local incident = stanza:get_child(s_type, xmlns_inc):get_child("Incident", xmlns_iodef) + + for _, contact in ipairs(object.contacts) do + incident:tag("Contact", { role = (contact.ext_role and "ext-role") or contact.role, + ["ext-role"] = (contact.ext_role and contact.role) or nil, + type = (contact.ext_type and "ext-type") or contact.type, + ["ext-type"] = (contact.ext_type and contact.type) or nil }) + :tag("Email"):text(contact.email):up() + :tag("Telephone"):text(contact.telephone):up() + :tag("PostalAddress"):text(contact.postaladdr):up() + :tag("AdditionalData") + :tag("jid", { xmlns = contact.xmlns }):text(contact.jid):up():up():up() + + end + + incident:tag("RelatedActivity"):up(); + + for _, related in ipairs(object.related) do + incident:get_child("RelatedActivity") + :tag("IncidentID", { name = related.name }):text(related.text):up(); + end + + incident:tag("Assessment") + :tag("Impact", { + lang = object.assessment.lang, + severity = object.assessment.severity, + completion = object.assessment.completion, + type = object.assessment.type + }):up():up(); + + incident:tag("EventData") + :tag("Flow") + :tag("System", { category = "source" }):up() + :tag("System", { category = "target" }):up():up():up(); + + local e_data = incident:get_child("EventData") + + local sources = e_data:get_child("Flow").tags[1] + local targets = e_data:get_child("Flow").tags[2] + + for _, source in ipairs(object.event_data.sources) do + sources:tag("Node") + :tag("Address", { category = source.address.cat, ["ext-category"] = source.address.ext }) + :text(source.address.text):up() + :tag("Counter", { type = source.counter.type, ["ext-type"] = source.counter.ext_type }) + :text(source.counter.value):up():up(); + end + + for _, target in ipairs(object.event_data.targets) do + targets:tag("Node"):up() ; local node = targets.tags[#targets.tags] + for _, address in ipairs(target.addresses) do + node:tag("Address", { category = address.cat, ["ext-category"] = address.ext }):text(address.text):up(); + end + node:tag("NodeRole", { category = target.noderole.cat, ["ext-category"] = target.noderole.ext }):up(); + end + + if object.event_data.expectation then + e_data:tag("Expectation", { action = object.event_data.expectation.action }):up(); + if object.event_data.expectation.desc then + local expectation = e_data:get_child("Expectation") + expectation:tag("Description"):text(object.event_data.expectation.desc):up(); + end + end + + if object.history then + local history = incident:tag("History"):up(); + + for _, item in ipairs(object.history) do + history:tag("HistoryItem", { action = item.action }) + :tag("DateTime"):text(item.date):up() + :tag("Description"):text(item.desc):up():up(); + end + end + + -- Sanitize contact empty tags + for _, tag in ipairs(incident) do + if tag.name == "Contact" then + for i, check in ipairs(tag) do + if (check.name == "Email" or check.name == "PostalAddress" or check.name == "Telephone") and + not check:get_text() then + table.remove(tag, i) + end + end + end + end + + if s_type == "request" then stanza.attr.type = "get" + elseif s_type == "response" then stanza.attr.type = "set" + else stanza.attr.type = "set" end + + return stanza + end +end + + +_M = {} -- wraps methods into the library. +_M.ft_str = ft_str +_M.get_incident_layout = get_incident_layout +_M.render_list = render_list +_M.render_single = render_single +_M.get_type = get_type +_M.stanza_parser = stanza_parser +_M.stanza_construct = stanza_construct +_M.set_my_host = function(host) my_host = host end + +return _M + diff -r f88381a39c56 -r a9dfa7232d88 mod_incidents_handling/incidents_handling/mod_incidents_handling.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_incidents_handling/incidents_handling/mod_incidents_handling.lua Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,352 @@ +-- This plugin implements XEP-268 (Incidents Handling) +-- (C) 2012-2013, Marco Cirillo (LW.Org) + +-- Note: Only part of the IODEF specifications are supported. + +module:depends("adhoc") + +local datamanager = require "util.datamanager" +local dataforms_new = require "util.dataforms".new +local st = require "util.stanza" +local id_gen = require "util.uuid".generate + +local pairs, os_time = pairs, os.time + +local xmlns_inc = "urn:xmpp:incident:2" +local xmlns_iodef = "urn:ietf:params:xml:ns:iodef-1.0" + +local my_host = module:get_host() +local ih_lib = module:require("incidents_handling") +ih_lib.set_my_host(my_host) +incidents = {} + +local expire_time = module:get_option_number("incidents_expire_time", 0) + +-- Incidents Table Methods + +local _inc_mt = {} ; _inc_mt.__index = _inc_mt + +function _inc_mt:init() + self:clean() ; self:save() +end + +function _inc_mt:clean() + if expire_time > 0 then + for id, incident in pairs(self) do + if ((os_time() - incident.time) > expire_time) and incident.status ~= "open" then + incident = nil + end + end + end +end + +function _inc_mt:save() + if not datamanager.store("incidents", my_host, "incidents_store", incidents) then + module:log("error", "Failed to save the incidents store!") + end +end + +function _inc_mt:add(stanza, report) + local data = ih_lib.stanza_parser(stanza) + local new_object = { + time = os_time(), + status = (not report and "open") or nil, + data = data + } + + self[data.id.text] = new_object + self:clean() ; self:save() +end + +function _inc_mt:new_object(fields, formtype) + local start_time, end_time, report_time = fields.started, fields.ended, fields.reported + + local _desc, _contacts, _related, _impact, _sources, _targets = fields.description, fields.contacts, fields.related, fields.impact, fields.sources, fields.targets + local fail = false + + local _lang, _dtext = _desc:match("^(%a%a)%s(.*)$") + if not _lang or not _dtext then return false end + local desc = { text = _dtext, lang = _lang } + + local contacts = {} + for contact in _contacts:gmatch("[%w%p]+%s[%w%p]+%s[%w%p]+") do + local address, atype, role = contact:match("^([%w%p]+)%s([%w%p]+)%s([%w%p]+)$") + if not address or not atype or not role then fail = true ; break end + contacts[#contacts + 1] = { + role = role, + ext_role = (role ~= "creator" or role ~= "admin" or role ~= "tech" or role ~= "irt" or role ~= "cc" and true) or nil, + type = atype, + ext_type = (atype ~= "person" or atype ~= "organization" and true) or nil, + jid = (atype == "jid" and address) or nil, + email = (atype == "email" and address) or nil, + telephone = (atype == "telephone" and address) or nil, + postaladdr = (atype == "postaladdr" and address) or nil + } + end + + local related = {} + if _related then + for related in _related:gmatch("[%w%p]+%s[%w%p]+") do + local fqdn, id = related:match("^([%w%p]+)%s([%w%p]+)$") + if fqdn and id then related[#related + 1] = { text = id, name = fqdn } end + end + end + + local _severity, _completion, _type = _impact:match("^([%w%p]+)%s([%w%p]+)%s([%w%p]+)$") + local assessment = { lang = "en", severity = _severity, completion = _completion, type = _type } + + local sources = {} + for source in _sources:gmatch("[%w%p]+%s[%w%p]+%s[%d]+%s[%w%p]+") do + local address, cat, count, count_type = source:match("^([%w%p]+)%s([%w%p]+)%s(%d+)%s([%w%p]+)$") + if not address or not cat or not count or not count_type then fail = true ; break end + local cat, cat_ext = ih_lib.get_type(cat, "category") + local count_type, count_ext = ih_lib.get_type(count_type, "counter") + + sources[#sources + 1] = { + address = { cat = cat, ext = cat_ext, text = address }, + counter = { type = count_type, ext_type = count_ext, value = count } + } + end + + local targets, _preprocess = {}, {} + for target in _targets:gmatch("[%w%p]+%s[%w%p]+%s[%w%p]+") do + local address, cat, noderole, noderole_ext + local address, cat, noderole = target:match("^([%w%p]+)%s([%w%p]+)%s([%w%p]+)$") + if not address or not cat or not noderole then fail = true ; break end + cat, cat_ext = ih_lib.get_type(cat, "category") + noderole_ext = ih_lib.get_type(cat, "noderole") + + if not _preprocess[noderole] then _preprocess[noderole] = { addresses = {}, ext = noderole_ext } end + + _preprocess[noderole].addresses[#_preprocess[noderole].addresses + 1] = { + text = address, cat = cat, ext = cat_ext + } + end + for noderole, data in pairs(_preprocess) do + local nr_cat = (data.ext and "ext-category") or noderole + local nr_ext = (data.ext and noderole) or nil + targets[#targets + 1] = { addresses = data.addresses, noderole = { cat = nr_cat, ext = nr_ext } } + end + + local new_object = {} + if not fail then + new_object["time"] = os_time() + new_object["status"] = (formtype == "request" and "open") or nil + new_object["type"] = formtype + new_object["data"] = { + id = { text = id_gen(), name = my_host }, + start_time = start_time, + end_time = end_time, + report_time = report_time, + desc = desc, + contacts = contacts, + related = related, + assessment = assessment, + event_data = { sources = sources, targets = targets } + } + + self[new_object.data.id.text] = new_object + self:clean() ; self:save() + return new_object.data.id.text + else return false end +end + +-- // Handler Functions // + +local function report_handler(event) + local origin, stanza = event.origin, event.stanza + + incidents:add(stanza, true) + return origin.send(st.reply(stanza)) +end + +local function inquiry_handler(event) + local origin, stanza = event.origin, event.stanza + + local inc_id = stanza:get_child("inquiry", xmlns_inc):get_child("Incident", xmlns_iodef):get_child("IncidentID"):get_text() + if incidents[inc_id] then + module:log("debug", "Server %s queried for incident %s which we know about, sending it", stanza.attr.from, inc_id) + local report_iq = stanza_construct(incidents[inc_id]) + report_iq.attr.from = stanza.attr.to + report_iq.attr.to = stanza.attr.from + report_iq.attr.type = "set" + + origin.send(st.reply(stanza)) + origin.send(report_iq) + return true + else + module:log("error", "Server %s queried for incident %s but we don't know about it", stanza.attr.from, inc_id) + origin.send(st.error_reply(stanza, "cancel", "item-not-found")) ; return true + end +end + +local function request_handler(event) + local origin, stanza = event.origin, event.stanza + + local req_id = stanza:get_child("request", xmlns_inc):get_child("Incident", xmlns_iodef):get_child("IncidentID"):get_text() + if not incidents[req_id] then + origin.send(st.error_reply(stanza, "cancel", "item-not-found")) ; return true + else + origin.send(st.reply(stanza)) ; return true + end +end + +local function response_handler(event) + local origin, stanza = event.origin, event.stanza + + local res_id = stanza:get_child("response", xmlns_inc):get_child("Incident", xmlns_iodef):get_child("IncidentID"):get_text() + if incidents[res_id] then + incidents[res_id] = nil + incidents:add(stanza, true) + origin.send(st.reply(stanza)) ; return true + else + origin.send(st.error_reply(stanza, "cancel", "item-not-found")) ; return true + end +end + +local function results_handler(event) return true end -- TODO results handling + +-- // Adhoc Commands // + +local function list_incidents_command_handler(self, data, state) + local list_incidents_layout = ih_lib.render_list(incidents) + + if state then + if state.step == 1 then + if data.action == "cancel" then + return { status = "canceled" } + elseif data.action == "prev" then + return { status = "executing", actions = { "next", default = "next" }, form = list_incidents_layout }, {} + end + + local single_incident_layout = state.form_layout + local fields = single_incident_layout:data(data.form) + + if fields.response then + incidents[state.id].status = "closed" + + local iq_send = ih_lib.stanza_construct(incidents[state.id]) + module:send(iq_send) + return { status = "completed", info = "Response sent." } + else + return { status = "completed" } + end + else + if data.action == "cancel" then return { status = "canceled" } end + local fields = list_incidents_layout:data(data.form) + + if fields.ids then + local single_incident_layout = ih_lib.render_single(incidents[fields.ids]) + return { status = "executing", actions = { "prev", "complete", default = "complete" }, form = single_incident_layout }, { step = 1, form_layout = single_incident_layout, id = fields.ids } + else + return { status = "completed", error = { message = "You need to select the report ID to continue." } } + end + end + else + return { status = "executing", actions = { "next", default = "next" }, form = list_incidents_layout }, {} + end +end + +local function send_inquiry_command_handler(self, data, state) + local send_inquiry_layout = dataforms_new{ + title = "Send an inquiry about an incident report to a host"; + instructions = "Please specify both the server host and the incident ID."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/commands" }; + { name = "server", type = "text-single", label = "Server to inquiry" }; + { name = "hostname", type = "text-single", label = "Involved incident host" }; + { name = "id", type = "text-single", label = "Incident ID" }; + } + + if state then + if data.action == "cancel" then return { status = "canceled" } end + local fields = send_inquiry_layout:data(data.form) + + if not fields.hostname or not fields.id or not fields.server then + return { status = "completed", error = { message = "You must supply the server to quest, the involved incident host and the incident ID." } } + else + local iq_send = st.iq({ from = my_host, to = fields.server, type = "get" }) + :tag("inquiry", { xmlns = xmlns_inc }) + :tag("Incident", { xmlns = xmlns_iodef, purpose = "traceback" }) + :tag("IncidentID", { name = data.hostname }):text(fields.id):up():up():up() + + module:log("debug", "Sending incident inquiry to %s", fields.server) + module:send(iq_send) + return { status = "completed", info = "Inquiry sent, if an answer can be obtained from the remote server it'll be listed between incidents." } + end + else + return { status = "executing", form = send_inquiry_layout }, "executing" + end +end + +local function rr_command_handler(self, data, state, formtype) + local send_layout = ih_lib.get_incident_layout(formtype) + local err_no_fields = { status = "completed", error = { message = "You need to fill all fields, except the eventual related incident." } } + local err_proc = { status = "completed", error = { message = "There was an error processing your request, check out the syntax" } } + + if state then + if data.action == "cancel" then return { status = "canceled" } end + local fields = send_layout:data(data.form) + + if fields.started and fields.ended and fields.reported and fields.description and fields.contacts and + fields.impact and fields.sources and fields.targets and fields.entity then + if formtype == "request" and not fields.expectation then return err_no_fields end + local id = incidents:new_object(fields, formtype) + if not id then return err_proc end + + local stanza = ih_lib.stanza_construct(id) + stanza.attr.from = my_host + stanza.attr.to = fields.entity + module:log("debug","Sending incident %s stanza to: %s", formtype, stanza.attr.to) + module:send(stanza) + + return { status = "completed", info = string.format("Incident %s sent to %s.", formtype, fields.entity) } + else + return err_no_fields + end + else + return { status = "executing", form = send_layout }, "executing" + end +end + +local function send_report_command_handler(self, data, state) + return rr_command_handler(self, data, state, "report") +end + +local function send_request_command_handler(self, data, state) + return rr_command_handler(self, data, state, "request") +end + +local adhoc_new = module:require "adhoc".new +local list_incidents_descriptor = adhoc_new("List Incidents", xmlns_inc.."#list", list_incidents_command_handler, "admin") +local send_inquiry_descriptor = adhoc_new("Send Incident Inquiry", xmlns_inc.."#send_inquiry", send_inquiry_command_handler, "admin") +local send_report_descriptor = adhoc_new("Send Incident Report", xmlns_inc.."#send_report", send_report_command_handler, "admin") +local send_request_descriptor = adhoc_new("Send Incident Request", xmlns_inc.."#send_request", send_request_command_handler, "admin") +module:provides("adhoc", list_incidents_descriptor) +module:provides("adhoc", send_inquiry_descriptor) +module:provides("adhoc", send_report_descriptor) +module:provides("adhoc", send_request_descriptor) + +-- // Hooks // + +module:hook("iq-set/host/urn:xmpp:incident:2:report", report_handler) +module:hook("iq-get/host/urn:xmpp:incident:2:inquiry", inquiry_handler) +module:hook("iq-get/host/urn:xmpp:incident:2:request", request_handler) +module:hook("iq-set/host/urn:xmpp:incident:2:response", response_handler) +module:hook("iq-result/host/urn:xmpp:incident:2", results_handler) + +-- // Module Methods // + +module.load = function() + if datamanager.load("incidents", my_host, "incidents_store") then incidents = datamanager.load("incidents", my_host, "incidents_store") end + setmetatable(incidents, _inc_mt) ; incidents:init() +end + +module.save = function() + return { incidents = incidents } +end + +module.restore = function(data) + incidents = data.incidents or {} + setmetatable(incidents, _inc_mt) ; incidents:init() +end diff -r f88381a39c56 -r a9dfa7232d88 mod_last_offline/mod_last_offline.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_last_offline/mod_last_offline.lua Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,33 @@ +local datamanager = require "util.datamanager"; +local jid_split = require "util.jid".split; +local time = os.time; +local NULL = {}; +local host = module.host; + +module:hook("resource-unbind", function(event) + local session = event.session; + if session.username then + datamanager.store(session.username, host, "last_online", { + timestamp = time(), + }); + end +end); + +local function offline_stamp(event) + local stanza = event.stanza; + local node, to_host = jid_split(stanza.attr.from); + if to_host == host and event.origin == hosts[host] and stanza.attr.type == "unavailable" then + local timestamp = (datamanager.load(node, host, "last_online") or NULL).timestamp; + if timestamp then + stanza:tag("delay", { + xmlns = "urn:xmpp:delay", + from = host, + stamp = datetime.datetime(timestamp), + }):up(); + end + end +end + +module:hook("pre-presence/bare", offline_stamp); +module:hook("pre-presence/full", offline_stamp); + diff -r f88381a39c56 -r a9dfa7232d88 mod_lib_ldap/README.md --- a/mod_lib_ldap/README.md Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_lib_ldap/README.md Tue Mar 12 12:10:25 2013 +0000 @@ -12,7 +12,7 @@ With that note in mind, you need to set 'allow\_unencrypted\_plain\_auth' to true in your configuration if you want to use LDAP authentication. -To enable LDAP authentication, set 'authentication' to 'ldap' in your configuration file. +To enable LDAP authentication, set 'authentication' to 'ldap2' in your configuration file. See also http://prosody.im/doc/authentication. # LDAP Storage diff -r f88381a39c56 -r a9dfa7232d88 mod_lib_ldap/dev/posix-users.ldif --- a/mod_lib_ldap/dev/posix-users.ldif Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_lib_ldap/dev/posix-users.ldif Tue Mar 12 12:10:25 2013 +0000 @@ -19,6 +19,10 @@ ou: Users objectclass: organizationalUnit +dn: ou=Admins,ou=Users,dc=example,dc=com +ou: Admins +objectclass: organizationalUnit + dn: uid=one,ou=Users,dc=example,dc=com objectclass: posixAccount objectclass: person @@ -212,6 +216,28 @@ K7j+qA4/I0UUNElHxV4s0HW/D81vBcP54IeNWiYZP5ehNeM3NrP5rFImYZ6gUUVDirmsJNKxGsE6 9YZB/wABNTJkcEEfWiigpM6rwgyR6vDLJ/q4zvb6Dmiiioe5dj//2Q== +dn: uid=six,ou=Admins,ou=Users,dc=example,dc=com +objectclass: posixAccount +objectclass: person +uid: six +uidNumber: 1005 +gidNumber: 1005 +sn: Testerson +cn: Admin Testerson +userPassword: 123456 +homeDirectory: /home/six + +dn: uid=seven,ou=Users,dc=example,dc=com +objectclass: posixAccount +objectclass: person +uid: seven +uidNumber: 1006 +gidNumber: 1006 +sn: User +cn: Invalid User +userPassword: 1234567 +homeDirectory: /home/seven + dn: cn=Everyone,ou=Groups,dc=example,dc=com objectclass: posixGroup cn: Everyone diff -r f88381a39c56 -r a9dfa7232d88 mod_lib_ldap/dev/prosody-posix-ldap.cfg.lua --- a/mod_lib_ldap/dev/prosody-posix-ldap.cfg.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_lib_ldap/dev/prosody-posix-ldap.cfg.lua Tue Mar 12 12:10:25 2013 +0000 @@ -1,6 +1,6 @@ -- Use Include 'prosody-posix-ldap.cfg.lua' from prosody.cfg.lua to include this file -authentication = 'ldap' -- Indicate that we want to use LDAP for authentication -storage = 'ldap' -- Indicate that we want to use LDAP for roster/vcard storage +authentication = 'ldap2' -- Indicate that we want to use LDAP for authentication +storage = 'ldap' -- Indicate that we want to use LDAP for roster/vcard storage ldap = { hostname = 'localhost', -- LDAP server location @@ -8,10 +8,10 @@ bind_password = 'prosody', -- Bind password (optional if anonymous bind is supported) user = { - basedn = 'ou=Users,dc=example,dc=com', -- The base DN where user records can be found - filter = 'objectClass=posixAccount', -- Filter expression to find user records under basedn - usernamefield = 'uid', -- The field that contains the user's ID (this will be the username portion of the JID) - namefield = 'cn', -- The field that contains the user's full name (this will be the alias found in the roster) + basedn = 'ou=Users,dc=example,dc=com', -- The base DN where user records can be found + filter = '(&(objectClass=posixAccount)(!(uid=seven)))', -- Filter expression to find user records under basedn + usernamefield = 'uid', -- The field that contains the user's ID (this will be the username portion of the JID) + namefield = 'cn', -- The field that contains the user's full name (this will be the alias found in the roster) }, groups = { diff -r f88381a39c56 -r a9dfa7232d88 mod_lib_ldap/dev/t/00-login.t --- a/mod_lib_ldap/dev/t/00-login.t Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_lib_ldap/dev/t/00-login.t Tue Mar 12 12:10:25 2013 +0000 @@ -11,9 +11,10 @@ 'three', 'four', 'five', + 'six', ); -plan tests => scalar(@users) + 2; +plan tests => scalar(@users) + 3; foreach my $username (@users) { my $conn = TestConnection->new($username); @@ -23,7 +24,7 @@ }); my $error = $conn->cond->recv; - ok(! $error) or diag($error); + ok(! $error) or diag("$username login failed: $error"); } do { @@ -38,7 +39,7 @@ }; do { - my $conn = TestConnection->new('six', password => '12345'); + my $conn = TestConnection->new('invalid', password => '12345'); $conn->reg_cb(session_ready => sub { $conn->cond->send; @@ -47,3 +48,14 @@ my $error = $conn->cond->recv; ok($error); }; + +do { + my $conn = TestConnection->new('seven', password => '1234567'); + + $conn->reg_cb(session_ready => sub { + $conn->cond->send; + }); + + my $error = $conn->cond->recv; + ok($error); +}; diff -r f88381a39c56 -r a9dfa7232d88 mod_lib_ldap/dev/t/TestConnection.pm --- a/mod_lib_ldap/dev/t/TestConnection.pm Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_lib_ldap/dev/t/TestConnection.pm Tue Mar 12 12:10:25 2013 +0000 @@ -14,6 +14,8 @@ three => '34512', four => '45123', five => '51234', + six => '123456', + seven => '1234567', ); sub new { diff -r f88381a39c56 -r a9dfa7232d88 mod_lib_ldap/ldap.lib.lua --- a/mod_lib_ldap/ldap.lib.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_lib_ldap/ldap.lib.lua Tue Mar 12 12:10:25 2013 +0000 @@ -177,7 +177,27 @@ -- XXX consider renaming this...it doesn't bind the current connection function _M.bind(username, password) - local who = format('%s=%s,%s', params.user.usernamefield, username, params.user.basedn); + local conn = _M.getconnection(); + local filter = format('%s=%s', params.user.usernamefield, username); + + if filter then + filter = _M.filter.combine_and(filter, params.user.filter); + end + + local who = _M.singlematch { + attrs = params.user.usernamefield, + base = params.user.basedn, + filter = filter, + }; + + if who then + who = who.dn; + module:log('debug', '_M.bind - who: %s', who); + else + module:log('debug', '_M.bind - no DN found for username = %s', username); + return nil, format('no DN found for username = %s', username); + end + local conn, err = ldap.open_simple(params.hostname, who, password, params.use_tls); if conn then @@ -192,9 +212,10 @@ local ld = _M.getconnection(); query.sizelimit = 1; - query.scope = 'onelevel'; + query.scope = 'subtree'; for dn, attribs in ld:search(query) do + attribs.dn = dn; return attribs; end end diff -r f88381a39c56 -r a9dfa7232d88 mod_mam/mod_mam.lua --- a/mod_mam/mod_mam.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_mam/mod_mam.lua Tue Mar 12 12:10:25 2013 +0000 @@ -23,6 +23,7 @@ local tostring = tostring; local time_now = os.time; local m_min = math.min; +local t_insert = table.insert; local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse; local uuid = require "util.uuid".generate; local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50); @@ -165,9 +166,12 @@ local first, last, index; local n = 0; local start = qset and qset.index or 1; + local results = {}; + -- An empty means: give the last n items. So we loop backwards. + local reverse = qset and qset.before or false; module:log("debug", "Loaded %d items, about to filter", #data); - for i=start,#data do + for i=(reverse and #data or start),(reverse and start or #data),(reverse and -1 or 1) do local item = data[i]; local when, with, resource = item.when, item.with, item.resource; local id = item.id; @@ -195,7 +199,11 @@ local orig_stanza = st.deserialize(item.stanza); orig_stanza.attr.xmlns = "jabber:client"; fwd_st:add_child(orig_stanza); - origin.send(fwd_st); + if reverse then + t_insert(results, 1, fwd_st); + else + results[#results + 1] = fwd_st; + end if not first then index = i; first = id; @@ -219,13 +227,16 @@ break end end + for _,v in pairs(results) do + origin.send(v); + end -- That's all folks! module:log("debug", "Archive query %s completed", tostring(qid)); local reply = st.reply(stanza); if last then -- This is a bit redundant, isn't it? - reply:query(xmlns_mam):add_child(rsm.generate{first = first, last = last, count = n}); + reply:query(xmlns_mam):add_child(rsm.generate{first = (reverse and last or first), last = (reverse and first or last), count = n}); end origin.send(reply); return true diff -r f88381a39c56 -r a9dfa7232d88 mod_register_json/mod_register_json.lua --- a/mod_register_json/mod_register_json.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_register_json/mod_register_json.lua Tue Mar 12 12:10:25 2013 +0000 @@ -69,7 +69,6 @@ return http_response(event, 400, "JSON Decoding failed.") else -- Decode JSON data and check that all bits are there else throw an error - req_body = json_decode(body) if req_body["username"] == nil or req_body["password"] == nil or req_body["host"] == nil or req_body["ip"] == nil then module:log("debug", "%s supplied an insufficent number of elements or wrong elements for the JSON registration", user) return http_response(event, 400, "Invalid syntax.") @@ -86,7 +85,7 @@ -- And nodeprep the username local username = nodeprep(req_body["username"]) if not username then - module:log("debug", "%s supplied an username containing invalid characters: %s", user, username) + module:log("debug", "An username containing invalid characters was supplied: %s", user) return http_response(event, 406, "Supplied username contains invalid characters, see RFC 6122.") else if not usermanager.user_exists(username, req_body["host"]) then diff -r f88381a39c56 -r a9dfa7232d88 mod_s2s_never_encrypt_blacklist/mod_s2s_never_encrypt_blacklist.lua --- a/mod_s2s_never_encrypt_blacklist/mod_s2s_never_encrypt_blacklist.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_s2s_never_encrypt_blacklist/mod_s2s_never_encrypt_blacklist.lua Tue Mar 12 12:10:25 2013 +0000 @@ -1,18 +1,24 @@ -- Filter out servers which gets choppy and buggy when it comes to starttls. -local bad_servers = module:get_option_set("tls_s2s_blacklist") -local bad_servers_ip = module:get_option_set("tls_s2s_blacklist_ip") +local bad_servers = module:get_option_set("tls_s2s_blacklist", {}) +local bad_servers_ip = module:get_option_set("tls_s2s_blacklist_ip", {}) +local libev = module:get_option_boolean("use_libevent") local function disable_tls_for_baddies_in(event) - if bad_servers:contains(event.origin.to_host) or bad_servers_ip:contains(event.origin.conn:ip()) - then event.origin.conn.starttls = nil end + local session = event.origin + if bad_servers:contains(session.from_host) or bad_servers_ip:contains(session.conn:ip()) then + module:log("debug", "disabling tls on incoming stream from %s...", tostring(session.from_host)); + if libev then session.conn.starttls = false; else session.conn.starttls = nil; end + end end local function disable_tls_for_baddies_out(event) - if bad_servers:contains(event.origin.from_host) or bad_servers_ip:contains(event.origin.conn:ip()) - then event.origin.conn.starttls = nil end + local session = event.origin + if bad_servers:contains(session.to_host) then + module:log("debug", "disabling tls on outgoing stream from %s...", tostring(session.to_host)); + if libev then session.conn.starttls = false; else session.conn.starttls = nil; end + end end -module:hook("s2s-stream-features", disable_tls_for_baddies_out, 10) -module:hook("stanza/http://etherx.jabber.org/streams:features", disable_tls_for_baddies_in, 510) - +module:hook("s2s-stream-features", disable_tls_for_baddies_in, 600) +module:hook("stanza/http://etherx.jabber.org/streams:features", disable_tls_for_baddies_out, 600) diff -r f88381a39c56 -r a9dfa7232d88 mod_service_directories/mod_service_directories.lua --- a/mod_service_directories/mod_service_directories.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_service_directories/mod_service_directories.lua Tue Mar 12 12:10:25 2013 +0000 @@ -14,6 +14,7 @@ local adhoc_new = module:require "adhoc".new; local to_ascii = require "util.encodings".idna.to_ascii; local nameprep = require "util.encodings".stringprep.nameprep; +local dataforms_new = require "util.dataforms".new; local pairs, ipairs = pairs, ipairs; local module = module; local hosts = hosts; @@ -79,7 +80,7 @@ -- Admin ad-hoc command to subscribe local function add_contact_handler(self, data, state) - local layout = { + local layout = dataforms_new{ title = "Adding a Server Buddy"; instructions = "Fill out this form to add a \"server buddy\"."; @@ -92,7 +93,7 @@ elseif data.action == "canceled" then return { status = "canceled" }; else - local fields = layout:data(data); + local fields = layout:data(data.form); local peerjid = nameprep(fields.peerjid); if not peerjid or peerjid == "" or #peerjid > 1023 or not to_ascii(peerjid) then return { status = "completed", error = { message = "Invalid JID" } }; diff -r f88381a39c56 -r a9dfa7232d88 mod_smacks/mod_smacks.lua --- a/mod_smacks/mod_smacks.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_smacks/mod_smacks.lua Tue Mar 12 12:10:25 2013 +0000 @@ -83,6 +83,11 @@ queue[#queue+1] = cached_stanza; end + if session.hibernating then + -- The session is hibernating, no point in sending the stanza + -- over a dead connection. It will be delivered upon resumption. + return true; + end local ok, err = _send(stanza); if ok and #queue > max_unacked_stanzas and not session.awaiting_ack and attr and not attr.xmlns then session.awaiting_ack = true; @@ -203,7 +208,7 @@ module:hook("pre-resource-unbind", function (event) local session, err = event.session, event.error; - if session.smacks and err ~= "session closed" then + if session.smacks and err then if not session.resumption_token then local queue = session.outgoing_stanza_queue; if #queue > 0 then @@ -244,6 +249,14 @@ end); module:hook_stanza(xmlns_sm, "resume", function (session, stanza) + if session.full_jid then + session.log("debug", "Tried to resume after resource binding"); + session.send(st.stanza("failed", sm_attr) + :tag("unexpected-request", { xmlns = xmlns_errors }) + ); + return true; + end + local id = stanza.attr.previd; local original_session = session_registry[id]; if not original_session then diff -r f88381a39c56 -r a9dfa7232d88 mod_strict_https/mod_strict_https.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_strict_https/mod_strict_https.lua Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,44 @@ +-- HTTP Strict Transport Security +-- https://tools.ietf.org/html/rfc6797 + +module:set_global(); + +local http_server = require "net.http.server"; + +local hsts_header = module:get_option_string("hsts_header", "max-age=31556952"); -- This means "Don't even try to access without HTTPS for a year" + +local _old_send_response; +local _old_fire_event; + +local modules = {}; + +function module.load() + _old_send_response = http_server.send_response; + function http_server.send_response(response, body) + response.headers.strict_transport_security = hsts_header; + return _old_send_response(response, body); + end + + _old_fire_event = http_server._events.fire_event; + function http_server._events.fire_event(event, payload) + local request = payload.request; + local host = event:match("^[A-Z]+ ([^/]+)"); + local module = modules[host]; + if module and not request.secure then + payload.response.headers.location = module:http_url(request.path); + return 301; + end + return _old_fire_event(event, payload); + end +end +function module.unload() + http_server.send_response = _old_send_response; + http_server._events.fire_event = _old_fire_event; +end +function module.add_host(module) + local http_host = module:get_option_string("http_host", module.host); + modules[http_host] = module; + function module.unload() + modules[http_host] = nil; + end +end diff -r f88381a39c56 -r a9dfa7232d88 mod_uptime_presence/mod_uptime_presence.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_uptime_presence/mod_uptime_presence.lua Tue Mar 12 12:10:25 2013 +0000 @@ -0,0 +1,17 @@ +local st = require"util.stanza"; +local datetime = require"util.datetime"; + +local presence = st.presence({ from = module.host }) + :tag("delay", { xmlns = "urn:xmpp:delay", + stamp = datetime.datetime(prosody.start_time) }); + +module:hook("presence/host", function(event) + local stanza = event.stanza; + if stanza.attr.type == "probe" then + presence.attr.id = stanza.attr.id; + presence.attr.to = stanza.attr.from; + module:send(presence); + return true; + end +end, 10); + diff -r f88381a39c56 -r a9dfa7232d88 mod_vjud/mod_vjud.lua --- a/mod_vjud/mod_vjud.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_vjud/mod_vjud.lua Tue Mar 12 12:10:25 2013 +0000 @@ -6,6 +6,8 @@ local jid_split = require "util.jid".prepped_split; local vcard = module:require "vcard"; local rawget, rawset = rawget, rawset; +local s_lower = string.lower; +local s_find = string.find; local st = require "util.stanza"; local template = require "util.template"; @@ -28,6 +30,8 @@ ]]; +local search_mode = module:get_option_string("vjud_mode", "opt-in"); +local allow_remote = module:get_option_boolean("allow_remote_searches", search_mode ~= "all"); local base_host = module:get_option_string("vjud_search_domain", module:get_host_type() == "component" and module.host:gsub("^[^.]+%.","") @@ -36,23 +40,6 @@ module:depends"disco"; module:add_feature("jabber:iq:search"); -local opted_in; -function module.load() - opted_in = dm_load(nil, module.host, "user_index") or {}; -end -function module.unload() - dm_store(nil, module.host, "user_index", opted_in); -end - -local opt_in_layout = dataforms_new{ - title = "Search settings"; - instructions = "Do you want to appear in search results?"; - { - name = "searchable", - label = "Appear in search results?", - type = "boolean", - }, -}; local vCard_mt = { __index = function(t, k) if type(k) ~= "string" then return nil end @@ -77,21 +64,32 @@ local at_host = "@"..base_host; +local users; -- The user iterator + module:hook("iq/host/jabber:iq:search:query", function(event) local origin, stanza = event.origin, event.stanza; + if not (allow_remote or origin.type == "c2s") then + origin.send(st.error_reply(stanza, "cancel", "not-allowed")) + return true; + end + if stanza.attr.type == "get" then origin.send(st.reply(stanza):add_child(get_reply)); else -- type == "set" local query = stanza.tags[1]; local first, last, nick, email = - (query:get_child_text"first" or false), - (query:get_child_text"last" or false), - (query:get_child_text"nick" or false), - (query:get_child_text"email" or false); + s_lower(query:get_child_text"first" or ""), + s_lower(query:get_child_text"last" or ""), + s_lower(query:get_child_text"nick" or ""), + s_lower(query:get_child_text"email" or ""); + first = #first >= 2 and first; + last = #last >= 2 and last; + nick = #nick >= 2 and nick; + email = #email >= 2 and email; if not ( first or last or nick or email ) then - origin.send(st.error_reply(stanza, "modify", "not-acceptable", "All fields were empty")); + origin.send(st.error_reply(stanza, "modify", "not-acceptable", "All fields were empty or too short")); return true; end @@ -110,13 +108,13 @@ }); end else - for username in pairs(opted_in) do + for username in users() do local vCard = get_user_vcard(username); - if vCard and ( - (vCard.N and vCard.N[2] == first) or - (vCard.N and vCard.N[1] == last) or - (vCard.NICKNAME and vCard.NICKNAME[1] == nick) or - (vCard.EMAIL and vCard.EMAIL[1] == email)) then + if vCard + and ((first and vCard.N and s_find(s_lower(vCard.N[2]), first, nil, true)) + or (last and vCard.N and s_find(s_lower(vCard.N[1]), last, nil, true)) + or (nick and vCard.NICKNAME and s_find(s_lower(vCard.NICKNAME[1]), nick, nil, true)) + or (email and vCard.EMAIL and s_find(s_lower(vCard.EMAIL[1]), email, nil, true))) then reply:add_child(item_template.apply{ jid = username..at_host; first = vCard.N and vCard.N[2] or nil; @@ -132,30 +130,55 @@ return true; end); -local function opt_in_handler(self, data, state) - local username, hostname = jid_split(data.from); - if state then -- the second return value - if data.action == "cancel" then - return { status = "canceled" }; - end - - if not username or not hostname or hostname ~= base_host then - return { status = "error", error = { type = "cancel", - condition = "forbidden", message = "Invalid user or hostname." } }; - end +if search_mode == "all" then + function users() + return usermanager.users(base_host); + end +else -- if "opt-in", default + local opted_in; + function module.load() + opted_in = dm_load(nil, module.host, "user_index") or {}; + end + function module.unload() + dm_store(nil, module.host, "user_index", opted_in); + end + function users() + return pairs(opted_in); + end + local opt_in_layout = dataforms_new{ + title = "Search settings"; + instructions = "Do you want to appear in search results?"; + { + name = "searchable", + label = "Appear in search results?", + type = "boolean", + }, + }; + local function opt_in_handler(self, data, state) + local username, hostname = jid_split(data.from); + if state then -- the second return value + if data.action == "cancel" then + return { status = "canceled" }; + end - local fields = opt_in_layout:data(data.form); - opted_in[username] = fields.searchable or nil + if not username or not hostname or hostname ~= base_host then + return { status = "error", error = { type = "cancel", + condition = "forbidden", message = "Invalid user or hostname." } }; + end - return { status = "completed" } - else -- No state, send the form. - return { status = "executing", actions = { "complete" }, + local fields = opt_in_layout:data(data.form); + opted_in[username] = fields.searchable or nil + + return { status = "completed" } + else -- No state, send the form. + return { status = "executing", actions = { "complete" }, form = { layout = opt_in_layout, values = { searchable = opted_in[username] } } }, true; + end end -end -local adhoc_new = module:require "adhoc".new; -local adhoc_vjudsetup = adhoc_new("Search settings", "vjudsetup", opt_in_handler);--, "self");-- and nil); -module:depends"adhoc"; -module:provides("adhoc", adhoc_vjudsetup); + local adhoc_new = module:require "adhoc".new; + local adhoc_vjudsetup = adhoc_new("Search settings", "vjudsetup", opt_in_handler);--, "self");-- and nil); + module:depends"adhoc"; + module:provides("adhoc", adhoc_vjudsetup); +end diff -r f88381a39c56 -r a9dfa7232d88 mod_websocket/mod_websocket.lua --- a/mod_websocket/mod_websocket.lua Thu Nov 22 18:59:10 2012 +0000 +++ b/mod_websocket/mod_websocket.lua Tue Mar 12 12:10:25 2013 +0000 @@ -1,6 +1,4 @@ -- Prosody IM --- Copyright (C) 2008-2010 Matthew Wild --- Copyright (C) 2008-2010 Waqas Hussain -- Copyright (C) 2012 Florian Zeitz -- -- This project is MIT/X11 licensed. Please see the @@ -9,28 +7,19 @@ module:set_global(); -local add_task = require "util.timer".add_task; -local new_xmpp_stream = require "util.xmppstream".new; -local nameprep = require "util.encodings".stringprep.nameprep; -local sessionmanager = require "core.sessionmanager"; -local st = require "util.stanza"; -local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session; -local uuid_generate = require "util.uuid".generate; +local add_filter = require "util.filters".add_filter; local sha1 = require "util.hashes".sha1; local base64 = require "util.encodings".base64.encode; -local band = require "bit".band; -local bxor = require "bit".bxor; - -local xpcall, tostring, type = xpcall, tostring, type; -local traceback = debug.traceback; +local softreq = require "util.dependencies".softreq; +local portmanager = require "core.portmanager"; -local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; - -local log = module._log; - -local c2s_timeout = module:get_option_number("c2s_timeout"); -local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5); -local opt_keepalives = module:get_option_boolean("tcp_keepalives", false); +local bit; +pcall(function() bit = require"bit"; end); +bit = bit or softreq"bit32" +if not bit then module:log("error", "No bit module found. Either LuaJIT 2, lua-bitop or Lua 5.2 is required"); end +local band = bit.band; +local bxor = bit.bxor; +local rshift = bit.rshift; local cross_domain = module:get_option("cross_domain_websocket"); if cross_domain then @@ -44,11 +33,9 @@ end end -local sessions = module:shared("sessions"); -local core_process_stanza = prosody.core_process_stanza; - -local stream_callbacks = { default_ns = "jabber:client", handlestanza = core_process_stanza }; -local listener = {}; +module:depends("c2s") +local sessions = module:shared("c2s/sessions"); +local c2s_listener = portmanager.get_service("c2s").listener; -- Websocket helpers local function parse_frame(frame) @@ -118,11 +105,11 @@ result = result .. string.char(length); elseif length <= 0xFFFF then -- 2-byte length result = result .. string.char(126); - result = result .. string.char(length/0x100) .. string.char(length%0x100); + result = result .. string.char(rshift(length, 8)) .. string.char(length%0x100); else -- 8-byte length result = result .. string.char(127); for i = 7, 0, -1 do - result = result .. string.char(( length / (2^(8*i)) ) % 0x100); + result = result .. string.char(rshift(length, 8*i) % 0x100); end end @@ -131,198 +118,33 @@ return result; end ---- Stream events handlers -local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; -local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" }; - -function stream_callbacks.streamopened(session, attr) - local send = session.send; - session.host = nameprep(attr.to); - if not session.host then - session:close{ condition = "improper-addressing", - text = "A valid 'to' attribute is required on stream headers" }; - return; - end - session.version = tonumber(attr.version) or 0; - session.streamid = uuid_generate(); - (session.log or session)("debug", "Client sent opening to %s", session.host); +--- Filter stuff +function handle_request(event, path) + local request, response = event.request, event.response; + local conn = response.conn; - if not hosts[session.host] then - -- We don't serve this host... - session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)}; - return; - end - - -- COMPAT: Some current client implementations need this to be self-closing - if session.self_closing_stream then - send(""..tostring(st.stanza("stream:stream", { - xmlns = 'jabber:client', ["xmlns:stream"] = 'http://etherx.jabber.org/streams'; - id = session.streamid, from = session.host, version = '1.0', ["xml:lang"] = 'en' }))); - else - send(""..st.stanza("stream:stream", { - xmlns = 'jabber:client', ["xmlns:stream"] = 'http://etherx.jabber.org/streams'; - id = session.streamid, from = session.host, version = '1.0', ["xml:lang"] = 'en' }):top_tag()); - end - - (session.log or log)("debug", "Sent reply to client"); - session.notopen = nil; - - -- If session.secure is *false* (not nil) then it means we /were/ encrypting - -- since we now have a new stream header, session is secured - if session.secure == false then - session.secure = true; + if not request.headers.sec_websocket_key then + response.headers.content_type = "text/html"; + return [[Websocket +

It works! Now point your WebSocket client to this URL to connect to Prosody.

+ ]]; end - local features = st.stanza("stream:features"); - hosts[session.host].events.fire_event("stream-features", { origin = session, features = features }); - module:fire_event("stream-features", session, features); - - send(features); -end - -function stream_callbacks.streamclosed(session) - session.log("debug", "Received
"); - session:close(false); -end - -function stream_callbacks.error(session, error, data) - if error == "no-stream" then - session.log("debug", "Invalid opening stream header"); - session:close("invalid-namespace"); - elseif error == "parse-error" then - (session.log or log)("debug", "Client XML parse error: %s", tostring(data)); - session:close("not-well-formed"); - elseif error == "stream-error" then - local condition, text = "undefined-condition"; - for child in data:children() do - if child.attr.xmlns == xmlns_xmpp_streams then - if child.name ~= "text" then - condition = child.name; - else - text = child:get_text(); - end - if condition ~= "undefined-condition" and text then - break; - end - end - end - text = condition .. (text and (" ("..text..")") or ""); - session.log("info", "Session closed by remote with error: %s", text); - session:close(nil, text); - end -end - -local function handleerr(err) log("error", "Traceback[c2s]: %s: %s", tostring(err), traceback()); end -function stream_callbacks.handlestanza(session, stanza) - stanza = session.filter("stanzas/in", stanza); - if stanza then - return xpcall(function () return core_process_stanza(session, stanza) end, handleerr); - end -end + local wants_xmpp = false; + (request.headers.sec_websocket_protocol or ""):gsub("([^,]*),?", function (proto) + if proto == "xmpp" then wants_xmpp = true; end + end); ---- Session methods -local function session_close(session, reason) - local log = session.log or log; - if session.conn then - if session.notopen then - -- COMPAT: Some current client implementations need this to be self-closing - if session.self_closing_stream then - session.send(""..tostring(st.stanza("stream:stream", default_stream_attr))); - else - session.send(""..st.stanza("stream:stream", default_stream_attr):top_tag()); - end - end - if reason then -- nil == no err, initiated by us, false == initiated by client - if type(reason) == "string" then -- assume stream error - log("debug", "Disconnecting client, is: %s", reason); - session.send(st.stanza("stream:error"):tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' })); - elseif type(reason) == "table" then - if reason.condition then - local stanza = st.stanza("stream:error"):tag(reason.condition, stream_xmlns_attr):up(); - if reason.text then - stanza:tag("text", stream_xmlns_attr):text(reason.text):up(); - end - if reason.extra then - stanza:add_child(reason.extra); - end - log("debug", "Disconnecting client, is: %s", tostring(stanza)); - session.send(stanza); - elseif reason.name then -- a stanza - log("debug", "Disconnecting client, is: %s", tostring(reason)); - session.send(reason); - end - end - end - session.send("
"); - function session.send() return false; end - - local reason = (reason and (reason.text or reason.condition)) or reason; - session.log("info", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed"); - - -- Authenticated incoming stream may still be sending us stanzas, so wait for from remote - local conn = session.conn; - if reason == nil and not session.notopen and session.type == "c2s" then - -- Grace time to process data from authenticated cleanly-closed stream - add_task(stream_close_timeout, function () - if not session.destroyed then - session.log("warn", "Failed to receive a stream close response, closing connection anyway..."); - sm_destroy_session(session, reason); - conn:close(); - end - end); - else - sm_destroy_session(session, reason); - conn:close(); - end - end -end - -module:hook_global("user-deleted", function(event) - local username, host = event.username, event.host; - local user = hosts[host].sessions[username]; - if user and user.sessions then - for jid, session in pairs(user.sessions) do - session:close{ condition = "not-authorized", text = "Account deleted" }; - end - end -end, 200); - ---- Port listener -function listener.onconnect(conn) - local session = sm_new_session(conn); - sessions[conn] = session; - - session.log("info", "Client connected"); - - -- Client is using legacy SSL (otherwise mod_tls sets this flag) - if conn:ssl() then - session.secure = true; - end - - if opt_keepalives then - conn:setoption("keepalive", opt_keepalives); - end - - session.close = session_close; - - session.conn.starttls = nil; - - local stream = new_xmpp_stream(session, stream_callbacks); - session.stream = stream; - session.notopen = true; - - function session.reset_stream() - session.notopen = true; - session.stream:reset(); + if not wants_xmpp then + return 501; end local function websocket_close(code, message) - local data = string.char(code/0x100) .. string.char(code%0x100) .. message; + local data = string.char(rshift(code, 8)) .. string.char(code%0x100) .. message; conn:write(build_frame({opcode = 0x8, FIN = true, data = data})); conn:close(); end - local filter = session.filter; local dataBuffer; local function handle_frame(frame) module:log("debug", "Websocket received: %s (%i bytes)", frame.data, #frame.data); @@ -365,115 +187,58 @@ dataBuffer = frame.data; elseif frame.opcode == 0x2 then -- Binary frame websocket_close(1003, "Only text frames are supported"); - return false; + return; elseif frame.opcode == 0x8 then -- Close request websocket_close(1000, "Goodbye"); - return false; + return; elseif frame.opcode == 0x9 then -- Ping frame frame.opcode = 0xA; conn:write(build_frame(frame)); - return true; + return ""; else log("warn", "Received frame with unsupported opcode %i", frame.opcode); - return true; + return ""; end if frame.FIN then data = dataBuffer; dataBuffer = nil; - -- COMPAT: Some current client implementations send a self-closing - data, session.self_closing_stream = data:gsub("^($", "%1>"); - session.self_closing_stream = (session.self_closing_stream == 1) - - data = filter("bytes/in", data); - if data then - local ok, err = stream:feed(data); - if ok then return; end - log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_")); - session:close("not-well-formed"); - end + return data; end - return true; + return ""; end + conn:setlistener(c2s_listener); + c2s_listener.onconnect(conn); + local frameBuffer = ""; - function session.data(data) + add_filter(sessions[conn], "bytes/in", function(data) + local cache = ""; frameBuffer = frameBuffer .. data; local frame, length = parse_frame(frameBuffer); while frame do frameBuffer = frameBuffer:sub(length + 1); - if not handle_frame(frame) then return; end + local result = handle_frame(frame); + if not result then return; end + cache = cache .. result; frame, length = parse_frame(frameBuffer); end - end - - function session.send(s) - conn:write(build_frame({ FIN = true, opcode = 0x01, data = tostring(s)})); - end - - if c2s_timeout then - add_task(c2s_timeout, function () - if session.type == "c2s_unauthed" then - session:close("connection-timeout"); - end - end); - end - - session.dispatch_stanza = stream_callbacks.handlestanza; -end + return cache; -function listener.onincoming(conn, data) - local session = sessions[conn]; - if session then - session.data(data); - else - listener.onconnect(conn, data); - session = sessions[conn]; - session.data(data); - end -end - -function listener.ondisconnect(conn, err) - local session = sessions[conn]; - if session then - (session.log or log)("info", "Client disconnected: %s", err or "connection closed"); - sm_destroy_session(session, err); - sessions[conn] = nil; - end -end - -function listener.associate_session(conn, session) - sessions[conn] = session; -end - -function handle_request(event, path) - local request, response = event.request, event.response; - - if not request.headers.sec_websocket_key then - response.headers.content_type = "text/html"; - return [[Websocket -

It works! Now point your WebSocket client to this URL to connect to Prosody.

- ]]; - end - - local wants_xmpp = false; - (request.headers.sec_websocket_protocol or ""):gsub("([^,]*),?", function (proto) - if proto == "xmpp" then wants_xmpp = true; end end); - if not wants_xmpp then - return 501; - end + add_filter(sessions[conn], "bytes/out", function(data) + return build_frame({ FIN = true, opcode = 0x01, data = tostring(data)}); + end); - response.conn:setlistener(listener); response.status = "101 Switching Protocols"; - response.headers.Upgrade = "websocket"; - response.headers.Connection = "Upgrade"; - response.headers.Sec_WebSocket_Accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")); - response.headers.Sec_WebSocket_Protocol = "xmpp"; - response.headers.Access_Control_Allow_Origin = cross_domain; + response.headers.upgrade = "websocket"; + response.headers.connection = "Upgrade"; + response.headers.sec_webSocket_accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")); + response.headers.sec_webSocket_protocol = "xmpp"; + response.headers.access_control_allow_origin = cross_domain; return ""; end @@ -481,7 +246,8 @@ function module.add_host(module) module:depends("http"); module:provides("http", { - name = "xmpp-websocket"; + name = "websocket"; + default_path = "xmpp-websocket"; route = { ["GET"] = handle_request; ["GET /"] = handle_request;