prosody-modules-c53cc1ae4788/0002755000175500017550000000000013164216767015614 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_invite/0002755000175500017550000000000013164216767017751 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_invite/mod_invite.lua0000644000175500017550000001157013163400720022572 0ustar debacledebaclelocal adhoc_new = module:require "adhoc".new; local uuid_new = require "util.uuid".generate; local jid_split = require "util.jid".split; local jid_join = require "util.jid".join; local http_formdecode = require "net.http".formdecode; local usermanager = require "core.usermanager"; local rostermanager = require "core.rostermanager"; local tohtml = require "util.stanza".xml_escape local nodeprep = require "util.encodings".stringprep.nodeprep; local tostring = tostring; local invite_storage = module:open_store(); local inviter_storage = module:open_store("inviter"); local serve = module:depends"http_files".serve; module:depends"adhoc"; module:depends"http"; local function apply_template(template, args) return template:gsub("{{([^}]*)}}", function (k) if args[k] then return tohtml(args[k]) else return k end end) end function generate_page(event) local request, response = event.request, event.response; local tokens = invite_storage:get() or {}; local token = request.path:match("^/invite/([^/]*)$"); response.headers.content_type = "text/html; charset=utf-8"; if not token or not tokens[token] then local template = assert(module:load_resource("invite/invite_result.html")):read("*a"); return apply_template(template, { classes = "alert-danger", message = "This invite has expired." }) end local template = assert(module:load_resource("invite/invite.html")):read("*a"); return apply_template(template, { user = jid_join(tokens[token], module.host), server = module.host, token = token }); end function subscribe(user1, user2) local user1_jid = jid_join(user1, module.host); local user2_jid = jid_join(user2, module.host); rostermanager.set_contact_pending_out(user2, module.host, user1_jid); rostermanager.set_contact_pending_in(user1, module.host, user2_jid); rostermanager.subscribed(user1, module.host, user2_jid); rostermanager.process_inbound_subscription_approval(user2, module.host, user1_jid); end function handle_form(event) local request, response = event.request, event.response; local form_data = http_formdecode(request.body); local user, password, token = form_data["user"], form_data["password"], form_data["token"]; local tokens = invite_storage:get() or {}; local template = assert(module:load_resource("invite/invite_result.html")):read("*a"); response.headers.content_type = "text/html; charset=utf-8"; if not user or #user == 0 or not password or #password == 0 or not token then return apply_template(template, { classes = "alert-warning", message = "Please fill in all fields." }) end if not tokens[token] then return apply_template(template, { classes = "alert-danger", message = "This invite has expired." }) end -- Shamelessly copied from mod_register_web. local prepped_username = nodeprep(user); if not prepped_username or #prepped_username == 0 then return apply_template(template, { classes = "alert-warning", message = "This username contains invalid characters." }) end if usermanager.user_exists(prepped_username, module.host) then return apply_template(template, { classes = "alert-warning", message = "This username is already in use." }) end local registering = { username = prepped_username , host = module.host, allowed = true } module:fire_event("user-registering", registering); if not registering.allowed then return apply_template(template, { classes = "alert-danger", message = "Registration is not allowed." }) end local ok, err = usermanager.create_user(prepped_username, password, module.host); if ok then subscribe(prepped_username, tokens[token]); subscribe(tokens[token], prepped_username); inviter_storage:set(prepped_username, { inviter = tokens[token] }); rostermanager.roster_push(tokens[token], module.host, jid_join(prepped_username, module.host)); tokens[token] = nil; invite_storage:set(nil, tokens); return apply_template(template, { classes = "alert-success", message = "Your account has been created! You can now log in using an XMPP client." }) else module:log("debug", "Registration failed: " .. tostring(err)); return apply_template(template, { classes = "alert-danger", message = "An unknown error has occurred." }) end end module:provides("http", { route = { ["GET /bootstrap.min.css"] = serve(module:get_directory() .. "/invite/bootstrap.min.css"); ["GET /*"] = generate_page; POST = handle_form; }; }); function invite_command_handler(_, data) local uuid = uuid_new(); local user, host = jid_split(data.from); if host ~= module.host then return { status = "completed", error = { message = "You are not allowed to invite users to this server." }}; end local tokens = invite_storage:get() or {}; tokens[uuid] = user; invite_storage:set(nil, tokens); return { info = module:http_url() .. "/" .. uuid, status = "completed" }; end local adhoc_invite = adhoc_new("Invite user", "invite", invite_command_handler, "user") module:add_item("adhoc", adhoc_invite);prosody-modules-c53cc1ae4788/mod_invite/invite/0002755000175500017550000000000013164217014021231 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_invite/invite/invite_result.html0000644000175500017550000000042013163400720025002 0ustar debacledebacle Invite

Account creation

{{message}}
prosody-modules-c53cc1ae4788/mod_invite/invite/invite.html0000644000175500017550000000276613163400720023423 0ustar debacledebacle Invite
Account creation

Already have an XMPP account? Add {{user}} to your contact list.

{{user}} invites you to create an XMPP account on the server {{server}}.

@{{server}}
prosody-modules-c53cc1ae4788/mod_invite/README.markdown0000644000175500017550000000116413163400720022431 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: 'Allows users to invite new users' ... Introduction ============ This module allows users with an account to generate single-use invite URLs using an ad-hoc command. The invite URLs allow new users to create an account even if public registration is disabled. After the account is created, the inviter and the invitee are automatically added to the other's roster. The inviter of a user is stored, so can be used later (for example, for detecting spammers). This module depends on Prosody's internal webserver. Compatibility ============= ----- ------- 0.9 Works ----- ------- prosody-modules-c53cc1ae4788/mod_privacy_lists/0002755000175500017550000000000013164216767021346 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_privacy_lists/README.markdown0000644000175500017550000000112713163400720024025 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: 'Privacy lists (XEP-0016) support' ... Introduction ------------ Privacy lists are a flexible method for blocking communications. Originally known as mod\_privacy and bundled with Prosody, this module is being phased out in favour of the newer simpler blocking (XEP-0191) protocol, implemented in [mod\_blocklist][doc:modules:mod_blocklist]. Configuration ------------- None. Each user can specify their privacy lists using their client (if it supports XEP-0016). Compatibility ------------- ------ ------- 0.9 Works 0.10 Works ------ ------- prosody-modules-c53cc1ae4788/mod_privacy_lists/mod_privacy_lists.lua0000644000175500017550000003664713163400720025600 0ustar debacledebacle-- Prosody IM -- Copyright (C) 2009-2014 Matthew Wild -- Copyright (C) 2009-2014 Waqas Hussain -- Copyright (C) 2009 Thilo Cestonaro -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- module:add_feature("jabber:iq:privacy"); local st = require "util.stanza"; local bare_sessions, full_sessions = prosody.bare_sessions, prosody.full_sessions; local util_Jid = require "util.jid"; local jid_bare = util_Jid.bare; local jid_split, jid_join = util_Jid.split, util_Jid.join; local load_roster = require "core.rostermanager".load_roster; local to_number = tonumber; local privacy_storage = module:open_store("privacy"); local user_sessions = hosts[module.host].sessions; local function get_lists(username) return user_sessions[username].privacy_lists; end local function save_lists(username) local privacy_lists = user_sessions[username].privacy_lists; if privacy_lists.default == nil and next(privacy_lists.lists) == nil then privacy_lists = nil; end return privacy_storage:set(username, privacy_lists); end module:hook("resource-bind", function (event) local username = event.session.username; user_sessions[username].privacy_lists = privacy_storage:get(username) or { lists = {} }; end); function isListUsed(origin, name, privacy_lists) local user = bare_sessions[origin.username.."@"..origin.host]; if user then for resource, session in pairs(user.sessions) do if resource ~= origin.resource then if session.activePrivacyList == name then return true; elseif session.activePrivacyList == nil and privacy_lists.default == name then return true; end end end end end function isAnotherSessionUsingDefaultList(origin) local user = bare_sessions[origin.username.."@"..origin.host]; if user then for resource, session in pairs(user.sessions) do if resource ~= origin.resource and session.activePrivacyList == nil then return true; end end end end function declineList(privacy_lists, origin, stanza, which) if which == "default" then if isAnotherSessionUsingDefaultList(origin) then return { "cancel", "conflict", "Another session is online and using the default list."}; end privacy_lists.default = nil; origin.send(st.reply(stanza)); elseif which == "active" then origin.activePrivacyList = nil; origin.send(st.reply(stanza)); else return {"modify", "bad-request", "Neither default nor active list specifed to decline."}; end return true; end function activateList(privacy_lists, origin, stanza, which, name) local list = privacy_lists.lists[name]; if which == "default" and list then if isAnotherSessionUsingDefaultList(origin) then return {"cancel", "conflict", "Another session is online and using the default list."}; end privacy_lists.default = name; origin.send(st.reply(stanza)); elseif which == "active" and list then origin.activePrivacyList = name; origin.send(st.reply(stanza)); elseif not list then return {"cancel", "item-not-found", "No such list: "..name}; else return {"modify", "bad-request", "No list chosen to be active or default."}; end return true; end function deleteList(privacy_lists, origin, stanza, name) local list = privacy_lists.lists[name]; if list then if isListUsed(origin, name, privacy_lists) then return {"cancel", "conflict", "Another session is online and using the list which should be deleted."}; end if privacy_lists.default == name then privacy_lists.default = nil; end if origin.activePrivacyList == name then origin.activePrivacyList = nil; end privacy_lists.lists[name] = nil; origin.send(st.reply(stanza)); return true; end return {"modify", "bad-request", "Not existing list specifed to be deleted."}; end function createOrReplaceList (privacy_lists, origin, stanza, name, entries) local bare_jid = origin.username.."@"..origin.host; if privacy_lists.lists == nil then privacy_lists.lists = {}; end local list = {}; privacy_lists.lists[name] = list; local orderCheck = {}; list.name = name; list.items = {}; for _,item in ipairs(entries) do if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then return {"modify", "bad-request", "Order attribute not valid."}; end if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then return {"modify", "bad-request", "Type attribute not valid."}; end local tmp = {}; orderCheck[item.attr.order] = true; tmp["type"] = item.attr.type; tmp["value"] = item.attr.value; tmp["action"] = item.attr.action; tmp["order"] = to_number(item.attr.order); tmp["presence-in"] = false; tmp["presence-out"] = false; tmp["message"] = false; tmp["iq"] = false; if #item.tags > 0 then for _,tag in ipairs(item.tags) do tmp[tag.name] = true; end end if tmp.type == "subscription" then if tmp.value ~= "both" and tmp.value ~= "to" and tmp.value ~= "from" and tmp.value ~= "none" then return {"cancel", "bad-request", "Subscription value must be both, to, from or none."}; end end if tmp.action ~= "deny" and tmp.action ~= "allow" then return {"cancel", "bad-request", "Action must be either deny or allow."}; end list.items[#list.items + 1] = tmp; end table.sort(list.items, function(a, b) return a.order < b.order; end); origin.send(st.reply(stanza)); if bare_sessions[bare_jid] ~= nil then local iq = st.iq ( { type = "set", id="push1" } ); iq:tag ("query", { xmlns = "jabber:iq:privacy" } ); iq:tag ("list", { name = list.name } ):up(); iq:up(); for resource, session in pairs(bare_sessions[bare_jid].sessions) do iq.attr.to = bare_jid.."/"..resource session.send(iq); end else return {"cancel", "bad-request", "internal error."}; end return true; end function getList(privacy_lists, origin, stanza, name) local reply = st.reply(stanza); reply:tag("query", {xmlns="jabber:iq:privacy"}); if name == nil then if privacy_lists.lists then if origin.activePrivacyList then reply:tag("active", {name=origin.activePrivacyList}):up(); end if privacy_lists.default then reply:tag("default", {name=privacy_lists.default}):up(); end for name,list in pairs(privacy_lists.lists) do reply:tag("list", {name=name}):up(); end end else local list = privacy_lists.lists[name]; if list then reply = reply:tag("list", {name=list.name}); for _,item in ipairs(list.items) do reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order}); if item["message"] then reply:tag("message"):up(); end if item["iq"] then reply:tag("iq"):up(); end if item["presence-in"] then reply:tag("presence-in"):up(); end if item["presence-out"] then reply:tag("presence-out"):up(); end reply:up(); end else return {"cancel", "item-not-found", "Unknown list specified."}; end end origin.send(reply); return true; end module:hook("iq/bare/jabber:iq:privacy:query", function(data) local origin, stanza = data.origin, data.stanza; if stanza.attr.to == nil then -- only service requests to own bare JID local query = stanza.tags[1]; -- the query element local valid = false; local privacy_lists = get_lists(origin.username); if privacy_lists.lists[1] then -- Code to migrate from old privacy lists format, remove in 0.8 module:log("info", "Upgrading format of stored privacy lists for %s@%s", origin.username, origin.host); local lists = privacy_lists.lists; for idx, list in ipairs(lists) do lists[list.name] = list; lists[idx] = nil; end end if stanza.attr.type == "set" then if #query.tags == 1 then -- the element MUST NOT include more than one child element for _,tag in ipairs(query.tags) do if tag.name == "active" or tag.name == "default" then if tag.attr.name == nil then -- Client declines the use of active / default list valid = declineList(privacy_lists, origin, stanza, tag.name); else -- Client requests change of active / default list valid = activateList(privacy_lists, origin, stanza, tag.name, tag.attr.name); end elseif tag.name == "list" and tag.attr.name then -- Client adds / edits a privacy list if #tag.tags == 0 then -- Client removes a privacy list valid = deleteList(privacy_lists, origin, stanza, tag.attr.name); else -- Client edits a privacy list valid = createOrReplaceList(privacy_lists, origin, stanza, tag.attr.name, tag.tags); end end end end elseif stanza.attr.type == "get" then local name = nil; local listsToRetrieve = 0; if #query.tags >= 1 then for _,tag in ipairs(query.tags) do if tag.name == "list" then -- Client requests a privacy list from server name = tag.attr.name; listsToRetrieve = listsToRetrieve + 1; end end end if listsToRetrieve == 0 or listsToRetrieve == 1 then valid = getList(privacy_lists, origin, stanza, name); end end if valid ~= true then valid = valid or { "cancel", "bad-request", "Couldn't understand request" }; if valid[1] == nil then valid[1] = "cancel"; end if valid[2] == nil then valid[2] = "bad-request"; end origin.send(st.error_reply(stanza, valid[1], valid[2], valid[3])); else save_lists(origin.username); end return true; end end); function checkIfNeedToBeBlocked(e, session) local origin, stanza = e.origin, e.stanza; local user = user_sessions[session.username]; local privacy_lists = user and user.privacy_lists; local bare_jid = session.username.."@"..session.host; local to = stanza.attr.to or bare_jid; local from = stanza.attr.from; local is_to_user = bare_jid == jid_bare(to); local is_from_user = bare_jid == jid_bare(from); --module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from)); if not privacy_lists or privacy_lists.lists == nil or not (session.activePrivacyList or privacy_lists.default) then return; -- Nothing to block, default is Allow all end if is_from_user and is_to_user then --module:log("debug", "Not blocking communications between user's resources"); return; -- from one of a user's resource to another => HANDS OFF! end local listname = session.activePrivacyList; if listname == nil then listname = privacy_lists.default; -- no active list selected, use default list end local list = privacy_lists.lists[listname]; if not list then -- should never happen module:log("warn", "given privacy list not found. name: %s for user %s", listname, bare_jid); return; end for _,item in ipairs(list.items) do local apply = false; local block = false; if ( (stanza.name == "message" and item.message) or (stanza.name == "iq" and item.iq) or (stanza.name == "presence" and is_to_user and item["presence-in"]) or (stanza.name == "presence" and is_from_user and item["presence-out"]) or (item.message == false and item.iq == false and item["presence-in"] == false and item["presence-out"] == false) ) then apply = true; end if apply then local evilJid = {}; apply = false; if is_to_user then --module:log("debug", "evil jid is (from): %s", from); evilJid.node, evilJid.host, evilJid.resource = jid_split(from); else --module:log("debug", "evil jid is (to): %s", to); evilJid.node, evilJid.host, evilJid.resource = jid_split(to); end if item.type == "jid" and (evilJid.node and evilJid.host and evilJid.resource and item.value == evilJid.node.."@"..evilJid.host.."/"..evilJid.resource) or (evilJid.node and evilJid.host and item.value == evilJid.node.."@"..evilJid.host) or (evilJid.host and evilJid.resource and item.value == evilJid.host.."/"..evilJid.resource) or (evilJid.host and item.value == evilJid.host) then apply = true; block = (item.action == "deny"); elseif item.type == "group" then local roster = load_roster(session.username, session.host); local roster_entry = roster[jid_join(evilJid.node, evilJid.host)]; if roster_entry then local groups = roster_entry.groups; for group in pairs(groups) do if group == item.value then apply = true; block = (item.action == "deny"); break; end end end elseif item.type == "subscription" then -- we need a valid bare evil jid local roster = load_roster(session.username, session.host); local roster_entry = roster[jid_join(evilJid.node, evilJid.host)]; if (not(roster_entry) and item.value == "none") or (roster_entry and roster_entry.subscription == item.value) then apply = true; block = (item.action == "deny"); end elseif item.type == nil then apply = true; block = (item.action == "deny"); end end if apply then if block then -- drop and not bounce groupchat messages, otherwise users will get kicked if stanza.attr.type == "groupchat" then return true; end module:log("debug", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from)); if stanza.name == "message" then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); elseif stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); end return true; -- stanza blocked ! else --module:log("debug", "stanza explicitly allowed!") return; end end end end function preCheckIncoming(e) local session; if e.stanza.attr.to ~= nil then local node, host, resource = jid_split(e.stanza.attr.to); if node == nil or host == nil then return; end if resource == nil then local prio = 0; if bare_sessions[node.."@"..host] ~= nil then for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do if session_.priority ~= nil and session_.priority >= prio then session = session_; prio = session_.priority; end end end else session = full_sessions[node.."@"..host.."/"..resource]; end if session ~= nil then return checkIfNeedToBeBlocked(e, session); else --module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource)); end end end function preCheckOutgoing(e) local session = e.origin; if e.stanza.attr.from == nil then e.stanza.attr.from = session.username .. "@" .. session.host; if session.resource ~= nil then e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource; end end if session.username then -- FIXME do properly return checkIfNeedToBeBlocked(e, session); end end module:hook("pre-message/full", preCheckOutgoing, 500); module:hook("pre-message/bare", preCheckOutgoing, 500); module:hook("pre-message/host", preCheckOutgoing, 500); module:hook("pre-iq/full", preCheckOutgoing, 500); module:hook("pre-iq/bare", preCheckOutgoing, 500); module:hook("pre-iq/host", preCheckOutgoing, 500); module:hook("pre-presence/full", preCheckOutgoing, 500); module:hook("pre-presence/bare", preCheckOutgoing, 500); module:hook("pre-presence/host", preCheckOutgoing, 500); module:hook("message/full", preCheckIncoming, 500); module:hook("message/bare", preCheckIncoming, 500); module:hook("message/host", preCheckIncoming, 500); module:hook("iq/full", preCheckIncoming, 500); module:hook("iq/bare", preCheckIncoming, 500); module:hook("iq/host", preCheckIncoming, 500); module:hook("presence/full", preCheckIncoming, 500); module:hook("presence/bare", preCheckIncoming, 500); module:hook("presence/host", preCheckIncoming, 500); prosody-modules-c53cc1ae4788/mod_addressing/0002755000175500017550000000000013164216767020576 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_addressing/mod_addressing.lua0000644000175500017550000000344113163400720024242 0ustar debacledebacle-- TODO Querying other servers for support, needs to keep track of remote -- server disco features local xmlns_address = 'http://jabber.org/protocol/address'; local function handle_extended_addressing(data) local stanza = data.stanza; if stanza.attr.type == "error" then return -- so we don't process bounces end local orig_to = stanza.attr.to; local addresses = stanza:get_child("addresses", xmlns_address); if addresses then module:log("debug", "Extended addressing found"); local destinations = {}; addresses:maptags(function(address) if address.attr.xmlns == xmlns_address and address.name == "address" then local type, jid, delivered = address.attr.type, address.attr.jid, address.attr.delivered; if (type == "cc" or type == "bcc" or type == "to") and jid and not delivered then destinations[#destinations+1] = jid; module:log("debug", "%s to %s", type, jid) if type == "to" or type == "cc" then address.attr.delivered = "true"; return address; elseif type == "bcc" then return nil; end end end return address; -- unsupported stuff goes right back end); for i=1,#destinations do stanza.attr.to = destinations[i]; module:log("debug", "posting stanza to %s", destinations[i]) module:send(stanza); end stanza.attr.to = orig_to; return stanza.attr.to == module.host or nil; end end module:hook("message/host", handle_extended_addressing, 10); module:hook("message/bare", handle_extended_addressing, 10); module:hook("message/full", handle_extended_addressing, 10); module:hook("presence/host", handle_extended_addressing, 10); module:hook("presence/bare", handle_extended_addressing, 10); module:hook("presence/full", handle_extended_addressing, 10); -- IQ stanzas makes no sense module:add_feature(xmlns_address); prosody-modules-c53cc1ae4788/mod_addressing/README.markdown0000644000175500017550000000043113163400720023252 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: 'XEP-0033: Extended Stanza Addressing' ... Introduction ============ This module is a partial implementation of [XEP-0033: Extended Stanza Addressing](http://xmpp.org/extensions/xep-0033.html). TODO ==== Query external servers for support. prosody-modules-c53cc1ae4788/mod_extdisco/0002755000175500017550000000000013164216767020275 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_extdisco/mod_extdisco.lua0000644000175500017550000000312213163400720023434 0ustar debacledebaclelocal st = require "util.stanza"; local services = module:get_option("external_services"); local xmlns_extdisco = "urn:xmpp:extdisco:1"; module:add_feature(xmlns_extdisco); module:hook("iq-get/host/"..xmlns_extdisco..":services", function (event) local origin, stanza = event.origin, event.stanza; local service = stanza:get_child("service", xmlns_extdisco); local service_type = service and service.attr.type; local reply = st.reply(stanza); for host, service_info in pairs(services) do if not(service_type) or service_info.type == service_type then reply:tag("service", { host = host; port = service_info.port; transport = service_info.transport; type = service_info.type; username = service_info.username; password = service_info.password; }):up(); end end origin.send(reply); return true; end); module:hook("iq-get/host/"..xmlns_extdisco..":credentials", function (event) local origin, stanza = event.origin, event.stanza; local credentials = stanza:get_child("credentials", xmlns_extdisco); local host = credentials and credentials.attr.host; if not host then origin.send(st.error_reply(stanza, "cancel", "bad-request", "No host specified")); return true; end local service_info = services[host]; if not service_info then origin.send(st.error_reply(stanza, "cancel", "item-not-found", "No such service known")); return true; end local reply = st.reply(stanza) :tag("credentials") :tag("service", { host = host; username = service_info.username; password = service_info.password; }):up(); origin.send(reply); return true; end); prosody-modules-c53cc1ae4788/mod_extdisco/README.markdown0000644000175500017550000000214313163400720022753 0ustar debacledebacle--- summary: External Service Discovery ... Introduction ============ This module adds support for [XEP-0215: External Service Discovery], which lets Prosody advertise non-XMPP services. Configuration ============= Example services from the XEP: ``` {.lua} modules_enabled = { -- other modules ... "extdisco"; } external_services = { ["stun.shakespeare.lit"] = { port="9998"; transport="udp"; type="stun"; }; ["relay.shakespeare.lit"] = { password="jj929jkj5sadjfj93v3n"; port="9999"; transport="udp"; type="turn"; username="nb78932lkjlskjfdb7g8"; }; ["192.0.2.1"] = { port="8888"; transport="udp"; type="stun"; }; ["192.0.2.1"] = { port="8889"; password="93jn3bakj9s832lrjbbz"; transport="udp"; type="turn"; username="auu98sjl2wk3e9fjdsl7"; }; ["ftp.shakespeare.lit"] = { name="Shakespearean File Server"; password="guest"; port="20"; transport="tcp"; type="ftp"; username="guest"; }; } ``` prosody-modules-c53cc1ae4788/mod_statistics_statsd/0002755000175500017550000000000013164216767022227 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_statistics_statsd/mod_statistics_statsd.lua0000644000175500017550000000243313163400720027324 0ustar debacledebaclelocal statsmanager = require "core.statsmanager"; local udp = require "socket".udp(); local server = module:get_option_string("statsd_server_ip", "127.0.0.1"); local server_port = module:get_option_number("statsd_server_port", 8124); local max_datagram_size = module:get_option_number("statds_packet_size", 512); function push_stats(stats, meta) local metric_strings, remaining_bytes = {}, max_datagram_size; for name, value in pairs(stats) do local value_meta = meta[name]; module:log("warn", "%s %s", name, tostring(value_meta)); --if not value_meta then -- Simple value (gauge) local metric_string = ("%s|%d|g"):format(name, value); if #metric_string > remaining_bytes then udp:sendto(table.concat(metric_strings, "\n"), server, server_port); metric_strings, remaining_bytes = {}, max_datagram_size; end table.insert(metric_strings, metric_string); remaining_bytes = remaining_bytes - (#metric_string + 1); -- +1 for newline --end end if #metric_strings > 0 then udp:sendto(table.concat(metric_strings, "\n"), server, server_port); end end module:hook_global("stats-updated", function (event) push_stats(event.changed_stats, event.stats_extra); end); function module.load() local all, changed, extra = statsmanager.get_stats(); push_stats(all, extra); end prosody-modules-c53cc1ae4788/mod_http_host_status_check/0002755000175500017550000000000013164216767023227 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_http_host_status_check/mod_http_host_status_check.lua0000644000175500017550000000572413163400720031332 0ustar debacledebaclelocal heartbeats = module:shared("/*/host_status_check/heartbeats"); local events = module:shared("/*/host_status_check/connection_events"); local host_status_ok = module:shared("host_status_ok"); local time = require "socket".gettime; local template = require "util.interpolation".new("%b{}", function (s) return s end) module:depends "http" local threshold = module:get_option_number("status_check_heartbeat_threshold", 10); local function status_string(status, duration, comment) local string_timestamp; if duration then string_timestamp = ("(%0.2fs%s)"):format(duration, comment or ""); elseif comment then string_timestamp = ("(%s)"):format(comment); else return status and "UP" or "DOWN"; end return (status and "UP " or "DOWN ")..string_timestamp; end local function string_pad(s, len) return s..(" "):rep(len-#s); end local status_page_template = [[ STATUS {status} {host_statuses%HOST {item} {idx} }]]; function status_page() local host_statuses = {}; local current_time = time(); local all_ok = true; local failed_hosts = {}; for host in pairs(hosts) do local last_heartbeat_time = heartbeats[host]; local ok, status_text = true; local is_component = hosts[host].type == "component" and hosts[host].modules.component; if is_component then local current_status = hosts[host].modules.component.connected; if events[host] then local tracked_status = events[host].connected; if tracked_status == current_status then status_text = status_string(current_status, time() - events[host].timestamp); else status_text = status_string(current_status, nil, "!"); end else status_text = status_string(current_status, nil, "?"); end if not current_status then ok = false; end else local event_info = events[host]; local connected = true; if event_info then connected = event_info.connected; end status_text = status_string(connected, event_info and (time() - events[host].timestamp), not event_info and "?"); end if last_heartbeat_time then local time_since_heartbeat = current_time - last_heartbeat_time; if ok then if time_since_heartbeat > threshold then status_text = ("TIMEOUT (%0.2fs)"):format(time_since_heartbeat); ok = false; else status_text = status_text:gsub("^%S+", "GOOD"); end end end if not ok then all_ok = false; table.insert(failed_hosts, host); end if not ok or is_component or last_heartbeat_time then host_statuses[host] = string_pad(status_text, 20); end local last_ok = host_status_ok[host]; if last_ok ~= ok then if last_ok ~= nil then module:log("warn", "Host status check %s (%s)", ok and "OK" or "FAILED", status_text); end host_status_ok[host] = ok; end end local page = template(status_page_template, { status = all_ok and "OK" or ("FAIL: "..table.concat(failed_hosts, ", ")); host_statuses = host_statuses; }); return page; end module:provides("http", { route = { GET = status_page; }; }) prosody-modules-c53cc1ae4788/mod_http_host_status_check/README.markdown0000644000175500017550000000161213163400720025705 0ustar debacledebacle--- labels: Stage-Beta summary: HTTP Host Status Check ... Introduction ============ This module exposes serves over HTTP the information collected by [mod\_host\_status\_check] and [mod\_host\_status\_heartbeat] in a convenient format for automated monitoring tools. Configuration ============= [mod\_http\_status\_check] relies on Prosodys HTTP server and mod\_http for serving HTTP requests. See [Prosodys HTTP server documentation][doc:http] for information about how to configure ports, HTTP Host names etc. Simply add this module to modules\_enabled for the host you would like to serve it from. There is a single configuration option: ``` {.lua} -- The maximum number of seconds that a host can go without sending a heartbeat, -- before we mark it as TIMEOUT (default: 5) status_check_heartbeat_threshold = 5; ``` Compatibility ============= Works with Prosody 0.9.x and later. prosody-modules-c53cc1ae4788/mod_auth_imap/0002755000175500017550000000000013164216767020422 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_auth_imap/auth_imap/0002755000175500017550000000000013164216767022371 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_auth_imap/auth_imap/mod_auth_imap.lua0000644000175500017550000000413613163400720025663 0ustar debacledebacle-- IMAP authentication backend for Prosody -- -- Copyright (C) 2011 FIMXE from hg annotate -u local name = "IMAP SASL"; local log = require "util.logger".init("auth_imap"); local imap_host = module:get_option_string("imap_auth_host", "localhost"); local imap_port = module:get_option_number("imap_auth_port"); local imap_service_realm = module:get_option_string("imap_auth_realm", module:get_option("sasl_realm")); local imap_service_name = module:get_option_string("imap_auth_service_name"); local append_host = module:get_option_boolean("auth_append_host"); local verify_certificate = module:get_option_boolean("auth_imap_verify_certificate", true); local ssl_params = module:get_option("auth_imap_ssl", { mode = "client", protocol = "sslv23"; capath = "/etc/ssl/certs"; options = { "no_sslv2", "no_sslv3" }; verify = verify_certificate and { "peer", "fail_if_no_peer_cert" } or nil; ciphers = "HIGH:!DSS:!aNULL@STRENGTH"; }); local new_imap_sasl = module:require "sasl_imap".new; local new_sasl = function(realm) return new_imap_sasl( imap_service_realm or realm, imap_service_name or "xmpp", imap_host, imap_port, ssl_params, append_host ); end do local s = new_sasl(module.host) assert(s, "Could not create a new SASL object"); assert(s.mechanisms, "SASL object has no mechanims method"); local m = {}; for k in pairs(s:mechanisms()) do table.insert(m, k); end log("debug", "Mechanims found: %s", table.concat(m, ", ")); end provider = { name = module.name:gsub("^auth_",""); }; function provider.test_password(username, password) return nil, "Legacy auth not supported with "..name; end function provider.get_password(username) return nil, "Passwords unavailable for "..name; end function provider.set_password(username, password) return nil, "Passwords unavailable for "..name; end function provider.user_exists(username) -- FIXME return true end function provider.create_user(username, password) return nil, "Account creation/modification not available with "..name; end function provider.get_sasl_handler() return new_sasl(module.host); end module:add_item("auth-provider", provider); prosody-modules-c53cc1ae4788/mod_auth_imap/auth_imap/sasl_imap.lib.lua0000644000175500017550000001460113163400720025570 0ustar debacledebacle-- Dovecot authentication backend for Prosody -- -- Copyright (C) 2011 Kim Alvefur -- local log = require "util.logger".init("sasl_imap"); local setmetatable = setmetatable; local s_match = string.match; local t_concat = table.concat; local tostring, tonumber = tostring, tonumber; local socket = require "socket" local ssl = require "ssl" local x509 = require "util.x509"; local base64 = require "util.encodings".base64; local b64, unb64 = base64.encode, base64.decode; local _M = {}; local method = {}; method.__index = method; -- For extracting the username. local mitm = { PLAIN = function(message) return s_match(message, "^[^%z]*%z([^%z]+)%z[^%z]+"); end, ["SCRAM-SHA-1"] = function(message) return s_match(message, "^[^,]+,[^,]*,n=([^,]*)"); end, ["DIGEST-MD5"] = function(message) return s_match(message, "username=\"([^\"]*)\""); end, } local function connect(host, port, ssl_params) port = tonumber(port) or (ssl_params and 993 or 143); log("debug", "connect() to %s:%s:%d", ssl_params and "ssl" or "tcp", host, tonumber(port)); local conn = socket.tcp(); -- Create a connection to imap socket log("debug", "connecting to imap at '%s:%d'", host, port); local ok, err = conn:connect(host, port); conn:settimeout(10); if not ok then log("error", "error connecting to imap at '%s:%d': %s", host, port, err); return false; end if ssl_params then -- Perform SSL handshake local ok, err = ssl.wrap(conn, ssl_params); if ok then conn = ok; ok, err = conn:dohandshake(); end if not ok then log("error", "error initializing ssl connection to imap at '%s:%d': %s", host, port, err); conn:close(); return false; end -- Verify certificate if ssl_params.verify then if not conn.getpeercertificate then log("error", "unable to verify certificate, newer LuaSec required: https://prosody.im/doc/depends#luasec"); conn:close(); return false; end if not x509.verify_identity(host, nil, conn:getpeercertificate()) then log("warn", "invalid certificate for imap service %s:%d, denying connection", host, port); return false; end end end -- Parse IMAP handshake local supported_mechs = {}; local line = conn:receive("*l"); if not line then return false; end log("debug", "imap greeting: '%s'", line); local caps = line:match("^%*%s+OK%s+(%b[])"); if not caps or not caps:match("^%[CAPABILITY ") then conn:send("A CAPABILITY\n"); line = conn:receive("*l"); log("debug", "imap capabilities response: '%s'", line); caps = line:match("^%*%s+CAPABILITY%s+(.*)$"); if not conn:receive("*l"):match("^A OK") then log("debug", "imap capabilities command failed") conn:close(); return false; end elseif caps then caps = caps:sub(2,-2); -- Strip surrounding [] end if caps then for cap in caps:gmatch("%S+") do log("debug", "Capability: %s", cap); local mech = cap:match("AUTH=(.*)"); if mech then log("debug", "Supported SASL mechanism: %s", mech); supported_mechs[mech] = mitm[mech] and true or nil; end end end return conn, supported_mechs; end -- create a new SASL object which can be used to authenticate clients function _M.new(realm, service_name, host, port, ssl_params, append_host) log("debug", "new(%q, %q, %q, %d)", realm or "", service_name or "", host or "", port or 0); local sasl_i = { realm = realm; service_name = service_name; _host = host; _port = port; _ssl_params = ssl_params; _append_host = append_host; }; local conn, mechs = connect(host, port, ssl_params); if not conn then return nil, "Socket connection failure"; end if append_host then mechs = { PLAIN = mechs.PLAIN }; end sasl_i.conn, sasl_i.mechs = conn, mechs; return setmetatable(sasl_i, method); end -- get a fresh clone with the same realm and service name function method:clean_clone() if self.conn then self.conn:close(); self.conn = nil; end log("debug", "method:clean_clone()"); return _M.new(self.realm, self.service_name, self._host, self._port, self._ssl_params, self._append_host) end -- get a list of possible SASL mechanisms to use function method:mechanisms() log("debug", "method:mechanisms()"); return self.mechs; end -- select a mechanism to use function method:select(mechanism) log("debug", "method:select(%q)", mechanism); if not self.selected and self.mechs[mechanism] then self.tag = tostring({}):match("0x(%x*)$"); self.selected = mechanism; local selectmsg = t_concat({ self.tag, "AUTHENTICATE", mechanism }, " "); log("debug", "Sending %d bytes: %q", #selectmsg, selectmsg); local ok, err = self.conn:send(selectmsg.."\n"); if not ok then log("error", "Could not write to socket: %s", err); return "failure", "internal-server-error", err end local line, err = self.conn:receive("*l"); if not line then log("error", "Could not read from socket: %s", err); return "failure", "internal-server-error", err end log("debug", "Received %d bytes: %q", #line, line); return line:match("^+") end end -- feed new messages to process into the library function method:process(message) local username = mitm[self.selected](message); if username then self.username = username; end if self._append_host and self.selected == "PLAIN" then message = message:gsub("^([^%z]*%z[^%z]+)(%z[^%z]+)$", "%1@"..self.realm.."%2"); end log("debug", "method:process(%d bytes): %q", #message, message:gsub("%z", ".")); local ok, err = self.conn:send(b64(message).."\n"); if not ok then log("error", "Could not write to socket: %s", err); return "failure", "internal-server-error", err end log("debug", "Sent %d bytes to socket", ok); local line, err = self.conn:receive("*l"); if not line then log("error", "Could not read from socket: %s", err); return "failure", "internal-server-error", err end log("debug", "Received %d bytes from socket: %s", #line, line); while line and line:match("^%* ") do line, err = self.conn:receive("*l"); end if line:match("^%+") and #line > 2 then local data = line:sub(3); data = data and unb64(data); return "challenge", unb64(data); elseif line:sub(1, #self.tag) == self.tag then local ok, rest = line:sub(#self.tag+1):match("(%w+)%s+(.*)"); ok = ok:lower(); log("debug", "%s: %s", ok, rest); if ok == "ok" then return "success" elseif ok == "no" then return "failure", "not-authorized", rest; end elseif line:match("^%* BYE") then local err = line:match("BYE%s*(.*)"); return "failure", "not-authorized", err; end end return _M; prosody-modules-c53cc1ae4788/mod_auth_imap/README.markdown0000644000175500017550000000137113163400720023102 0ustar debacledebacle--- labels: - 'Stage-Alpha' - 'Type-Auth' summary: IMAP authentication module ... Introduction ============ This is a Prosody authentication plugin which uses a generic IMAP server as the backend. Configuration ============= option type default --------------------------------- --------- -------------------------------- imap\_auth\_host string localhost imap\_auth\_port number nil imap\_auth\_realm string Same as the sasl\_realm option imap\_auth\_service\_name string nil auth\_append\_host boolean false auth\_imap\_verify\_certificate boolean true auth\_imap\_ssl table A SSL/TLS config prosody-modules-c53cc1ae4788/mod_statistics_cputotal/0002755000175500017550000000000013164216767022560 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_statistics_cputotal/mod_statistics_cputotal.lua0000644000175500017550000000035213163400720030204 0ustar debacledebacle-- Provides total CPU time, useful for DERIVE module:set_global(); module:provides("statistics", { statistics = { cpu_total = { -- milliseconds of CPU time used get = function() return os.clock() * 1000; end } } }); prosody-modules-c53cc1ae4788/mod_auth_ldap/0002755000175500017550000000000013164216767020414 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_auth_ldap/README.markdown0000644000175500017550000000543113163400720023075 0ustar debacledebacle--- labels: - 'Stage-Alpha' - 'Type-Auth' summary: LDAP authentication module ... Introduction ============ This is a Prosody authentication plugin which uses LDAP as the backend. Dependecies =========== This module depends on [LuaLDAP](http://www.keplerproject.org/lualdap/) for connecting to an LDAP server. Configuration ============= Copy the module to the prosody modules/plugins directory. In Prosody's configuration file, under the desired host section, add: ``` {.lua} authentication = "ldap" ldap_base = "ou=people,dc=example,dc=com" ``` Further LDAP options are: Name Description Default value ---------------- ---------------------------------------------------------------------------------------------------------------------- -------------------- ldap\_base LDAP base directory which stores user accounts **Required field** ldap\_server Space-separated list of hostnames or IPs, optionally with port numbers (e.g. "localhost:8389") `"localhost"` ldap\_rootdn The distinguished name to auth against `""` (anonymous) ldap\_password Password for rootdn `""` ldap\_filter Search filter, with `$user` and `$host` substituded for user- and hostname `"(uid=$user)"` ldap\_scope Search scope. other values: "base" and "onelevel" `"subtree"` ldap\_tls Enable TLS (StartTLS) to connect to LDAP (can be true or false). The non-standard 'LDAPS' protocol is not supported. `false` ldap\_mode How passwords are validated. `"bind"` ldap\_admins Search filter to match admins, works like ldap\_scope **Note:** lua-ldap reads from `/etc/ldap/ldap.conf` and other files like `~prosody/.ldaprc` if they exist. Users wanting to use a particular TLS root certificate can specify it in the normal way using TLS\_CACERT in the OpenLDAP config file. Modes ===== The `"getpasswd"` mode requires plain text access to passwords in LDAP and feeds them into Prosodys authentication system. This enables more secure authentication mechanisms but does not work for all deployments. The `"bind"` mode performs an LDAP bind, does not require plain text access to passwords but limits you to the PLAIN authentication mechanism. Compatibility ============= Works with 0.8 and later. prosody-modules-c53cc1ae4788/mod_auth_ldap/mod_auth_ldap.lua0000644000175500017550000001052013163400720023672 0ustar debacledebacle-- mod_auth_ldap local jid_split = require "util.jid".split; local new_sasl = require "util.sasl".new; local lualdap = require "lualdap"; local function ldap_filter_escape(s) return (s:gsub("[*()\\%z]", function(c) return ("\\%02x"):format(c:byte()) end)); end -- Config options local ldap_server = module:get_option_string("ldap_server", "localhost"); local ldap_rootdn = module:get_option_string("ldap_rootdn", ""); local ldap_password = module:get_option_string("ldap_password", ""); local ldap_tls = module:get_option_boolean("ldap_tls"); local ldap_scope = module:get_option_string("ldap_scope", "subtree"); local ldap_filter = module:get_option_string("ldap_filter", "(uid=$user)"):gsub("%%s", "$user", 1); local ldap_base = assert(module:get_option_string("ldap_base"), "ldap_base is a required option for ldap"); local ldap_mode = module:get_option_string("ldap_mode", "bind"); local ldap_admins = module:get_option_string("ldap_admin_filter"); local host = ldap_filter_escape(module:get_option_string("realm", module.host)); -- Initiate connection local ld = nil; module.unload = function() if ld then pcall(ld, ld.close); end end function ldap_do_once(method, ...) if ld == nil then local err; ld, err = lualdap.open_simple(ldap_server, ldap_rootdn, ldap_password, ldap_tls); if not ld then return nil, err, "reconnect"; end end -- luacheck: ignore 411/success local success, iterator, invariant, initial = pcall(ld[method], ld, ...); if not success then ld = nil; return nil, iterator, "search"; end local success, dn, attr = pcall(iterator, invariant, initial); if not success then ld = nil; return success, dn, "iter"; end return dn, attr, "return"; end function ldap_do(method, retry_count, ...) local dn, attr, where; for _=1,1+retry_count do dn, attr, where = ldap_do_once(method, ...); if dn or not(attr) then break; end -- nothing or something found module:log("warn", "LDAP: %s %s (in %s)", tostring(dn), tostring(attr), where); -- otherwise retry end if not dn and attr then module:log("error", "LDAP: %s", tostring(attr)); end return dn, attr; end local function get_user(username) module:log("debug", "get_user(%q)", username); return ldap_do("search", 2, { base = ldap_base; scope = ldap_scope; sizelimit = 1; filter = ldap_filter:gsub("%$(%a+)", { user = ldap_filter_escape(username); host = host; }); }); end local provider = {}; function provider.create_user(username, password) -- luacheck: ignore 212 return nil, "Account creation not available with LDAP."; end function provider.user_exists(username) return not not get_user(username); end function provider.set_password(username, password) local dn, attr = get_user(username); if not dn then return nil, attr end if attr.userPassword == password then return true end return ldap_do("modify", 2, dn, { '=', userPassword = password }); end if ldap_mode == "getpasswd" then function provider.get_password(username) local dn, attr = get_user(username); if dn and attr then return attr.userPassword; end end function provider.test_password(username, password) return provider.get_password(username) == password; end function provider.get_sasl_handler() return new_sasl(module.host, { plain = function(sasl, username) -- luacheck: ignore 212/sasl local password = provider.get_password(username); if not password then return "", nil; end return password, true; end }); end elseif ldap_mode == "bind" then local function test_password(userdn, password) return not not lualdap.open_simple(ldap_server, userdn, password, ldap_tls); end function provider.test_password(username, password) local dn = get_user(username); if not dn then return end return test_password(dn, password) end function provider.get_sasl_handler() return new_sasl(module.host, { plain_test = function(sasl, username, password) -- luacheck: ignore 212/sasl return provider.test_password(username, password), true; end }); end else module:log("error", "Unsupported ldap_mode %s", tostring(ldap_mode)); end if ldap_admins then function provider.is_admin(jid) local username = jid_split(jid); return ldap_do("search", 2, { base = ldap_base; scope = ldap_scope; sizelimit = 1; filter = ldap_admins:gsub("%$(%a+)", { user = ldap_filter_escape(username); host = host; }); }); end end module:provides("auth", provider); prosody-modules-c53cc1ae4788/mod_fallback_vcard/0002755000175500017550000000000013164216767021371 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_fallback_vcard/mod_fallback_vcard.lua0000644000175500017550000000332013163400720025624 0ustar debacledebaclelocal datamanager = require "util.datamanager"; local usermanager = require "core.usermanager"; local st = require "util.stanza"; local host = module.host; local jid_split = require "util.jid".split; local orgname = module:get_option_string("default_vcard_orgname"); local orgmail = module:get_option_boolean("default_vcard_orgmail"); module:hook("iq/bare/vcard-temp:vCard", function(event) local session, stanza = event.origin, event.stanza; local to = stanza.attr.to; local username = jid_split(to); if not username then return end local vcard = datamanager.load(username, host, "vcard"); local data = datamanager.load(username, host, "account_details"); local exists = usermanager.user_exists(username, host); module:log("debug", "has %s: %s", "vcard", tostring(vcard)); module:log("debug", "has %s: %s", "data", tostring(data)); module:log("debug", "has %s: %s", "exists", tostring(exists)); data = data or {}; if not(vcard) and data and exists then -- MAYBE -- first .. " " .. last -- first, last = name:match("^(%w+) (%w+)$") local vcard = st.reply(stanza):tag("vCard", { xmlns = "vcard-temp" }) :tag("VERSION"):text("3.0"):up() :tag("N") :tag("FAMILY"):text(data.last or ""):up() :tag("GIVEN"):text(data.first or ""):up() :up() :tag("FN"):text(data.name or ""):up() :tag("NICKNAME"):text(data.nick or username):up() :tag("JABBERID"):text(username.."@"..host):up(); if orgmail then vcard:tag("EMAIL"):tag("USERID"):text(username.."@"..host):up():up(); elseif data.email then vcard:tag("EMAIL"):tag("USERID"):text(data.email):up():up(); end if orgname then vcard:tag("ORG"):tag("ORGNAME"):text(orgname):up():up(); end session.send(vcard); return true; end end, 1); prosody-modules-c53cc1ae4788/mod_log_http/0002755000175500017550000000000013164216767020273 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_log_http/mod_log_http.lua0000644000175500017550000000372713163400720023443 0ustar debacledebaclemodule:set_global(); local http = require "net.http"; local codes = require "net.http.codes"; local json = require "util.json"; local log = assert(io.open(assert(module:get_option_string("log_http_file"), "Please supply log_http_file in the config"), "a+")); local function append_request(id, req) local headers = {}; for k, v in pairs(req.headers) do table.insert(headers, { name = k, value = v }); end local queryString = {}; if req.query then for _, pair in ipairs(http.formdecode(req.query)) do table.insert(queryString, pair); end end log:write("<<<", json.encode({ id = id; type = "request"; method = req.method; url = req.url; httpVersion = "HTTP/1.1"; cookies = {}; headers = headers; queryString = queryString; postData = req.body and { mimeType = req.headers["Content-Type"]; text = req.body; } or nil; headersSize = -1; bodySize = -1; }), "\n"); end local function append_response(id, resp) local headers = {}; for k, v in pairs(resp.headers) do table.insert(headers, { name = k, value = v }); end log:write(">>>", json.encode({ id = id; type = "response"; status = resp.code; statusText = codes[resp.code]; httpVersion = resp.httpversion; cookies = {}; headers = headers; content = resp.body and { size = #resp.body; mimeType = resp.headers.content_type; text = resp.body; } or nil; headersSize = -1; bodySize = -1; }), "\n"); end module:hook_object_event(http.events, "request", function (event) module:log("warn", "Request to %s!", event.url); append_request(event.request.id, event.request); end); module:hook_object_event(http.events, "request-connection-error", function (event) module:log("warn", "Failed to make request to %s!", event.url); end); module:hook_object_event(http.events, "response", function (event) module:log("warn", "Received response %d from %s!", event.code, event.url); append_response(event.request.id, event.response); end); function module.unload() log:close(); end prosody-modules-c53cc1ae4788/mod_log_http/README.markdown0000644000175500017550000000062213163400720022751 0ustar debacledebacle--- summary: HTTP request logging ... Introduction ============ This module logs *outgoing* requests that go via the internal net.http API. Output format liable to change. Configuration ============= One option is required, set `log_http_file` to the file path you would like to log to. Compatibility ============= ----- ------- 0.10 Works (requires 375cf924fce1 or later) ----- ------- prosody-modules-c53cc1ae4788/mod_flash_policy/0002755000175500017550000000000013164216767021127 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_flash_policy/mod_flash_policy.lua0000644000175500017550000000246113163400720025125 0ustar debacledebaclelocal filters = require "util.filters"; local config = {} config.file = module:get_option_string("crossdomain_file", ""); config.string = module:get_option_string("crossdomain_string", [[]]); local string = '' if not config.file ~= '' then local f = assert(io.open(config.file)); string = f:read("*all"); else string = config.string end module:log("debug", "crossdomain string: "..string); module:set_global(); function filter_policy(data, session) -- Since we only want to check the first block of data, remove the filter filters.remove_filter(session, "bytes/in", filter_policy); if data == "\0" then session.send(string.."\0"); return nil; -- Drop data to prevent it reaching the XMPP parser else return data; -- Pass data through, it wasn't a policy request end end function filter_session(session) if session.type == "c2s_unauthed" then filters.add_filter(session, "bytes/in", filter_policy, -1); end end function module.load() filters.add_filter_hook(filter_session); end function module.unload() filters.remove_filter_hook(filter_session); endprosody-modules-c53cc1ae4788/mod_flash_policy/README.markdown0000644000175500017550000000352013163400720023605 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: Adds support for flash socket policy ... Introduction ============ This Prosody plugin adds support for flash socket policies. When connecting with a flash client (from a webpage, not an exe) to prosody the flash client requests for an xml "file" on port 584 or the connecting port (5222 in the case of default xmpp). Responding on port 584 is tricky because it requires root priviliges to set up a socket on a port \< 1024. This plugins filters the incomming data from the flash client. So when the client connects with prosody it immediately sends a xml request string (`\0`). Prosody responds with a flash cross-domain-policy. See http://www.adobe.com/devnet/flashplayer/articles/socket\_policy\_files.html for more information. Usage ===== Add "flash\_policy" to your modules\_enabled list. Configuration ============= --------------------- -------------------------------------------------------------------------------- crossdomain\_file Optional. The path to a file containing an cross-domain-policy in xml format. crossdomain\_string Optional. A cross-domain-policy as string. Should include the xml declaration. --------------------- -------------------------------------------------------------------------------- Both configuration options are optional. If both are not specified a cross-domain-policy with "``" is used as default. Compatibility ============= ----- ------- 0.7 Works ----- ------- Caveats/Todos/Bugs ================== - The assumption is made that the first packet received will always contain the policy request data, and all of it. This isn't robust against fragmentation, but on the other hand I highly doubt you'll be seeing that with such a small packet. - Only tested by me on a single server :) prosody-modules-c53cc1ae4788/mod_auth_joomla/0002755000175500017550000000000013164216767020755 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_auth_joomla/mod_auth_joomla.lua0000644000175500017550000001277113163400720024606 0ustar debacledebacle-- Joomla authentication backend for Prosody -- -- Copyright (C) 2011 Waqas Hussain -- local new_sasl = require "util.sasl".new; local nodeprep = require "util.encodings".stringprep.nodeprep; local saslprep = require "util.encodings".stringprep.saslprep; local DBI = require "DBI" local md5 = require "util.hashes".md5; local uuid_gen = require "util.uuid".generate; local connection; local params = module:get_option("sql"); local prefix = params and params.prefix or "jos_"; local resolve_relative_path = require "core.configmanager".resolve_relative_path; local function test_connection() if not connection then return nil; end if connection:ping() then return true; else module:log("debug", "Database connection closed"); connection = nil; end end local function connect() if not test_connection() then prosody.unlock_globals(); local dbh, err = DBI.Connect( params.driver, params.database, params.username, params.password, params.host, params.port ); prosody.lock_globals(); if not dbh then module:log("debug", "Database connection failed: %s", tostring(err)); return nil, err; end module:log("debug", "Successfully connected to database"); dbh:autocommit(true); -- don't run in transaction connection = dbh; return connection; end end do -- process options to get a db connection params = params or { driver = "SQLite3" }; if params.driver == "SQLite3" then params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite"); end assert(params.driver and params.database, "Both the SQL driver and the database need to be specified"); assert(connect()); end local function getsql(sql, ...) if params.driver == "PostgreSQL" then sql = sql:gsub("`", "\""); end if not test_connection() then connect(); end -- do prepared statement stuff local stmt, err = connection:prepare(sql); if not stmt and not test_connection() then error("connection failed"); end if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end -- run query local ok, err = stmt:execute(...); if not ok and not test_connection() then error("connection failed"); end if not ok then return nil, err; end return stmt; end local function setsql(sql, ...) local stmt, err = getsql(sql, ...); if not stmt then return stmt, err; end return stmt:affected(); end local function get_password(username) local stmt, err = getsql("SELECT `password` FROM `"..prefix.."users` WHERE `username`=?", username); if stmt then for row in stmt:rows(true) do return row.password; end end end local function getCryptedPassword(plaintext, salt) local salted = plaintext..salt; return md5(salted, true); end local function joomlaCheckHash(password, hash) local crypt, salt = hash:match("^([^:]*):(.*)$"); return (crypt or hash) == getCryptedPassword(password, salt or ''); end local function joomlaCreateHash(password) local salt = uuid_gen():gsub("%-", ""); local crypt = getCryptedPassword(password, salt); return crypt..':'..salt; end provider = {}; function provider.test_password(username, password) local hash = get_password(username); return hash and joomlaCheckHash(password, hash); end function provider.user_exists(username) module:log("debug", "test user %s existence", username); return get_password(username) and true; end function provider.get_password(username) return nil, "Getting password is not supported."; end function provider.set_password(username, password) local hash = joomlaCreateHash(password); local stmt, err = setsql("UPDATE `"..prefix.."users` SET `password`=? WHERE `username`=?", hash, username); return stmt and true, err; end function provider.create_user(username, password) return nil, "Account creation/modification not supported."; end local escapes = { [" "] = "\\20"; ['"'] = "\\22"; ["&"] = "\\26"; ["'"] = "\\27"; ["/"] = "\\2f"; [":"] = "\\3a"; ["<"] = "\\3c"; [">"] = "\\3e"; ["@"] = "\\40"; ["\\"] = "\\5c"; }; local unescapes = {}; for k,v in pairs(escapes) do unescapes[v] = k; end local function jid_escape(s) return s and (s:gsub(".", escapes)); end local function jid_unescape(s) return s and (s:gsub("\\%x%x", unescapes)); end function provider.get_sasl_handler() local sasl = {}; function sasl:clean_clone() return provider.get_sasl_handler(); end function sasl:mechanisms() return { PLAIN = true; }; end function sasl:select(mechanism) if not self.selected and mechanism == "PLAIN" then self.selected = mechanism; return true; end end function sasl:process(message) if not message then return "failure", "malformed-request"; end local authorization, authentication, password = message:match("^([^%z]*)%z([^%z]+)%z([^%z]+)"); if not authorization then return "failure", "malformed-request"; end authentication = saslprep(authentication); password = saslprep(password); if (not password) or (password == "") or (not authentication) or (authentication == "") then return "failure", "malformed-request", "Invalid username or password."; end local function test(authentication) local prepped = nodeprep(authentication); local normalized = jid_unescape(prepped); return normalized and provider.test_password(normalized, password) and prepped; end local username = test(authentication) or test(jid_escape(authentication)); if username then self.username = username; return "success"; end return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent."; end return sasl; end module:provides("auth", provider); prosody-modules-c53cc1ae4788/mod_auth_joomla/README.markdown0000644000175500017550000000127413163400720023437 0ustar debacledebacle--- labels: - 'Stage-Alpha' - 'Type-Auth' summary: Joomla authentication module ... Introduction ============ This module allows you to authenticate against an Joomla database. Configuration ============= SQL connection paramaters are identical to those of [SQL storage](https://prosody.im/doc/modules/mod_storage_sql) except for an additional 'prefix' parameter that defaults to 'jos\_'.\_ authentication = "joomla" sql = { -- See documentation for SQL storage driver = "MySQL"; database = "joomla"; host = "localhost"; username = "prosody"; password = "secretpassword"; prefix = "jos_"; } Compatibility ============= Prosody 0.8+ prosody-modules-c53cc1ae4788/mod_storage_appendmap/0002755000175500017550000000000013164216767022144 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_storage_appendmap/mod_storage_appendmap.lua0000644000175500017550000000636613163400720027167 0ustar debacledebaclelocal dump = require "util.serialization".serialize; local load = require "util.envload".envloadfile; local dm = require "core.storagemanager".olddm; local driver = {}; local map = {}; local map_mt = { __index = map }; map.remove = {}; function map:get(user, key) module:log("debug", "map:get(%s, %s)", tostring(user), tostring(key)) local filename = dm.getpath(user, module.host, self.store, "map"); module:log("debug", "File is %s", filename); local env = {}; if _VERSION == "Lua 5.1" then -- HACK env._ENV = env; -- HACK end -- SO MANY HACKS local chunk, err, errno = load(filename, env); if not chunk then if errno == 2 then return end return chunk, err; end local ok, err = pcall(chunk); if not ok then return ok, err; end if _VERSION == "Lua 5.1" then -- HACK env._ENV = nil; -- HACK end -- HACKS EVERYWHERE if key == nil then return env; end return env[key]; end local keywords = { ["do"] = true; ["and"] = true; ["else"] = true; ["break"] = true; ["if"] = true; ["end"] = true; ["goto"] = true; ["false"] = true; ["in"] = true; ["for"] = true; ["then"] = true; ["local"] = true; ["or"] = true; ["nil"] = true; ["true"] = true; ["until"] = true; ["elseif"] = true; ["function"] = true; ["not"] = true; ["repeat"] = true; ["return"] = true; ["while"] = true; -- _ENV is not technically a keyword but we need to treat it as such ["_ENV"] = true; }; function map:set_keys(user, keyvalues) local keys, values = {}, {}; if _VERSION == "Lua 5.1" then assert(keyvalues._ENV == nil, "'_ENV' is a restricted key"); end for key, value in pairs(keyvalues) do module:log("debug", "user %s sets %q to %s", user, key, tostring(value)) if type(key) ~= "string" or not key:find("^[%a_][%w_]*$") or keywords[key] then key = "_ENV[" .. dump(key) .. "]"; end table.insert(keys, key); if value == self.remove then table.insert(values, "nil") else table.insert(values, dump(value)) end end local data = table.concat(keys, ", ") .. " = " .. table.concat(values, ", ") .. ";\n"; return dm.append_raw(user, module.host, self.store, "map", data); end function map:set(user, key, value) if _VERSION == "Lua 5.1" then assert(key ~= "_ENV", "'_ENV' is a restricted key"); end if key == nil then local filename = dm.getpath(user, module.host, self.store, "map"); os.remove(filename); return true; end if type(key) ~= "string" or not key:find("^[%w_][%w%d_]*$") or key == "_ENV" then key = "_ENV[" .. dump(key) .. "]"; end local data = key .. " = " .. dump(value) .. ";\n"; return dm.append_raw(user, module.host, self.store, "map", data); end local keyval = {}; local keyval_mt = { __index = keyval }; function keyval:get(user) return map.get(self, user); end function keyval:set(user, data) map.set(self, user); if data then for k, v in pairs(data) do map.set(self, user, k, v); end end return true; end -- TODO some kind of periodic compaction thing? function map:_compact(user) local data = self:get(user); return keyval.set(self, user, data); end function driver:open(store, typ) if typ == "map" then return setmetatable({ store = store, }, map_mt); elseif typ == nil or typ == "keyval" then return setmetatable({ store = store, }, keyval_mt); end return nil, "unsupported-store"; end module:provides("storage", driver); prosody-modules-c53cc1ae4788/mod_storage_appendmap/README.markdown0000644000175500017550000000114313163400720024621 0ustar debacledebacle--- labels: - 'Stage-Alpha' - 'Type-Storage' summary: Experimental map store optimized for small incremental changes ... This is an experimental storage driver where changed data is appended. Data is simply written as `key = value` pairs to the end of the file. This allows changes to individual keys to be written without needing to write out the entire object again, but reads would grow gradually larger as it still needs to read old overwritten keys. This may be suitable for eg rosters where individual contacts are changed at a time. In theory, this could also allow rolling back changes. Requires 0.10 prosody-modules-c53cc1ae4788/mod_smacks_noerror/0002755000175500017550000000000013164216767021502 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_smacks_noerror/mod_smacks_noerror.lua0000644000175500017550000000203013163400720026043 0ustar debacledebaclelocal t_insert = table.insert; local mod_smacks = module:depends"smacks" local function discard_unacked_messages(session) local queue = session.outgoing_stanza_queue; local replacement_queue = {}; session.outgoing_stanza_queue = replacement_queue; for _, stanza in ipairs(queue) do if stanza.name == "message" and stanza.attr.xmlns == nil and ( stanza.attr.type == "chat" or ( stanza.attr.type or "normal" ) == "normal" ) then -- do nothing here for normal messages and don't send out "message delivery errors", -- because messages are already in MAM at this point (no need to frighten users) else t_insert(replacement_queue, stanza); end end end local handle_unacked_stanzas = mod_smacks.handle_unacked_stanzas; mod_smacks.handle_unacked_stanzas = function (session) -- Only deal with authenticated (c2s) sessions if session.username then discard_unacked_messages(session) end return handle_unacked_stanzas(session); end function module.unload() mod_smacks.handle_unacked_stanzas = handle_unacked_stanzas; end prosody-modules-c53cc1ae4788/mod_smacks_noerror/README.markdown0000644000175500017550000000233013163400720024156 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: Monkeypatch mod_smacks to silently discard unacked message stanzas when a hibernation times out ... Introduction ============ By default mod_smacks sends back error stanzas for every unacked message stanza when the hibernation times out. This leads to "message not delivered" errors displayed in clients. When you are certain that *all* your clients use MAM, this is unneccessary and confuses users (the message will eventually be delivered via MAM). This module therefore monkeypatches mod_smacks to silently drop those unacked message stanzas instead of sending error replies. Unacked iq stanzas are still answered with an error reply though. Warning ======= You most certainly *should not* use this module if you cannot be certain that *all* your clients support and use MAM! Compatibility ============= ----- ------------------------------------------------------------------- trunk Untested 0.10 Works 0.9 Untested but should work 0.8 Untested but should work, use version [7693724881b3] of mod_smacks ----- ------------------------------------------------------------------- [7693724881b3]: //hg.prosody.im/prosody-modules/raw-file/7693724881b3/mod_smacks/mod_smacks.lua prosody-modules-c53cc1ae4788/mod_sms_clickatell/0002755000175500017550000000000013164216767021444 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_sms_clickatell/README.markdown0000644000175500017550000000442213163400720024124 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: XMPP to SMS gateway using the Clickatell API ... Introduction ============ This module provides and SMS gateway component which uses the Clickatell HTTP API to deliver text messages. See clickatell.com for details on their services. Note that at present, this is entirely one way: replies will either go nowhere or as sms to the source number you specify. Configuration ============= In prosody.cfg.lua: Component "sms.example.com" "sms_clickatell" sms_message_prefix = "some text" The sms\_message\_prefix is a piece of text you want prefixing to all messages sent through the gateway. For example, I use the prefix "`[Via XMPP]` " to indicate to recipients that I've sent the message via the internet rather than the mobile network. Since my primary use case for this component is to be able to send messages to people only reachable via mobile when I myself only have internet access and no mobile reception, this option allows me to give a hint to my recipients that any reply they send may not reach me in a timely manner. Usage ===== Once you've installed and configured, you should be able to use service discovery in your XMPP client to find the component service. Once found, you need to register with the service, supplying your Clickatell username, password, API ID, and a source number for your text messages. The source number is the mobile number you want messages to 'originate' from i.e. where your recipients see messages coming from. The number should be in international format without leading plus sign, or you can use some other format if clickatell supports it. To send text messages to a target number, you need to add a contact in the form of `[number]@sms.example.com`, where `[number]` is the mobile number of the recipient, in international format without leading plus sign, and sms.example.com is the name for the component you configured above. For example: 447999000001@sms.yourdomain.com You should then be able to send messages to this contact which get sent as text messages to the number by the component. Compatibility ============= ----- ------- 0.7 Works ----- ------- Todo ==== - Refactor to create a framework for multiple sms gateway back ends, and split Clickatell specific code in to its own back end prosody-modules-c53cc1ae4788/mod_sms_clickatell/mod_sms_clickatell.lua0000644000175500017550000005130413163400720025757 0ustar debacledebacle-- mod_sms_clickatell -- -- A Prosody module for sending SMS text messages from XMPP using the -- Clickatell gateway's HTTP API -- -- Hacked from mod_twitter by Phil Stewart, March 2011. Anything from -- mod_twitter copyright The Guy Who Wrote mod_twitter. Everything else -- copyright 2011 Phil Stewart. Licensed under the same terms as Prosody -- (MIT license, as per below) -- --[[ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] -- Raise an error if the modules hasn't been loaded as a component in prosody's config if module:get_host_type() ~= "component" then error(module.name.." should be loaded as a component, check out http://prosody.im/doc/components", 0); end local jid_split = require "util.jid".split; local st = require "util.stanza"; local datamanager = require "util.datamanager"; local timer = require "util.timer"; local config_get = require "core.configmanager".get; local http = require "net.http"; local base64 = require "util.encodings".base64; local serialize = require "util.serialization".serialize; local pairs, ipairs = pairs, ipairs; local setmetatable = setmetatable; local component_host = module:get_host(); local component_name = module.name; local data_cache = {}; --local clickatell_api_id = module:get_option_string("clickatell_api_id"); local sms_message_prefix = module:get_option_string("sms_message_prefix") or ""; --local sms_source_number = module:get_option_string("sms_source_number") or ""; --local users = setmetatable({}, {__mode="k"}); -- User data is held in smsuser objects local smsuser = {}; smsuser.__index = smsuser; -- Users table is used to store user data in the form of smsuser objects. -- It is indexed by the base jid of the user, so when a non-extant entry in the -- table is referenced, we pass the jid to smsuser:register to load the user local users = {}; setmetatable(users, { __index = function (table, key) return smsuser:register(key); end }); -- Create a new smsuser object function smsuser:new() newuser = {}; setmetatable(newuser, self); return newuser; end -- Store (save) the user object function smsuser:store() datamanager.store(self.jid, component_host, "data", self.data); end -- For debug function smsuser:logjid() module:log("logjid: ", self.jid); end -- Register a user against the base jid of the client. If a user entry for the -- bjid is already stored in the Prosody data manager, retrieve its data function smsuser:register(bjid) reguser = smsuser:new(); reguser.jid = bjid; reguser.data = datamanager.load(bjid, component_host, "data") or {}; return reguser; end -- Add a roster entry for the user -- SMS users must me of the form number@component_host function smsuser:roster_add(sms_number) if self.data.roster == nil then self.data.roster = {} end if self.data.roster[sms_number] == nil then self.data.roster[sms_number] = {screen_name=sms_number, subscription=nil}; end self:store(); end -- Update the roster entry of sms_number with new screen name function smsuser:roster_update_screen_name(sms_number, screen_name) if self.data.roster[sms_number] == nil then smsuser:roster_add(sms_number); end self.data.roster[sms_number].screen_name = screen_name; self:store(); end -- Update the roster entry of sms_number with new subscription detail function smsuser:roster_update_subscription(sms_number, subscription) if self.data.roster[sms_number] == nil then smsuser:roster_add(sms_number); end self.data.roster[sms_number].subscription = subscription; self:store(); end -- Delete an entry from the roster function smsuser:roster_delete(sms_number) self.data.roster[sms_number] = nil; self:store(); end -- function smsuser:roster_stanza_args(sms_number) if self.data.roster[sms_number] == nil then return nil end local args = {jid=sms_number.."@"..component_host, name=self.data.roster[sms_number].screen_name} if self.data.roster[sms_number].subscription ~= nil then args.subscription = self.data.roster[sms_number].subscription end return args end --[[ From mod_twitter, keeping 'cos I might use it later :-) function send_stanza(stanza) if stanza ~= nil then core_route_stanza(prosody.hosts[component_host], stanza) end end function dmsg(jid, msg) module:log("debug", msg or "nil"); if jid ~= nil then send_stanza(st.message({to=jid, from=component_host, type='chat'}):tag("body"):text(msg or "nil"):up()); end end function substring(string, start_string, ending_string) local s_value_start, s_value_finish = nil, nil; if start_string ~= nil then _, s_value_start = string:find(start_string); if s_value_start == nil then -- error return nil; end else return nil; end if ending_string ~= nil then _, s_value_finish = string:find(ending_string, s_value_start+1); if s_value_finish == nil then -- error return nil; end else s_value_finish = string:len()+1; end return string:sub(s_value_start+1, s_value_finish-1); end --]] local http_timeout = 30; local http_queue = setmetatable({}, { __mode = "k" }); -- auto-cleaning nil elements data_cache['prosody_os'] = prosody.platform; data_cache['prosody_version'] = prosody.version; local http_headers = { ["user-Agent"] = "Prosody ("..data_cache['prosody_version'].."; "..data_cache['prosody_os']..")" --"ELinks (0.4pre5; Linux 2.4.27 i686; 80x25)", }; function http_action_callback(response, code, request, xcallback) if http_queue == nil or http_queue[request] == nil then return; end local id = http_queue[request]; http_queue[request] = nil; if xcallback == nil then dmsg(nil, "http_action_callback reports that xcallback is nil"); else xcallback(id, response, request); end return true; end function http_add_action(tid, url, method, post, fcallback) local request = http.request(url, { headers = http_headers or {}, body = "", method = method or "GET" }, function(response_body, code, response, request) http_action_callback(response_body, code, request, fcallback) end); http_queue[request] = tid; timer.add_task(http_timeout, function() http.destroy_request(request); end); return true; end -- Clickatell SMS HTTP API interaction function function clickatell_send_sms(user, number, message) module.log("info", "Clickatell API interaction function triggered"); -- Don't attempt to send an SMS with a null or empty message if message == nil or message == "" then return false; end local sms_message = sms_message_prefix..message; local clickatell_base_url = "https://api.clickatell.com/http/sendmsg"; local params = {user=user.data.username, password=user.data.password, api_id=user.data.api_id, from=user.data.source_number, to=number, text=sms_message}; local query_string = ""; for param, data in pairs(params) do --module:log("info", "Inside query constructor: "..param..data); if query_string ~= "" then query_string = query_string.."&"; end query_string = query_string..param.."="..http.urlencode(data); end local url = clickatell_base_url.."?"..query_string; module:log("info", "Clickatell SMS URL: "..url); http_add_action(message, url, "GET", params, nil); return true; end function iq_success(origin, stanza) local reply = data_cache.success; if reply == nil then reply = st.iq({type='result', from=stanza.attr.to or component_host}); data_cache.success = reply; end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; origin.send(reply); return true; end -- XMPP Service Discovery (disco) info callback -- When a disco info query comes in, returns the identity and feature -- information as per XEP-0030 function iq_disco_info(stanza) module:log("info", "Disco info triggered"); local from = {}; from.node, from.host, from.resource = jid_split(stanza.attr.from); local bjid = from.node.."@"..from.host; local reply = data_cache.disco_info; if reply == nil then --reply = st.iq({type='result', from=stanza.attr.to or component_host}):query("http://jabber.org/protocol/disco#info"); reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info"); reply:tag("identity", {category='gateway', type='sms', name=component_name}):up(); reply:tag("feature", {var="urn:xmpp:receipts"}):up(); reply:tag("feature", {var="jabber:iq:register"}):up(); reply:tag("feature", {var="http://jabber.org/protocol/rosterx"}):up(); --reply = reply:tag("feature", {var="http://jabber.org/protocol/commands"}):up(); --reply = reply:tag("feature", {var="jabber:iq:time"}):up(); --reply = reply:tag("feature", {var="jabber:iq:version"}):up(); data_cache.disco_info = reply; end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; return reply; end -- XMPP Service Discovery (disco) items callback -- When a disco info query comes in, returns the items -- information as per XEP-0030 -- (Nothing much happening here at the moment) --[[ function iq_disco_items(stanza) module:log("info", "Disco items triggered"); local reply = data_cache.disco_items; if reply == nil then reply = st.iq({type='result', from=stanza.attr.to or component_host}):query("http://jabber.org/protocol/disco#items") :tag("item", {jid='testuser'..'@'..component_host, name='SMS Test Target'}):up(); data_cache.disco_items = reply; end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; return reply; end --]] -- XMPP Register callback -- The client must register with the gateway. In this case, the gateway is -- Clickatell's http api, so we function iq_register(origin, stanza) module:log("info", "Register event triggered"); if stanza.attr.type == "get" then local reply = data_cache.registration_form; if reply == nil then reply = st.iq({type='result', from=stanza.attr.to or component_host}) :tag("query", {xmlns="jabber:iq:register"}) :tag("instructions"):text("Use the enclosed form to register."):up(); reply:tag("x", {xmlns="jabber:x:data", type='form'}); reply:tag("title"):text('SMS Gateway Registration: Clickatell'):up(); reply:tag("instructions"):text("Enter your Clickatell username, password, API ID, and a source number for your text messages in international format (phone field)"):up(); reply:tag("field", {type='hidden', var='FORM_TYPE'}) :tag("value"):text('jabber:iq:register'):up():up(); reply:tag("field", {type='text-single', label='Username', var='username'}) :tag("required"):up():up(); reply:tag("field", {type='text-private', label='Password', var='password'}) :tag("required"):up():up(); reply:tag("field", {type='text-single', label='API ID', var='api_id'}) :tag("required"):up():up(); reply:tag("field", {type='text-single', label='Source Number', var='source_number'}) :tag("required"):up():up(); data_cache.registration_form = reply; --module:log("info", "Register stanza to go: "..reply:pretty_print()); end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; origin.send(reply); elseif stanza.attr.type == "set" then local from = {}; from.node, from.host, from.resource = jid_split(stanza.attr.from); local bjid = from.node.."@"..from.host; local username, password, api_id, source_number = "", "", "", ""; local reply; for tag in stanza.tags[1].tags[1]:childtags() do -- if tag.name == "remove" then -- iq_success(origin, stanza); -- return true; -- end if tag.attr.var == "username" then username = tag.tags[1][1]; end if tag.attr.var == "password" then password = tag.tags[1][1]; end if tag.attr.var == "api_id" then api_id = tag.tags[1][1]; end if tag.attr.var == "source_number" then source_number = tag.tags[1][1]; end end if username ~= nil and password ~= nil and api_id ~= nil then users[bjid] = smsuser:register(bjid); users[bjid].data.username = username; users[bjid].data.password = password; users[bjid].data.api_id = api_id; users[bjid].data.source_number = source_number; users[bjid]:store(); end iq_success(origin, stanza); return true; end end -- XMPP Roster callback -- When the client requests the roster associated with the gateway, returns -- the users accessible via text to the client's roster function iq_roster(stanza) module:log("info", "Roster request triggered"); local from = {} from.node, from.host, from.resource = jid_split(stanza.attr.from); local from_bjid = nil; if from.node ~= nil and from.host ~= nil then from_bjid = from.node.."@"..from.host; elseif from.host ~= nil then from_bjid = from.host; end local reply = st.iq({type='result', from=stanza.attr.to or component_host}):query("") if users[from_bjid].data.roster ~= nil then for sms_number, sms_data in pairs(users[from_bjid].data.roster) do reply:tag("item", users[from_bjid]:roster_stanza_args(sms_number)):up(); end end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; return reply; end -- Roster Exchange: iq variant -- Sends sms targets to client's roster function iq_roster_push(origin, stanza) module:log("info", "Sending Roster iq"); local from = {} from.node, from.host, from.resource = jid_split(stanza.attr.from); local from_bjid = nil; if from.node ~= nil and from.host ~= nil then from_bjid = from.node.."@"..from.host; elseif from.host ~= nil then from_bjid = from.host; end reply = st.iq({to=stanza.attr.from, type='set'}); reply:tag("query", {xmlns="jabber:iq:roster"}); if users[from_bjid].data.roster ~= nil then for sms_number, sms_data in pairs(users[from_bjid].data.roster) do reply:tag("item", users[from_bjid]:roster_stanza_args(sms_number)):up(); end end origin.send(reply); end -- XMPP Presence handling function presence_stanza_handler(origin, stanza) module:log("info", "Presence handler triggered"); local to = {}; local from = {}; local pres = {}; to.node, to.host, to.resource = jid_split(stanza.attr.to); from.node, from.host, from.resource = jid_split(stanza.attr.from); pres.type = stanza.attr.type; for _, tag in ipairs(stanza.tags) do pres[tag.name] = tag[1]; end local from_bjid = nil; if from.node ~= nil and from.host ~= nil then from_bjid = from.node.."@"..from.host; elseif from.host ~= nil then from_bjid = from.host; end local to_bjid = nil if to.node ~= nil and to.host ~= nil then to_bjid = to.node.."@"..to.host end if to.node == nil then -- Component presence -- If the client is subscribing, send a 'subscribed' presence if pres.type == 'subscribe' then origin.send(st.presence({to=from_bjid, from=component_host, type='subscribed'})); --origin.send(st.presence{to=from_bjid, type='subscribed'}); end -- The component itself is online, so send component's presence origin.send(st.presence({to=from_bjid, from=component_host})); -- Do roster item exchange: send roster items to client iq_roster_push(origin, stanza); else -- SMS user presence if pres.type == 'subscribe' then users[from_bjid]:roster_add(to.node); origin.send(st.presence({to=from_bjid, from=to_bjid, type='subscribed'})); end if pres.type == 'unsubscribe' then users[from_bjid]:roster_update_subscription(to.node, 'none'); iq_roster_push(origin, stanza); origin.send(st.presence({to=from_bjid, from=to_bjid, type='unsubscribed'})); users[from_bjid]:roster_delete(to.node) end if users[from_bjid].data.roster[to.node] ~= nil then origin.send(st.presence({to=from_bjid, from=to_bjid})); end end return true; end --[[ Not using this ATM function confirm_message_delivery(event) local reply = st.message({id=event.stanza.attr.id, to=event.stanza.attr.from, from=event.stanza.attr.to or component_host}):tag("received", {xmlns = "urn:xmpp:receipts"}); origin.send(reply); return true; end --]] -- XMPP Message handler - this is the bit that Actually Does Things (TM) -- bjid = base JID i.e. without resource identifier function message_stanza_handler(origin, stanza) module:log("info", "Message handler triggered"); local to = {}; local from = {}; local msg = {}; to.node, to.host, to.resource = jid_split(stanza.attr.to); from.node, from.host, from.resource = jid_split(stanza.attr.from); local bjid = nil; if from.node ~= nil and from.host ~= nil then from_bjid = from.node.."@"..from.host; elseif from.host ~= nil then from_bjid = from.host; end local to_bjid = nil; if to.node ~= nil and to.host ~= nil then to_bjid = to.node.."@"..to.host; elseif to.host ~= nil then to_bjid = to.host; end -- This bit looks like it confirms message receipts to the client for _, tag in ipairs(stanza.tags) do msg[tag.name] = tag[1]; if tag.attr.xmlns == "urn:xmpp:receipts" then confirm_message_delivery({origin=origin, stanza=stanza}); end -- can handle more xmlns end -- Now parse the message if stanza.attr.to == component_host then -- Messages directly to the component jget echoed origin.send(st.message({to=stanza.attr.from, from=component_host, type='chat'}):tag("body"):text(msg.body):up()); elseif users[from_bjid].data.roster[to.node] ~= nil then -- If message contains a body, send message to SMS Test User if msg.body ~= nil then clickatell_send_sms(users[from_bjid], to.node, msg.body); end end return true; end --]] -- Component event handler function sms_event_handler(event) local origin, stanza = event.origin, event.stanza; module:log("debug", "Received stanza: "..stanza:pretty_print()); local to_node, to_host, to_resource = jid_split(stanza.attr.to); -- Handle component internals (stanzas directed to component host, mainly iq stanzas) if to_node == nil then local type = stanza.attr.type; if type == "error" or type == "result" then return; end if stanza.name == "presence" then presence_stanza_handler(origin, stanza); end if stanza.name == "iq" and type == "get" then local xmlns = stanza.tags[1].attr.xmlns if xmlns == "http://jabber.org/protocol/disco#info" then origin.send(iq_disco_info(stanza)); return true; --[[ elseif xmlns == "http://jabber.org/protocol/disco#items" then origin.send(iq_disco_items(stanza)); return true; --]] elseif xmlns == "jabber:iq:register" then iq_register(origin, stanza); return true; end elseif stanza.name == "iq" and type == "set" then local xmlns = stanza.tags[1].attr.xmlns if xmlns == "jabber:iq:roster" then origin.send(iq_roster(stanza)); elseif xmlns == "jabber:iq:register" then iq_register(origin, stanza); return true; end end end -- Handle presence (both component and SMS users) if stanza.name == "presence" then presence_stanza_handler(origin, stanza); end -- Handle messages (both component and SMS users) if stanza.name == "message" then message_stanza_handler(origin, stanza); end end -- Prosody hooks: links our handler functions with the relevant events --module:hook("presence/host", presence_stanza_handler); --module:hook("message/host", message_stanza_handler); --module:hook("iq/host/jabber:iq:register:query", iq_register); module:add_feature("http://jabber.org/protocol/disco#info"); module:add_feature("http://jabber.org/protocol/disco#items"); --module:hook("iq/self/http://jabber.org/protocol/disco#info:query", iq_disco_info); --module:hook("iq/host/http://jabber.org/protocol/disco#items:query", iq_disco_items); --module:hook("account-disco-info", iq_disco_info); --module:hook("account-disco-items", iq_disco_items); --[[ module:hook("iq/host", function(data) -- IQ to a local host recieved local origin, stanza = data.origin, data.stanza; if stanza.attr.type == "get" or stanza.attr.type == "set" then return module:fire_event("iq/host/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data); else module:fire_event("iq/host/"..stanza.attr.id, data); return true; end end); --]] -- Component registration hooks: these hook in with the Prosody component -- manager module:hook("iq/bare", sms_event_handler); module:hook("message/bare", sms_event_handler); module:hook("presence/bare", sms_event_handler); module:hook("iq/full", sms_event_handler); module:hook("message/full", sms_event_handler); module:hook("presence/full", sms_event_handler); module:hook("iq/host", sms_event_handler); module:hook("message/host", sms_event_handler); module:hook("presence/host", sms_event_handler); prosody-modules-c53cc1ae4788/mod_alias/0002755000175500017550000000000013164216767017544 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_alias/mod_alias_postfixadmin.sh0000644000175500017550000000154013163400720024572 0ustar debacledebacle#!/bin/sh # Copyright (C) 2015 Travis Burtrum # This file is MIT/X11 licensed. # run like ./mod_alias_postfixadmin.sh "mysql -N -upostfixadmin -ppostfixadmin postfixadmin" > /etc/prosody/aliases.cfg.lua # then put: # Include "aliases.cfg.lua" # in prosody.cfg.lua mysql="$1" echo "-- alias plugin, generated by mod_alias_postfixadmin.sh" echo "aliases = {" echo "SELECT concat('["'"'"', address, '"'"'"] = "'"'"', goto, '"'"'";') FROM alias WHERE address != goto; SELECT concat('["'"'"', address, '"'"'"] = "'"'"', goto, '"'"'";') FROM ( select replace(address, concat('@', target_domain), concat('@', alias_domain)) as address, goto FROM alias JOIN alias_domain ON alias_domain.target_domain = SUBSTRING(alias.address, locate('@',alias.address) + 1, length(alias.address)) ) a WHERE a.address != a.goto;" | $mysql | sort | uniq echo "}" prosody-modules-c53cc1ae4788/mod_alias/mod_alias.lua0000644000175500017550000000264313163400720022161 0ustar debacledebacle-- Copyright (C) 2015 Travis Burtrum -- This file is MIT/X11 licensed. -- set like so in prosody config, works on full or bare jids, or hosts: --aliases = { -- ["old@example.net"] = "new@example.net"; -- ["you@example.com"] = "you@example.net"; -- ["conference.example.com"] = "conference.example.net"; --} local aliases = module:get_option("aliases", {}); local alias_response = module:get_option("alias_response", "User $alias can be contacted at $target"); local st = require "util.stanza"; function handle_alias(event) if event.stanza.attr.type ~= "error" then local alias = event.stanza.attr.to; local target = aliases[alias]; if target then local replacements = { alias = alias, target = target }; local error_message = alias_response:gsub("%$([%w_]+)", function (v) return replacements[v] or nil; end); local message = st.message{ type = "chat", from = alias, to = event.stanza.attr.from }:tag("body"):text(error_message); module:send(message); return event.origin.send(st.error_reply(event.stanza, "cancel", "gone", error_message)); end end end module:hook("message/bare", handle_alias, 300); module:hook("message/full", handle_alias, 300); module:hook("message/host", handle_alias, 300); module:hook("presence/bare", handle_alias, 300); module:hook("presence/full", handle_alias, 300); module:hook("presence/host", handle_alias, 300); prosody-modules-c53cc1ae4788/mod_alias/README.markdown0000644000175500017550000000326013163400720022223 0ustar debacledebacle--- summary: Point alias accounts or domains to correct XMPP user ... Introduction ============ This module allows you to set up aliases that alert people who try to contact them or add them to their roster what your actual JID is. This is useful for changing JIDs, or just in the case where you own both example.com and example.net, and want people who contact you@example.com to be alerted to contact you at you@example.net instead. This type of aliasing is well supported in the email world, but very hard to handle with XMPP, this module sidesteps all the hard problems by just sending the user a helpful message, requiring humans to decide what they actually want to do. This doesn't require any special support on other clients or servers, just the ability to recieve messages. Configuration ============= Add the module to the `modules_enabled` list. modules_enabled = { ... "alias"; } Then set up your list of aliases, aliases can be full or bare JIDs, or hosts: aliases = { ["old@example.net"] = "new@example.net"; ["you@example.com"] = "you@example.net"; ["conference.example.com"] = "conference.example.net"; } You can also set up a custom response, by default it is: alias_response = "User $alias can be contacted at $target"; A script named mod_alias_postfixadmin.sh is included in this directory to generate the aliases array directly from a postfixadmin MySQL database. Instructions for use are included in the script. Compatibility ============= ------- -------------- trunk Works 0.10 Works 0.9 Unknown 0.8 Unknown ------- -------------- prosody-modules-c53cc1ae4788/mod_delay/0002755000175500017550000000000013164216767017551 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_delay/README.markdown0000644000175500017550000000122113163400720022223 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: Add "XEP-0203 Delayed Delivery"-tags to every message stanza ... Introduction ============ This module adds "Delayed Delivery"-tags to every message stanza passing the server containing the current time on that server. This adds accurate message timings even when the message is delayed by slow networks on the receiving client or by any event. Compatibility ============= ----- ----------------------------------------------------- 0.10 Works ----- ----------------------------------------------------- Clients ======= Clients that support XEP-0203 (among others): - Gajim - Conversations - Yaxim prosody-modules-c53cc1ae4788/mod_delay/mod_delay.lua0000644000175500017550000000224613163400720022172 0ustar debacledebaclelocal add_filter = require "util.filters".add_filter; local remove_filter = require "util.filters".remove_filter; local datetime = require "util.datetime"; local xmlns_delay = "urn:xmpp:delay"; -- Raise an error if the modules has been loaded as a component in prosody's config if module:get_host_type() == "component" then error(module.name.." should NOT be loaded as a component, check out http://prosody.im/doc/components", 0); end local add_delay = function(stanza, session) if stanza and stanza.name == "message" and stanza:get_child("delay", xmlns_delay) == nil then -- only add delay tag to chat or groupchat messages (should we add a delay to anything else, too???) if stanza.attr.type == "chat" or stanza.attr.type == "groupchat" then -- session.log("debug", "adding delay to message %s", tostring(stanza)); stanza = stanza:tag("delay", { xmlns = xmlns_delay, from = session.host, stamp = datetime.datetime()}); end end return stanza; end module:hook("resource-bind", function(event) add_filter(event.session, "stanzas/in", add_delay, 1); end); module:hook("pre-resource-unbind", function (event) remove_filter(event.session, "stanzas/in", add_delay); end); prosody-modules-c53cc1ae4788/mod_s2s_auth_fingerprint/0002755000175500017550000000000013164216767022612 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_s2s_auth_fingerprint/README.markdown0000644000175500017550000000212513163400720025270 0ustar debacledebacle--- labels: - 'Stage-Alpha' - 'Type-S2SAuth' summary: Fingerprint based s2s authentication ... Introduction ============ This module allows you to manually pin certificate fingerprints of remote servers. Details ======= Servers not listed in the configuration are not affected. Configuration ============= After installing and enabling this module, you can put fingerprints of remote servers in your config like this: ``` {.lua} s2s_auth_fingerprint_digest = "sha1" -- This is the default. Other options are "sha256" and "sha512" s2s_trusted_fingerprints = { ["jabber.org"] = "11:C2:3D:87:3F:95:F8:13:F8:CA:81:33:71:36:A7:00:E0:01:95:ED"; ["matthewwild.co.uk"] = { "FD:7F:B2:B9:4C:C4:CB:E2:E7:48:FB:0D:98:11:C7:D8:4D:2A:62:AA"; "CF:F3:EC:43:A9:D5:D1:4D:D4:57:09:55:52:BC:5D:73:06:1A:A1:A0"; }; } -- If you don't want to fall back to dialback, you can list the domains s2s_secure_domains too s2s_secure_domains = { "jabber.org"; } ``` Compatibility ============= ------- -------------- trunk Works 0.9 Works 0.8 Doesn't work ------- -------------- prosody-modules-c53cc1ae4788/mod_s2s_auth_fingerprint/mod_s2s_auth_fingerprint.lua0000644000175500017550000000254513163400720030276 0ustar debacledebacle-- Copyright (C) 2013-2014 Kim Alvefur -- This file is MIT/X11 licensed. module:set_global(); local digest_algo = module:get_option_string(module:get_name().."_digest", "sha1"); local fingerprints = {}; local function hashprep(h) return tostring(h):gsub(":",""):lower(); end local function hashfmt(h) return h:gsub("..","%0:", #h/2-1):upper(); end for host, set in pairs(module:get_option("s2s_trusted_fingerprints", {})) do local host_set = {} if type(set) == "table" then -- list of fingerprints for i=1,#set do host_set[hashprep(set[i])] = true; end else -- assume single fingerprint host_set[hashprep(set)] = true; end fingerprints[host] = host_set; end module:hook("s2s-check-certificate", function(event) local session, host, cert = event.session, event.host, event.cert; local host_fingerprints = fingerprints[host]; if host_fingerprints then local digest = cert and cert:digest(digest_algo); if host_fingerprints[digest] then module:log("info", "'%s' matched %s fingerprint %s", host, digest_algo:upper(), hashfmt(digest)); session.cert_chain_status = "valid"; session.cert_identity_status = "valid"; return true; else module:log("warn", "'%s' has unknown %s fingerprint %s", host, digest_algo:upper(), hashfmt(digest)); session.cert_chain_status = "invalid"; session.cert_identity_status = "invalid"; end end end); prosody-modules-c53cc1ae4788/mod_saslname/0002755000175500017550000000000013164216767020256 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_saslname/mod_saslname.lua0000644000175500017550000000054713163400720023406 0ustar debacledebaclelocal hostname = module:get_option_string("sasl_hostname", module.host); module:hook("stream-features", function(event) local features = event.features; local mechs = features:get_child("mechanisms", "urn:ietf:params:xml:ns:xmpp-sasl"); if mechs then mechs:tag("hostname", { xmlns = "urn:xmpp:domain-based-name:1" }) :text(hostname):up(); end end); prosody-modules-c53cc1ae4788/mod_saslname/README.markdown0000644000175500017550000000066213163400720022740 0ustar debacledebacle--- labels: - 'Stage-Stable' - 'Type-Auth' summary: 'XEP-0233: XMPP Server Registration for use with Kerberos V5' ... Introduction ============ This module implements a manual method for advertsing the Kerberos principal name as per [XEP-0233]. It could be used in conjection with a Kerberos authentication module. Configuration ============= sasl_hostname = "auth42.us.example.com" Compatibility ============= Prosody 0.7+ prosody-modules-c53cc1ae4788/mod_smacks_offline/0002755000175500017550000000000013164216767021436 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_smacks_offline/mod_smacks_offline.lua0000644000175500017550000000214513163400720025742 0ustar debacledebaclelocal t_insert = table.insert; local mod_smacks = module:depends"smacks" local function store_unacked_stanzas(session) local queue = session.outgoing_stanza_queue; local replacement_queue = {}; session.outgoing_stanza_queue = replacement_queue; for _, stanza in ipairs(queue) do if stanza.name == "message" and stanza.attr.xmlns == nil and ( stanza.attr.type == "chat" or ( stanza.attr.type or "normal" ) == "normal" ) then module:fire_event("message/offline/handle", { origin = session, stanza = stanza } ) else t_insert(replacement_queue, stanza); end end end local handle_unacked_stanzas = mod_smacks.handle_unacked_stanzas; local host_sessions = prosody.hosts[module.host].sessions; mod_smacks.handle_unacked_stanzas = function (session) if session.username then local sessions = host_sessions[session.username].sessions; if next(sessions) == session.resource and next(sessions, session.resource) == nil then store_unacked_stanzas(session) end end return handle_unacked_stanzas(session); end function module.unload() mod_smacks.handle_unacked_stanzas = handle_unacked_stanzas; end prosody-modules-c53cc1ae4788/mod_muc_block_pm/0002755000175500017550000000000013164216767021105 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_muc_block_pm/README.markdown0000644000175500017550000000103613163400720023563 0ustar debacledebacle--- summary: Prevent unaffiliated MUC participants from sending PMs --- # Introduction This module prevents unaffiliated users from sending private messages in chat rooms, unless someone with an affiliation (member, admin etc) messages them first. # Configuration The module doesn't have any options, just load it onto a MUC component. ``` lua Component "muc" modules_enabled = { "muc_block_pm"; } ``` # Compatibility Branch State -------- ----------------- 0.9 Works 0.10 Should work trunk *Does not work* prosody-modules-c53cc1ae4788/mod_muc_block_pm/mod_muc_block_pm.lua0000644000175500017550000000134713163400720025063 0ustar debacledebaclelocal bare_jid = require"util.jid".bare; local st = require"util.stanza"; local muc_rooms = module:depends"muc".rooms; module:hook("message/full", function(event) local stanza, origin = event.stanza, event.origin; local to, from = stanza.attr.to, stanza.attr.from; local room = muc_rooms[bare_jid(to)]; local to_occupant = room and room._occupants[to]; local from_occupant = room and room._occupants[room._jid_nick[from]] if not ( to_occupant and from_occupant ) then return end if from_occupant.affiliation then to_occupant._pm_block_override = true; elseif not from_occupant._pm_block_override then origin.send(st.error_reply(stanza, "cancel", "not-authorized", "Private messages are disabled")); return true; end end, 1); prosody-modules-c53cc1ae4788/mod_log_events/0002755000175500017550000000000013164216767020620 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_log_events/mod_log_events.lua0000644000175500017550000000056013163400720024305 0ustar debacledebaclemodule:set_global(); local helpers = require "util.helpers"; local function init(module, events, name) helpers.log_events(events, name, module._log); function module.unload() helpers.revert_log_events(events); end end init(module, prosody.events, "global"); function module.add_host(module) init(module, prosody.hosts[module.host].events, module.host); end prosody-modules-c53cc1ae4788/mod_spam_reporting/0002755000175500017550000000000013164216767021504 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_spam_reporting/mod_spam_reporting.lua0000644000175500017550000000204613163400720026056 0ustar debacledebacle-- XEP-0377: Spam Reporting for Prosody -- Copyright (C) -2016 Kim Alvefur -- -- This file is MIT/X11 licensed. local jid_prep = require "util.jid".prep; module:depends("blocklist"); module:add_feature("urn:xmpp:reporting:0"); module:add_feature("urn:xmpp:reporting:reason:spam:0"); module:add_feature("urn:xmpp:reporting:reason:abuse:0"); module:hook("iq-set/self/urn:xmpp:blocking:block", function (event) for item in event.stanza.tags[1]:childtags("item") do local report = item:get_child("report", "urn:xmpp:reporting:0"); local jid = jid_prep(item.attr.jid); if report and jid then local type = report:get_child("spam") and "spam" or report:get_child("abuse") and "abuse" or "unknown"; local reason = report:get_child_text("text") or "no reason given"; module:log("warn", "Received report of %s from JID '%s', %s", type, jid, reason); module:fire_event(module.name.."/"..type.."-report", { origin = event.origin, stanza = event.stanza, jid = jid, item = item, report = report, reason = reason, }); end end end, 1); prosody-modules-c53cc1ae4788/mod_spam_reporting/README.markdown0000644000175500017550000000045613163400720024167 0ustar debacledebacle--- depends: - 'mod\_blocklist' labels: - 'Stage-Beta' summary: 'XEP-0377: Spam Reporting' --- This module is a very basic implementation of [XEP-0377: Spam Reporting]. When someone reports spam or abuse, a line about this is logged and an event is fired so that other modules can act on the report. prosody-modules-c53cc1ae4788/mod_register_redirect/0002755000175500017550000000000013164216767022160 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_register_redirect/README.markdown0000644000175500017550000000247713163400720024650 0ustar debacledebacle--- labels: - 'Stage-Stable' summary: 'XEP-077 IBR Registration Redirect.' ... Introduction ------------ Registration Redirect as explained in the [IBR XEP](http://xmpp.org/extensions/xep-0077.html#redirect). Details ------- This module shows instructions on how to register to the server, should it be necessary to perform it through other means Out-Of-Band or not, and also let's registrations origining from ip addresses in the whitelist to go through normally. Usage ----- Copy the module file into your Prosody modules directory. The module will work "out of the box" as long as at least an admin entry is specified (see admins = {} option into prosody's documentation).These are the optional parameters you can specify into your global server/hostname configuration: registration_whitelist = { "*your whitelisted web server ip address*" } registrarion_url = "*your web registration page url*" registration_text = "Your custom instructions banner here" registration_oob = true (default) or false, in the case there's no applicable OOB method (e.g. the server admins needs to be contacted by phone) To not employ any whitelisting (i.e. registration is handled externally). no_registration_whitelist = true Compatibility ------------- 0.9 works 0.8 works 0.7 might not work 0.6 won't work 0.5 won't work prosody-modules-c53cc1ae4788/mod_register_redirect/mod_register_redirect.lua0000644000175500017550000000773713163400720027222 0ustar debacledebacle-- (C) 2010-2011 Marco Cirillo (LW.Org) -- (C) 2011 Kim Alvefur -- -- Registration Redirect module for Prosody -- -- Redirects IP addresses not in the whitelist to a web page or another method to complete the registration. local st = require "util.stanza" local cman = configmanager local ip_wl = module:get_option_set("registration_whitelist", { "127.0.0.1", "::1" }) local url = module:get_option_string("registration_url", nil) local inst_text = module:get_option_string("registration_text", nil) local oob = module:get_option_boolean("registration_oob", true) local admins_g = cman.get("*", "core", "admins") local admins_l = cman.get(module:get_host(), "core", "admins") local no_wl = module:get_option_boolean("no_registration_whitelist", false) if type(admins_g) ~= "table" then admins_g = nil end if type(admins_l) ~= "table" then admins_l = nil end function reg_redirect(event) local stanza, origin = event.stanza, event.origin if not no_wl and ip_wl:contains(origin.ip) then return; end -- perform checks to set default responses and sanity checks. if not inst_text then if url and oob then if url:match("^%w+[:].*$") then if url:match("^(%w+)[:].*$") == "http" or url:match("^(%w+)[:].*$") == "https" then inst_text = "Please visit "..url.." to register an account on this server." elseif url:match("^(%w+)[:].*$") == "mailto" then inst_text = "Please send an e-mail at "..url:match("^%w+[:](.*)$").." to register an account on this server." elseif url:match("^(%w+)[:].*$") == "xmpp" then inst_text = "Please contact "..module:get_host().."'s server administrator via xmpp to register an account on this server at: "..url:match("^%w+[:](.*)$") else module:log("error", "This module supports only http/https, mailto or xmpp as URL formats.") module:log("error", "If you want to use personalized instructions without an Out-Of-Band method,") module:log("error", "specify: register_oob = false; -- in your configuration along your banner string (register_text).") return origin.send(st.error_reply(stanza, "wait", "internal-server-error")) -- bouncing request. end else module:log("error", "Please check your configuration, the URL you specified is invalid") return origin.send(st.error_reply(stanza, "wait", "internal-server-error")) -- bouncing request. end else if admins_l then local ajid; for _,v in ipairs(admins_l) do ajid = v ; break end inst_text = "Please contact "..module:get_host().."'s server administrator via xmpp to register an account on this server at: "..ajid else if admins_g then local ajid; for _,v in ipairs(admins_g) do ajid = v ; break end inst_text = "Please contact "..module:get_host().."'s server administrator via xmpp to register an account on this server at: "..ajid else module:log("error", "Please be sure to, _at the very least_, configure one server administrator either global or hostwise...") module:log("error", "if you want to use this module, or read it's configuration wiki at: http://code.google.com/p/prosody-modules/wiki/mod_register_redirect") return origin.send(st.error_reply(stanza, "wait", "internal-server-error")) -- bouncing request. end end end elseif inst_text and url and oob then if not url:match("^%w+[:].*$") then module:log("error", "Please check your configuration, the URL specified is not valid.") return origin.send(st.error_reply(stanza, "wait", "internal-server-error")) -- bouncing request. end end -- Prepare replies. local reply = st.reply(event.stanza) if oob then reply:query("jabber:iq:register") :tag("instructions"):text(inst_text):up() :tag("x", {xmlns = "jabber:x:oob"}) :tag("url"):text(url); else reply:query("jabber:iq:register") :tag("instructions"):text(inst_text):up() end if stanza.attr.type == "get" then return origin.send(reply) else return origin.send(st.error_reply(stanza, "cancel", "not-authorized")) end end module:hook("stanza/iq/jabber:iq:register:query", reg_redirect, 10) prosody-modules-c53cc1ae4788/mod_s2s_keysize_policy/0002755000175500017550000000000013164216767022304 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_s2s_keysize_policy/mod_s2s_keysize_policy.lua0000644000175500017550000000274713163400720027466 0ustar debacledebacle-- mod_s2s_keysize_policy.lua -- Requires LuaSec with this patch: https://github.com/brunoos/luasec/pull/12 module:set_global(); local datetime_parse = require"util.datetime".parse; local pat = "^([JFMAONSD][ceupao][glptbvyncr]) ?(%d%d?) (%d%d):(%d%d):(%d%d) (%d%d%d%d) GMT$"; local months = {Jan=1,Feb=2,Mar=3,Apr=4,May=5,Jun=6,Jul=7,Aug=8,Sep=9,Oct=10,Nov=11,Dec=12}; local function parse_x509_datetime(s) local month, day, hour, min, sec, year = s:match(pat); month = months[month]; return datetime_parse(("%04d-%02d-%02dT%02d:%02d:%02dZ"):format(year, month, day, hour, min, sec)); end local weak_key_cutoff = datetime_parse("2014-01-01T00:00:00Z"); -- From RFC 4492 local weak_key_size = { RSA = 2048, DSA = 2048, DH = 2048, EC = 233, } module:hook("s2s-check-certificate", function(event) local host, session, cert = event.host, event.session, event.cert; if cert and cert.pubkey then local _, key_type, key_size = cert:pubkey(); if key_size < ( weak_key_size[key_type] or 0 ) then local issued = parse_x509_datetime(cert:notbefore()); if issued > weak_key_cutoff then session.log("warn", "%s has a %s-bit %s key issued after 31 December 2013, invalidating trust!", host, key_size, key_type); session.cert_chain_status = "invalid"; session.cert_identity_status = "invalid"; else session.log("warn", "%s has a %s-bit %s key", host, key_size, key_type); end else session.log("info", "%s has a %s-bit %s key", host, key_size, key_type); end end end); prosody-modules-c53cc1ae4788/mod_s2s_keysize_policy/README.markdown0000644000175500017550000000222713163400720024765 0ustar debacledebacle--- summary: Distrust servers with too small keys ... Introduction ============ This module sets the security status of s2s connections to invalid if their key is too small and their certificate was issued after 2014, per CA/B Forum guidelines. Details ======= Certificate Authorities were no longer allowed to issue certificates with public keys smaller than 2048 bits (for RSA) after December 31 2013. This module was written to enforce this, as there were some CAs that were slow to comply. As of 2015, it might not be very relevant anymore, but still useful for anyone who wants to increase their security levels. When a server is determined to have a "too small" key, this module sets its chain and identity status to "invalid", so Prosody will treat it as a self-signed certificate istead. "Too small" ----------- The definition of "too small" is based on the key type and is taken from [RFC 4492]. Type bits ------ ------ RSA 2048 DSA 2048 DH 2048 EC 233 Compatibility ============= Works with Prosody 0.9 and later. Requires LuaSec with [support for inspecting public keys](https://github.com/brunoos/luasec/pull/19). prosody-modules-c53cc1ae4788/mod_json_streams/0002755000175500017550000000000013164216767021162 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_json_streams/mod_json_streams.lua0000644000175500017550000001057213163400720025215 0ustar debacledebacle-- -- XEP-0295: JSON Encodings for XMPP -- module.host = "*" local httpserver = require "net.httpserver"; local filters = require "util.filters" local json = require "util.json" local json_escapes = { ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"}; local s_char = string.char; for i=0,31 do local ch = s_char(i); if not json_escapes[ch] then json_escapes[ch] = ("\\u%.4X"):format(i); end end local state_out = 0; local state_key_before = 1; local state_key_in = 2; local state_key_escape = 3; local state_key_after = 4; local state_val_before = 5; local state_val_in = 6; local state_val_escape = 7; local state_val_after = 8; local whitespace = { [" "] = true, ["\n"] = true, ["\r"] = true, ["\t"] = true }; function json_decoder() local state = state_out; local quote; local output = ""; local buffer = ""; return function(input) for ch in input:gmatch(".") do module:log("debug", "%s | %d", ch, state) local final = false; if state == state_out then if whitespace[ch] then elseif ch ~= "{" then return nil, "{ expected"; else state = state_key_before end elseif state == state_key_before then if whitespace[ch] then elseif ch ~= "'" and ch ~= "\"" then return nil, "\" expected"; else quote = ch; state = state_key_in; end elseif state == state_key_in then if ch == quote then state = state_key_after; elseif ch ~= "s" then return nil, "invalid key, 's' expected"; -- only s as key allowed else end -- ignore key elseif state == state_key_after then if whitespace[ch] then elseif ch ~= ":" then return nil, ": expected"; else state = state_val_before; end elseif state == state_val_before then if whitespace[ch] then elseif ch ~= "'" and ch ~= "\"" then return nil, "\" expected"; else quote = ch; state = state_val_in; end elseif state == state_val_in then if ch == quote then state = state_val_after; elseif ch == "\\" then state = state_val_escape; else end elseif state == state_val_after then if whitespace[ch] then elseif ch ~= "}" then return nil, "} expected"; else state = state_out; final = true; end elseif state == state_val_escape then state = state_val_in; else module:log("error", "Unhandled state: "..state); return nil, "Unhandled state in parser" end buffer = buffer..ch; if final then module:log("debug", "%s", buffer) local tmp; pcall(function() tmp = json.decode(buffer); end); if not tmp then return nil, "Invalid JSON"; end output, buffer = output..tmp.s, ""; end end local _ = output; output = ""; return _; end; end function filter_hook(session) local determined = false; local is_json = false; local function in_filter(t) if not determined then is_json = (t:sub(1,1) == "{") and json_decoder(); determined = true; end if is_json then local s, err = is_json(t); if not err then return s; end session:close("not-well-formed"); return; end return t; end local function out_filter(t) if is_json then return '{"s":"' .. t:gsub(".", json_escapes) .. '"}'; -- encode end return t; end filters.add_filter(session, "bytes/in", in_filter, 100); filters.add_filter(session, "bytes/out", out_filter, 100); end function module.load() filters.add_filter_hook(filter_hook); end function module.unload() filters.remove_filter_hook(filter_hook); end function encode(data) if type(data) == "string" then data = json.encode({ s = data }); elseif type(data) == "table" and data.body then data.body = json.encode({ s = data.body }); data.headers["Content-Type"] = "application/json"; end return data; end function handle_request(method, body, request) local mod_bosh = modulemanager.get_module("*", "bosh") if mod_bosh then if body and method == "POST" then pcall(function() body = json.decode(body).s; end); end local _send = request.send; function request:send(data) return _send(self, encode(data)); end return encode(mod_bosh.handle_request(method, body, request)); end return "mod_bosh not loaded"; end local function setup() local ports = module:get_option("jsonstreams_ports") or { 5280 }; httpserver.new_from_config(ports, handle_request, { base = "jsonstreams" }); end if prosody.start_time then -- already started setup(); else prosody.events.add_handler("server-started", setup); end prosody-modules-c53cc1ae4788/mod_json_streams/strophe.jsonstreams.js0000644000175500017550000000515213163400720025533 0ustar debacledebacle /* jsonstreams plugin ** ** This plugin upgrades Strophe to support XEP-0295: JSON Encodings for XMPP ** */ Strophe.addConnectionPlugin('jsonstreams', { init: function (conn) { var parseXMLString = function(xmlStr) { var xmlDoc = null; if (window.ActiveXObject) { xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); xmlDoc.async=false; xmlDoc.loadXML(xmlStr); } else { var parser = new DOMParser(); xmlDoc = parser.parseFromString(xmlStr, "text/xml"); } return xmlDoc; } // replace Strophe.Request._newXHR with new jsonstreams version // if JSON is detected if (window.JSON) { var _newXHR = Strophe.Request.prototype._newXHR; Strophe.Request.prototype._newXHR = function () { var _xhr = _newXHR.apply(this, arguments); var xhr = { readyState: 0, responseText: null, responseXML: null, status: null, open: function(a, b, c) { return _xhr.open(a, b, c) }, abort: function() { _xhr.abort(); }, send: function(data) { data = JSON.stringify({"s":data}); return _xhr.send(data); } }; var req = this; xhr.onreadystatechange = this.func.bind(null, this); _xhr.onreadystatechange = function() { xhr.readyState = _xhr.readyState; if (xhr.readyState != 4) { xhr.status = 0; xhr.responseText = ""; xhr.responseXML = null; } else { xhr.status = _xhr.status; xhr.responseText = _xhr.responseText; xhr.responseXML = _xhr.responseXML; if (_xhr.responseText && !(_xhr.responseXML && _xhr.responseXML.documentElement && _xhr.responseXML.documentElement.tagName != "parsererror")) { var data = JSON.parse(_xhr.responseText); if (data && data.s) { xhr.responseText = data.s; xhr.responseXML = parseXMLString(data.s); } } } if ("function" == typeof xhr.onreadystatechange) { xhr.onreadystatechange(req); } } return xhr; }; } else { Strophe.error("jsonstreams plugin loaded, but JSON not found." + " Falling back to native XHR implementation."); } } }); prosody-modules-c53cc1ae4788/mod_json_streams/README.markdown0000644000175500017550000000276713163400720023654 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: JSON Encodings for XMPP ... Introduction ============ This plugin encodes XMPP as JSON. This is an implementation of [XEP-0295: JSON Encodings for XMPP](http://xmpp.org/extensions/xep-0295.html). Simply loading this module makes Prosody accept JSON on C2S streams (legacy XML clients are still supported). For BOSH, it requires mod\_bosh be loaded, and JSON should be directed at the `/jsonstreams` HTTP path. JSON for S2S isn't supported due to the lack of a discovery mechanism, so we have left that disabled to stay compatible with legacy XML servers. Configuration ============= Just add `"json_streams"` in your config's global `modules_enabled` list, for example: modules_enabled = { ... "json_streams"; } Strophe.js plugin ================= We also developed a [JSON streams plugin](http://prosody-modules.googlecode.com/hg/mod_json_streams/strophe.jsonstreams.js) for the popular [strophe.js](http://code.stanziq.com/strophe) library. Just include it like this after including the strophe library, and your strophe-based client will be speaking JSON: Be sure to set the HTTP path to `/jsonstreams`. No other changes are required. Compatibility ============= ------- ------- 0.8 Works trunk Works ------- ------- Quirks ====== - This plugin does not currently work with Prosody's [port multiplexing](http://prosody.im/doc/port_multiplexing) feature prosody-modules-c53cc1ae4788/mod_support_contact/0002755000175500017550000000000013164216767021702 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_support_contact/mod_support_contact.lua0000644000175500017550000000333513163400720026454 0ustar debacledebacle-- mod_support_contact.lua -- -- Config options: -- support_contact = "support@hostname"; -- a JID -- support_contact_nick = "Support!"; -- roster nick -- support_contact_group = "Users being supported!"; -- the roster group in the support contact's roster local host = module:get_host(); local support_contact = module:get_option_string("support_contact", "support@"..host); local support_contact_nick = module:get_option_string("support_contact_nick", "Support"); local support_contact_group = module:get_option_string("support_contact_group", "Users"); if not(support_contact and support_contact_nick) then return; end local rostermanager = require "core.rostermanager"; local jid_split = require "util.jid".split; local st = require "util.stanza"; module:hook("user-registered", function(event) module:log("debug", "Adding support contact"); local groups = support_contact_group and {[support_contact_group] = true;} or {}; local node, host = event.username, event.host; local jid = node and (node..'@'..host) or host; local roster; roster = rostermanager.load_roster(node, host); if hosts[host] then roster[support_contact] = {subscription = "both", name = support_contact_nick, groups = {}}; else roster[support_contact] = {subscription = "from", ask = "subscribe", name = support_contact_nick, groups = {}}; end rostermanager.save_roster(node, host, roster); node, host = jid_split(support_contact); if hosts[host] then roster = rostermanager.load_roster(node, host); roster[jid] = {subscription = "both", groups = groups}; rostermanager.save_roster(node, host, roster); rostermanager.roster_push(node, host, jid); else module:send(st.presence({from=jid, to=support_contact, type="subscribe"})); end end); prosody-modules-c53cc1ae4788/mod_support_contact/README.markdown0000644000175500017550000000265013163400720024363 0ustar debacledebacle--- labels: - 'Stage-Stable' summary: Add a support contact to new registrations ... Introduction ============ This Prosody plugin adds a default contact to newly registered accounts. Usage ===== Simply add "support\_contact" to your modules\_enabled list. When a new account is created, the new roster would be initialized to include a support contact. Configuration ============= ------------------------- -------------------------------------------------------------------------------------------------------------------------------- support\_contact The bare JID of the support contact. The default is support@hostname, where hostname is the host the new user's account is on. support\_contact\_nick Nickname of the support contact. The default is "Support". support\_contact\_group The roster group in the support contact's roster in which to add the new user. ------------------------- -------------------------------------------------------------------------------------------------------------------------------- Compatibility ============= ------ ------- 0.10 Works 0.9 Works 0.8 Works 0.7 Works 0.6 Works ------ ------- **For 0.8 and older** use [version 999a4b3e699b](http://hg.prosody.im/prosody-modules/file/999a4b3e699b/mod_support_contact/mod_support_contact.lua). Caveats/Todos/Bugs ================== - This only works for accounts created via in-band registration. prosody-modules-c53cc1ae4788/mod_delegation/0002755000175500017550000000000013164216767020566 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_delegation/README.markdown0000644000175500017550000000701013163400720023242 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: 'XEP-0355 (Namespace Delegation) implementation' ... Introduction ============ Namespace Delegation is an extension which allows server to delegate some features handling to an entity/component. Typical use case is an external PEP service, but it can be used more generally when your prefered server lack one internal feature and you found an external component which can do it. Details ======= You can have all the details by reading the [XEP-0355](http://xmpp.org/extensions/xep-0355.html). Only the admin mode is implemented so far. Usage ===== To use the module, like usual add **"delegation"** to your modules\_enabled. Note that if you use it with a local component, you also need to activate the module in your component section: modules_enabled = { [...] "delegation"; } [...] Component "youcomponent.yourdomain.tld" component_secret = "yourpassword" modules_enabled = {"delegation"} then specify delegated namespaces **in your host section** like that: VirtualHost "yourdomain.tld" delegations = { ["urn:xmpp:mam:0"] = { filtering = {"node"}; jid = "pubsub.yourdomain.tld"; }, ["http://jabber.org/protocol/pubsub"] = { jid = "pubsub.yourdomain.tld"; }, } Here all MAM requests with a "node" attribute (i.e. all MAM pubsub request) will be delegated to pubsub.yourdomain.tld. Similarly, all pubsub request to the host (i.e. the PEP requests) will be delegated to pubsub.yourdomain.tld. **/!\ Be extra careful when you give a delegation to an entity/component, it's a powerful access, only do it if you absoly trust the component/entity, and you know where the software is coming from** Configuration ============= The configuration is done with a table which map delegated namespace to namespace data. Namespace data MUST have a **jid** (in the form **jid = "delegated@domain.tld"**) and MAY have an additional **filtering** array. If filtering is present, request with attributes in the array will be delegated, other will be treated normally (i.e. by Prosody). If your are not a developper, the delegated namespace(s)/attribute(s) are most probably specified with the external component/entity you want to use. The pseudo-namespace `http://jabber.org/protocol/disco#items:*` is used to delegate remaining disco#items (i.e. items nodes not already handled by Prosody itself). Compatibility ============= If you use it with Prosody 0.9 and a component, you need to patch core/mod\_component.lua to fire a new signal. To do it, copy the following patch in a, for example, /tmp/component.patch file: diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua --- a/plugins/mod_component.lua +++ b/plugins/mod_component.lua @@ -85,6 +85,7 @@ session.type = "component"; module:log("info", "External component successfully authenticated"); session.send(st.stanza("handshake")); + module:fire_event("component-authenticated", { session = session }); return true; end Then, at the root of prosody, enter: `patch -p1 < /tmp/component.patch` ----- ---------------------------------------------------- 0.10 Works 0.9 Need a patched core/mod\_component.lua (see above) ----- ---------------------------------------------------- Note ==== This module is often used with mod\_privilege (c.f. XEP for more details) prosody-modules-c53cc1ae4788/mod_delegation/mod_delegation.lua0000644000175500017550000004777313163400720024242 0ustar debacledebacle-- XEP-0355 (Namespace Delegation) -- Copyright (C) 2015-2016 Jérôme Poisson -- -- This module is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- This module manage namespace delegation, a way to delegate server features -- to an external entity/component. Only the admin mode is implemented so far -- TODO: client mode local jid = require("util.jid") local st = require("util.stanza") local set = require("util.set") local delegation_session = module:shared("/*/delegation/session") -- FIXME: temporarily needed for disco_items_hook, to be removed when clean implementation is done local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; local jid_split = require "util.jid".split; local jid_bare = require "util.jid".bare; if delegation_session.connected_cb == nil then -- set used to have connected event listeners -- which allow a host to react on events from -- other hosts delegation_session.connected_cb = set.new() end local connected_cb = delegation_session.connected_cb local _DELEGATION_NS = 'urn:xmpp:delegation:1' local _FORWARDED_NS = 'urn:xmpp:forward:0' local _DISCO_INFO_NS = 'http://jabber.org/protocol/disco#info' local _DISCO_ITEMS_NS = 'http://jabber.org/protocol/disco#items' local _DATA_NS = 'jabber:x:data' local _MAIN_SEP = '::' local _BARE_SEP = ':bare:' local _REMAINING = ':*' local _MAIN_PREFIX = _DELEGATION_NS.._MAIN_SEP local _BARE_PREFIX = _DELEGATION_NS.._BARE_SEP local _DISCO_REMAINING = _DISCO_ITEMS_NS.._REMAINING local _PREFIXES = {_MAIN_PREFIX, _BARE_PREFIX} local disco_nest module:log("debug", "Loading namespace delegation module ") --> Configuration management <-- local ns_delegations = module:get_option("delegations", {}) local jid2ns = {} for namespace, ns_data in pairs(ns_delegations) do -- "connected" contain the full jid of connected managing entity ns_data.connected = nil if ns_data.jid then if jid2ns[ns_data.jid] == nil then jid2ns[ns_data.jid] = {} end jid2ns[ns_data.jid][namespace] = ns_data module:log("debug", "Namespace %s is delegated%s to %s", namespace, ns_data.filtering and " (with filtering)" or "", ns_data.jid) else module:log("warn", "Ignoring delegation for %s: no jid specified", tostring(namespace)) ns_delegations[namespace] = nil end end local function advertise_delegations(session, to_jid) -- send stanza to advertise delegations -- as expained in § 4.2 local message = st.message({from=module.host, to=to_jid}) :tag("delegation", {xmlns=_DELEGATION_NS}) -- we need to check if a delegation is granted because the configuration -- can be complicated if some delegations are granted to bare jid -- and other to full jids, and several resources are connected. local have_delegation = false for namespace, ns_data in pairs(jid2ns[to_jid]) do if ns_data.connected == to_jid then have_delegation = true message:tag("delegated", {namespace=namespace}) if type(ns_data.filtering) == "table" then for _, attribute in pairs(ns_data.filtering) do message:tag("attribute", {name=attribute}):up() end end message:up() end end if have_delegation then session.send(message) end end local function set_connected(entity_jid) -- set the "connected" key for all namespace managed by entity_jid -- if the namespace has already a connected entity, ignore the new one local function set_config(jid_) for namespace, ns_data in pairs(jid2ns[jid_]) do if ns_data.connected == nil then ns_data.connected = entity_jid -- disco remaining is a special namespace -- there is no disco nesting for it if namespace ~= _DISCO_REMAINING then disco_nest(namespace, entity_jid) end end end end local bare_jid = jid.bare(entity_jid) set_config(bare_jid) -- We can have a bare jid of a full jid specified in configuration -- so we try our luck with both (first connected resource will -- manage the namespaces in case of bare jid) if bare_jid ~= entity_jid then set_config(entity_jid) jid2ns[entity_jid] = jid2ns[bare_jid] end end local function on_presence(event) local session = event.origin local bare_jid = jid.bare(session.full_jid) if jid2ns[bare_jid] or jid2ns[session.full_jid] then set_connected(session.full_jid) advertise_delegations(session, session.full_jid) end end local function on_component_connected(event) -- method called by the module loaded by the component -- /!\ the event come from the component host, -- not from the host of this module local session = event.session local bare_jid = jid.join(session.username, session.host) local jid_delegations = jid2ns[bare_jid] if jid_delegations ~= nil then set_connected(bare_jid) advertise_delegations(session, bare_jid) end end local function on_component_auth(event) -- react to component-authenticated event from this host -- and call the on_connected methods from all other hosts -- needed for the component to get delegations advertising for callback in connected_cb:items() do callback(event) end end if module:get_host_type() ~= "component" then connected_cb:add(on_component_connected) end module:hook('component-authenticated', on_component_auth) module:hook('presence/initial', on_presence) --> delegated namespaces hook <-- local managing_ent_error local stanza_cache = {} -- we cache original stanza to build reply local function managing_ent_result(event) -- this function manage iq results from the managing entity -- it do a couple of security check before sending the -- result to the managed entity local stanza = event.stanza if stanza.attr.to ~= module.host then module:log("warn", 'forwarded stanza result has "to" attribute not addressed to current host, id conflict ?') return end module:unhook("iq-result/host/"..stanza.attr.id, managing_ent_result) module:unhook("iq-error/host/"..stanza.attr.id, managing_ent_error) -- lot of checks to do... local delegation = stanza.tags[1] if #stanza ~= 1 or delegation.name ~= "delegation" or delegation.attr.xmlns ~= _DELEGATION_NS then module:log("warn", "ignoring invalid iq result from managing entity %s", stanza.attr.from) stanza_cache[stanza.attr.from][stanza.attr.id] = nil return true end local forwarded = delegation.tags[1] if #delegation ~= 1 or forwarded.name ~= "forwarded" or forwarded.attr.xmlns ~= _FORWARDED_NS then module:log("warn", "ignoring invalid iq result from managing entity %s", stanza.attr.from) stanza_cache[stanza.attr.from][stanza.attr.id] = nil return true end local iq = forwarded.tags[1] if #forwarded ~= 1 or iq.name ~= "iq" or iq.attr.xmlns ~= 'jabber:client' or (iq.attr.type =='result' and #iq > 1) or (iq.attr.type == 'error' and #iq > 2) then module:log("warn", "ignoring invalid iq result from managing entity %s", stanza.attr.from) stanza_cache[stanza.attr.from][stanza.attr.id] = nil return true end iq.attr.xmlns = nil local original = stanza_cache[stanza.attr.from][stanza.attr.id] stanza_cache[stanza.attr.from][stanza.attr.id] = nil -- we get namespace from original and not iq -- because the namespace can be lacking in case of error local namespace = original.tags[1].attr.xmlns -- small hack for disco remaining feat if namespace == _DISCO_ITEMS_NS then namespace = _DISCO_REMAINING end local ns_data = ns_delegations[namespace] if stanza.attr.from ~= ns_data.connected or (iq.attr.type ~= "result" and iq.attr.type ~= "error") or iq.attr.id ~= original.attr.id or iq.attr.to ~= original.attr.from then module:log("warn", "ignoring forbidden iq result from managing entity %s, please check that the component is no trying to do something bad (stanza: %s)", stanza.attr.from, tostring(stanza)) module:send(st.error_reply(original, 'cancel', 'service-unavailable')) return true end -- at this point eveything is checked, -- and we (hopefully) can send the the result safely module:send(iq) return true end function managing_ent_error(event) local stanza = event.stanza if stanza.attr.to ~= module.host then module:log("warn", 'Stanza result has "to" attribute not addressed to current host, id conflict ?') return end module:unhook("iq-result/host/"..stanza.attr.id, managing_ent_result) module:unhook("iq-error/host/"..stanza.attr.id, managing_ent_error) local original = stanza_cache[stanza.attr.from][stanza.attr.id] stanza_cache[stanza.attr.from][stanza.attr.id] = nil module:log("warn", "Got an error after forwarding stanza to "..stanza.attr.from) module:send(st.error_reply(original, 'cancel', 'service-unavailable')) return true end local function forward_iq(stanza, ns_data) local to_jid = ns_data.connected stanza.attr.xmlns = 'jabber:client' local iq_stanza = st.iq({ from=module.host, to=to_jid, type="set" }) :tag("delegation", { xmlns=_DELEGATION_NS }) :tag("forwarded", { xmlns=_FORWARDED_NS }) :add_child(stanza) local iq_id = iq_stanza.attr.id -- we save the original stanza to check the managing entity result if not stanza_cache[to_jid] then stanza_cache[to_jid] = {} end stanza_cache[to_jid][iq_id] = stanza module:hook("iq-result/host/"..iq_id, managing_ent_result) module:hook("iq-error/host/"..iq_id, managing_ent_error) module:log("debug", "stanza forwarded") module:send(iq_stanza) end local function iq_hook(event) -- general hook for all the iq which forward delegated ones -- and continue normal behaviour else. If a namespace is -- delegated but managing entity is offline, a service-unavailable -- error will be sent, as requested by the XEP local session, stanza = event.origin, event.stanza if #stanza == 1 and stanza.attr.type == 'get' or stanza.attr.type == 'set' then local namespace = stanza.tags[1].attr.xmlns local ns_data = ns_delegations[namespace] if ns_data then if stanza.attr.from == ns_data.connected then -- we don't forward stanzas from managing entity itself return end if ns_data.filtering then local first_child = stanza.tags[1] for _, attribute in pairs(ns_data.filtering) do -- if any filtered attribute if not present, -- we must continue the normal bahaviour if not first_child.attr[attribute] then -- Filtered attribute is not present, we do normal workflow return end end end if not ns_data.connected then module:log("warn", "No connected entity to manage "..namespace) session.send(st.error_reply(stanza, 'cancel', 'service-unavailable')) else forward_iq(stanza, ns_data) end return true else -- we have no delegation, we continue normal behaviour return end end end module:hook("iq/self", iq_hook, 2^32) module:hook("iq/bare", iq_hook, 2^32) module:hook("iq/host", iq_hook, 2^32) --> discovery nesting <-- -- disabling internal features/identities local function find_form_type(stanza) local form_type = nil for field in stanza:childtags('field', 'jabber:x:data') do if field.attr.var=='FORM_TYPE' and field.attr.type=='hidden' then local value = field:get_child('value') if not value then module:log("warn", "No value found in FORM_TYPE field: "..tostring(stanza)) else form_type=value.get_text() end end end return form_type end -- modules whose features/identities are managed by delegation local disabled_modules = set.new() local disabled_identities = set.new() local function identity_added(event) local source = event.source if disabled_modules:contains(source) then local item = event.item local category, type_, name = item.category, item.type, item.name module:log("debug", "Removing (%s/%s%s) identity because of delegation", category, type_, name and "/"..name or "") disabled_identities:add(item) source:remove_item("identity", item) end end local function feature_added(event) local source, item = event.source, event.item for namespace, _ in pairs(ns_delegations) do if source ~= module and string.sub(item, 1, #namespace) == namespace then module:log("debug", "Removing %s feature which is delegated", item) source:remove_item("feature", item) disabled_modules:add(source) if source.items and source.items.identity then -- we remove all identities added by the source module -- that can cause issues if the module manages several features/identities -- but this case is probably rare (or doesn't happen at all) -- FIXME: any better way ? for _, identity in pairs(source.items.identity) do identity_added({source=source, item=identity}) end end end end end local function extension_added(event) local source, stanza = event.source, event.item local form_type = find_form_type(stanza) if not form_type then return end for namespace, _ in pairs(ns_delegations) do if source ~= module and string.sub(form_type, 1, #namespace) == namespace then module:log("debug", "Removing extension which is delegated: %s", tostring(stanza)) source:remove_item("extension", stanza) end end end -- for disco nesting (see § 7.2) we need to remove internal features -- we use handle_items as it allows to remove already added features -- and catch the ones which can come later module:handle_items("feature", feature_added, function(_) end) module:handle_items("identity", identity_added, function(_) end, false) module:handle_items("extension", extension_added, function(_) end) -- managing entity features/identities collection local disco_error local bare_features = set.new() local bare_identities = {} local bare_extensions = {} local function disco_result(event) -- parse result from disco nesting request -- and fill module features/identities and bare_features/bare_identities accordingly local session, stanza = event.origin, event.stanza if stanza.attr.to ~= module.host then module:log("warn", 'Stanza result has "to" attribute not addressed to current host, id conflict ?') return end module:unhook("iq-result/host/"..stanza.attr.id, disco_result) module:unhook("iq-error/host/"..stanza.attr.id, disco_error) local query = stanza:get_child("query", _DISCO_INFO_NS) if not query or not query.attr.node then session.send(st.error_reply(stanza, 'modify', 'not-acceptable')) return true end local node = query.attr.node local main if string.sub(node, 1, #_MAIN_PREFIX) == _MAIN_PREFIX then main=true elseif string.sub(node, 1, #_BARE_PREFIX) == _BARE_PREFIX then main=false else module:log("warn", "Unexpected node: "..node) session.send(st.error_reply(stanza, 'modify', 'not-acceptable')) return true end for feature in query:childtags("feature") do local namespace = feature.attr.var if main then module:add_feature(namespace) else bare_features:add(namespace) end end for identity in query:childtags("identity") do local category, type_, name = identity.attr.category, identity.attr.type, identity.attr.name if main then module:add_identity(category, type_, name) else table.insert(bare_identities, {category=category, type=type_, name=name}) end end for extension in query:childtags("x", _DATA_NS) do if main then module:add_extension(extension) else table.insert(bare_extensions, extension) end end end function disco_error(event) local stanza = event.stanza if stanza.attr.to ~= module.host then module:log("warn", 'Stanza result has "to" attribute not addressed to current host, id conflict ?') return end module:unhook("iq-result/host/"..stanza.attr.id, disco_result) module:unhook("iq-error/host/"..stanza.attr.id, disco_error) module:log("warn", "Got an error while requesting disco for nesting to "..stanza.attr.from) module:log("warn", "Ignoring disco nesting") end function disco_nest(namespace, entity_jid) -- manage discovery nesting (see § 7.2) -- first we reset the current values if module.items then module.items['feature'] = nil module.items['identity'] = nil module.items['extension'] = nil bare_features = set.new() bare_identities = {} bare_extensions = {} end for _, prefix in ipairs(_PREFIXES) do local node = prefix..namespace local iq = st.iq({from=module.host, to=entity_jid, type='get'}) :tag('query', {xmlns=_DISCO_INFO_NS, node=node}) local iq_id = iq.attr.id module:hook("iq-result/host/"..iq_id, disco_result) module:hook("iq-error/host/"..iq_id, disco_error) module:send(iq) end end -- disco to bare jids special cases -- disco#info local function disco_hook(event) -- this event is called when a disco info request is done on a bare jid -- we get the final reply and filter delegated features/identities/extensions local reply = event.reply reply.tags[1]:maptags(function(child) if child.name == 'feature' then local feature_ns = child.attr.var for namespace, _ in pairs(ns_delegations) do if string.sub(feature_ns, 1, #namespace) == namespace then module:log("debug", "Removing feature namespace %s which is delegated", feature_ns) return nil end end elseif child.name == 'identity' then for item in disabled_identities:items() do if item.category == child.attr.category and item.type == child.attr.type -- we don't check name, because mod_pep use a name for main disco, but not in account-disco-info hook -- and item.name == child.attr.name then module:log("debug", "Removing (%s/%s%s) identity because of delegation", item.category, item.type, item.name and "/"..item.name or "") return nil end end elseif child.name == 'x' and child.attr.xmlns == _DATA_NS then local form_type = find_form_type(child) if form_type then for namespace, _ in pairs(ns_delegations) do if string.sub(form_type, 1, #namespace) == namespace then module:log("debug", "Removing extension which is delegated: %s", tostring(child)) return nil end end end end return child end) for feature in bare_features:items() do reply:tag('feature', {var=feature}):up() end for _, item in ipairs(bare_identities) do reply:tag('identity', {category=item.category, type=item.type, name=item.name}):up() end for _, stanza in ipairs(bare_extensions) do reply:add_child(stanza) end end -- disco#items local function disco_items_node_hook(event) -- check if node is not handled by server -- and forward the disco request to suitable entity if not event.exists then -- this node is not handled by the server local ns_data = ns_delegations[_DISCO_REMAINING] if ns_data ~= nil then -- remaining delegation is requested, we forward forward_iq(event.stanza, ns_data) -- and stop normal event handling return true end end end module:hook("account-disco-items-node", disco_items_node_hook, -2^32) local function disco_items_hook(event) -- FIXME: we forward all bare-jid disco-items requests (without node) which will replace any Prosody reply -- for now it's OK because Prosody is not returning anything on request on bare jid -- but to be properly done, any Prosody reply should be kept and managing entities items should be added (merged) to it. -- account-disco-items can't be cancelled (return value of hooks are not checked in mod_disco), so corountine needs -- to be used with util.async (to get the IQ result, merge items then return from the event) local origin, stanza = event.origin, event.stanza; if stanza.attr.type ~= "get" then return; end local node = stanza.tags[1].attr.node; local username = jid_split(stanza.attr.to) or origin.username; if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then if node == nil or node == "" then local ns_data = ns_delegations[_DISCO_REMAINING] if ns_data ~= nil then forward_iq(event.stanza, ns_data) return true end end end end module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", disco_items_hook, 100) local function disco_items_raw_hook(event) -- this method is called when account-disco-items-* event are not called -- notably when a disco-item is done by an unsubscibed entity -- (i.e. an entity doing a disco#item on an entity without having -- presence subscription) -- we forward the request to managing entity -- it's the responsability of the managing entity to filter the items local ns_data = ns_delegations[_DISCO_REMAINING] if ns_data ~= nil then forward_iq(event.stanza, ns_data) return true end end module:hook("iq-get/bare/http://jabber.org/protocol/disco#items:query", disco_items_raw_hook, -2^32) prosody-modules-c53cc1ae4788/mod_conversejs/0002755000175500017550000000000013164216767020634 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_conversejs/mod_conversejs.lua0000644000175500017550000000172713163400720024343 0ustar debacledebacle-- mod_conversejs -- Copyright (C) 2017 Kim Alvefur local json_encode = require"util.json".encode; module:depends"bosh"; local has_ws = pcall(function () module:depends("websocket"); end); local template = [[ ]] module:provides("http", { route = { GET = function (event) event.response.headers.content_type = "text/html"; return template:format(json_encode({ -- debug = true, bosh_service_url = module:http_url("bosh","/http-bind"); websocket_url = has_ws and module:http_url("websocket","xmpp-websocket"):gsub("^http", "ws") or nil; authentication = module:get_option_string("authentication") == "anonymous" and "anonymous" or "login"; jid = module.host; })); end; } }); prosody-modules-c53cc1ae4788/mod_carbons/0002755000175500017550000000000013164216767020102 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_carbons/README.markdown0000644000175500017550000000210413163400720022555 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: Message Carbons ... Introduction ============ This module implements [XEP-0280: Message Carbons](http://xmpp.org/extensions/xep-0280.html), allowing users to maintain a shared and synchronized view of all conversations across all their online clients and devices. Configuration ============= As with all modules, you enable it by adding it to the modules\_enabled list. modules_enabled = { ... "carbons"; ... } The module has no further configuration. Clients ======= Clients that support XEP-0280: - [Gajim](http://gajim.org/) (Desktop) - [Adium (1.6)](http://adium.im/) (Desktop - OS X) - [Yaxim](http://yaxim.org/) (Mobile - Android) - [Conversations](https://play.google.com/store/apps/details?id=eu.siacs.conversations) (Mobile - Android) - [poezio](http://poezio.eu/en/) (Console) Compatibility ============= ------- ----------------------- 0.8 Works 0.9 Works 0.10 Included with prosody trunk Included with prosody ------- ----------------------- prosody-modules-c53cc1ae4788/mod_carbons/mod_carbons.lua0000644000175500017550000001223713163400720023055 0ustar debacledebacle-- XEP-0280: Message Carbons implementation for Prosody -- Copyright (C) 2011-2016 Kim Alvefur -- -- This file is MIT/X11 licensed. local st = require "util.stanza"; 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 = prosody.full_sessions, prosody.bare_sessions; local function toggle_carbons(event) local origin, stanza = event.origin, event.stanza; local state = stanza.tags[1].attr.mode or stanza.tags[1].name; module:log("debug", "%s %sd carbons", origin.full_jid, state); origin.want_carbons = state == "enable" and stanza.tags[1].attr.xmlns; origin.send(st.reply(stanza)); return true; end module:hook("iq-set/self/"..xmlns_carbons..":disable", toggle_carbons); module:hook("iq-set/self/"..xmlns_carbons..":enable", toggle_carbons); -- COMPAT module:hook("iq-set/self/"..xmlns_carbons_old..":disable", toggle_carbons); module:hook("iq-set/self/"..xmlns_carbons_old..":enable", toggle_carbons); module:hook("iq-set/self/"..xmlns_carbons_really_old..":carbons", toggle_carbons); local function message_handler(event, c2s) local origin, stanza = event.origin, event.stanza; local orig_type = stanza.attr.type or "normal"; local orig_from = stanza.attr.from; local orig_to = stanza.attr.to; if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body"))) then return -- Only chat type messages end -- Stanza sent by a local client local bare_jid = jid_bare(orig_from); local target_session = origin; local top_priority = false; local user_sessions = bare_sessions[bare_jid]; -- Stanza about to be delivered to a local client if not c2s then bare_jid = jid_bare(orig_to); target_session = full_sessions[orig_to]; user_sessions = bare_sessions[bare_jid]; if not target_session and user_sessions then -- The top resources will already receive this message per normal routing rules, -- so we are going to skip them in order to avoid sending duplicated messages. local top_resources = user_sessions.top_resources; top_priority = top_resources and top_resources[1].priority end end if not user_sessions then module:log("debug", "Skip carbons for offline user"); return -- No use in sending carbons to an offline user end if stanza:get_child("private", xmlns_carbons) then if not c2s then stanza:maptags(function(tag) if not ( tag.attr.xmlns == xmlns_carbons and tag.name == "private" ) then return tag; end end); end module:log("debug", "Message tagged private, ignoring"); return elseif stanza:get_child("no-copy", "urn:xmpp:hints") then module:log("debug", "Message has no-copy hint, ignoring"); return elseif not c2s and bare_jid == orig_from and stanza:get_child("x", "http://jabber.org/protocol/muc#user") then module:log("debug", "MUC PM, ignoring"); return end -- Create the carbon copy and wrap it as per the Stanza Forwarding XEP 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 }) :tag("forwarded", { xmlns = xmlns_forward }) :add_child(copy):reset(); -- COMPAT local carbon_old = st.message{ from = bare_jid, type = orig_type, } :tag(c2s and "sent" or "received", { xmlns = xmlns_carbons_old }):up() :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 if session.want_carbons -- 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) -- 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 -- COMPAT or session.want_carbons == xmlns_carbons_really_old and carbon_really_old -- COMPAT or carbon; session.send(carbon); end end end local function c2s_message_handler(event) return message_handler(event, true) end -- Stanzas sent by local clients module:hook("pre-message/host", c2s_message_handler, 0.05); -- priority between mod_message (0 in 0.9) and mod_firewall (0.1) module:hook("pre-message/bare", c2s_message_handler, 0.05); module:hook("pre-message/full", c2s_message_handler, 0.05); -- Stanzas to local clients module:hook("message/bare", message_handler, 0.05); module:hook("message/full", message_handler, 0.05); 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 prosody-modules-c53cc1ae4788/mod_c2s_conn_throttle/0002755000175500017550000000000013164216767022104 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_c2s_conn_throttle/mod_c2s_conn_throttle.lua0000644000175500017550000000327013163400720027056 0ustar debacledebacle-- Clients Connection Throttler. -- (C) 2012-2013, Marco Cirillo (LW.Org) local time = os.time local in_count = {} local logins_count = module:get_option_number("cthrottler_logins_count", 3) local throttle_time = module:get_option_number("cthrottler_time", 60) local function handle_sessions(event) local session = event.origin if not in_count[session.ip] and session.type == "c2s_unauthed" then in_count[session.ip] = { t = time(), c = 1 } elseif in_count[session.ip] and session.type == "c2s_unauthed" then if in_count[session.ip].starttls_c then in_count[session.ip].c = in_count[session.ip].starttls_c else in_count[session.ip].c = in_count[session.ip].c + 1 end if in_count[session.ip].c > logins_count and time() - in_count[session.ip].t < throttle_time then module:log("error", "Exceeded login count for %s, closing connection", session.ip) session:close{ condition = "policy-violation", text = "You exceeded the number of connections/logins allowed in "..throttle_time.." seconds, good bye." } return true elseif time() - in_count[session.ip].t > throttle_time then in_count[session.ip] = nil ; return end end end local function check_starttls(event) local session = event.origin if in_count[session.ip] and type(in_count[session.ip].starttls_c) ~= "number" and session.type == "c2s_unauthed" then in_count[session.ip].starttls_c = 1 elseif in_count[session.ip] and type(in_count[session.ip].starttls_c) == "number" and session.type == "c2s_unauthed" then in_count[session.ip].starttls_c = in_count[session.ip].starttls_c + 1 end end module:hook("stream-features", handle_sessions, 100) module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", check_starttls, 100) prosody-modules-c53cc1ae4788/mod_c2s_conn_throttle/README.markdown0000644000175500017550000000112013163400720024554 0ustar debacledebacle--- labels: - 'Stage-Stable' summary: c2s connections throttling module ... Introduction ============ This module allows to throttle those client connections which exceed a n\*seconds limit. Usage ===== Copy the module folder into your prosody modules directory. Place the module between your enabled modules either into the global or a vhost section. Optional configuration directives: ``` {.lua} cthrottler_logins_count = 3 -- number of login attempts allowed, default is 3 cthrottler_time = 60 -- .. in number of seconds, default is 60 ``` Info ==== - 0.8, works - 0.9, works prosody-modules-c53cc1ae4788/mod_auth_pam/0002755000175500017550000000000013164216767020251 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_auth_pam/mod_auth_pam.lua0000644000175500017550000000145313163400720023371 0ustar debacledebacle-- PAM authentication for Prosody -- Copyright (C) 2013 Kim Alvefur -- -- Requires https://github.com/devurandom/lua-pam -- and LuaPosix local posix = require "posix"; local pam = require "pam"; local new_sasl = require "util.sasl".new; function user_exists(username) return not not posix.getpasswd(username); end function test_password(username, password) local h, err = pam.start("xmpp", username, { function (t) if #t == 1 and t[1][1] == pam.PROMPT_ECHO_OFF then return { { password, 0} }; end end }); if h and h:authenticate() and h:endx(pam.SUCCESS) then return user_exists(username), true; end return nil, true; end function get_sasl_handler() return new_sasl(module.host, { plain_test = function(sasl, ...) return test_password(...) end }); end module:provides"auth"; prosody-modules-c53cc1ae4788/mod_auth_pam/README.markdown0000644000175500017550000000134113163400720022726 0ustar debacledebacle--- labels: - 'Stage-Alpha' - 'Type-Auth' summary: PAM authentication module --- Introduction ============ This module makes Prosody authenticate users against PAM (Linux Pluggable Authentication Modules) Dependencies ============ The module depends on [lua-pam](https://github.com/devurandom/lua-pam) and [LuaPosix](https://github.com/luaposix/luaposix). Setup ===== Create a `/etc/pam.d/xmpp` with something like this: auth [success=1 default=ignore] pam_unix.so obscure sha512 nodelay auth requisite pam_deny.so auth required pam_permit.so And switch authentication provider in the Prosody config: authentication = "pam" Compatibility ============= Compatible with 0.9 and up prosody-modules-c53cc1ae4788/mod_csi_compat/0002755000175500017550000000000013164216767020574 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_csi_compat/mod_csi_compat.lua0000644000175500017550000000124213163400720024233 0ustar debacledebaclelocal st = require "util.stanza"; module:depends("csi"); module:add_feature("google:queue"); module:hook("iq-set/self/google:queue:query", function(event) local origin, stanza = event.origin, event.stanza; (origin.log or module._log)("debug", "Google queue invoked (CSI compat mode)") local payload = stanza:get_child("query", "google:queue"); if payload:get_child("enable") then module:fire_event("csi-client-inactive", event); elseif payload:get_child("disable") then module:fire_event("csi-client-active", event); end -- is implemented as a noop, any IQ stanza would flush the queue anyways. origin.send(st.reply(stanza)); return true; end, 10); prosody-modules-c53cc1ae4788/mod_csi_compat/README.markdown0000644000175500017550000000062713163400720023257 0ustar debacledebacle--- labels: summary: 'Implement the google:queue protocol and map to mod\_csi events' ... Introduction ============ This module implements the google:queue protocol and maps it to [mod\_csi](mod_csi.html) events. Configuration ============= There is no configuration for this module, just add it to modules\_enabled as normal. Compatibility ============= ----- ------- 0.9 Works ----- ------- prosody-modules-c53cc1ae4788/mod_sift/0002755000175500017550000000000013164216767017420 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_sift/README.markdown0000644000175500017550000000146213163400720022101 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: 'XEP-0273: Stanza Interception and Filtering Technology' ... Introduction ============ [SIFT][XEP-0273] is a technology to allow clients to filter incoming traffic on the server. This helps save bandwidth, etc. Compatibility ============= ----- ------- 0.7 Works ----- ------- Quirks ====== This implementation is a work in progress. - Stanzas to full JIDs get sifted correctly - Stanzas to bare JIDs are currently allowed/disallowed for all resources as a whole, and not for individual resources - Presence is only sent to available resources, and probes are not sent for unavailable reasources - This module currently does not interact with offline messages (filtered messages are dropped with an error reply) - Not tested with privacy lists prosody-modules-c53cc1ae4788/mod_sift/mod_sift.lua0000644000175500017550000001446013163400720021711 0ustar debacledebacle local st = require "util.stanza"; local jid_bare = require "util.jid".bare; -- advertise disco features module:add_feature("urn:xmpp:sift:1"); -- supported features module:add_feature("urn:xmpp:sift:stanzas:iq"); module:add_feature("urn:xmpp:sift:stanzas:message"); module:add_feature("urn:xmpp:sift:stanzas:presence"); module:add_feature("urn:xmpp:sift:recipients:all"); module:add_feature("urn:xmpp:sift:senders:all"); -- allowed values of 'sender' and 'recipient' attributes local senders = { ["all"] = true; ["local"] = true; ["others"] = true; ["remote"] = true; ["self"] = true; }; local recipients = { ["all"] = true; ["bare"] = true; ["full"] = true; }; -- this function converts a , or element in -- the SIFT namespace into a hashtable, for easy lookup local function to_hashtable(element) if element ~= nil then local hash = {}; -- make sure the sender and recipient attributes has a valid value hash.sender = element.attr.sender or "all"; if not senders[hash.sender] then return false; end -- bad value, returning false hash.recipient = element.attr.recipient or "all"; if not recipients[hash.recipient] then return false; end -- bad value, returning false -- next we loop over all elements for _, tag in ipairs(element) do if tag.name == "allow" and tag.attr.xmlns == "urn:xmpp:sift:1" then -- make sure the element is valid if not tag.attr.name or not tag.attr.ns then return false; end -- missing required attributes, returning false hash[tag.attr.ns.."|"..tag.attr.name] = true; hash.allowed = true; -- just a flag indicating we have some elements allowed end end return hash; end end local data = {}; -- table with all our data -- handle SIFT set module:hook("iq/self/urn:xmpp:sift:1:sift", function(event) local origin, stanza = event.origin, event.stanza; if stanza.attr.type == "set" then local sifttag = stanza.tags[1]; -- -- first, get the elements we are interested in local message = sifttag:get_child("message"); local presence = sifttag:get_child("presence"); local iq = sifttag:get_child("iq"); -- for quick lookup, convert the elements into hashtables message = to_hashtable(message); presence = to_hashtable(presence); iq = to_hashtable(iq); -- make sure elements were valid if message == false or presence == false or iq == false then origin.send(st.error_reply(stanza, "modify", "bad-request")); return true; end local existing = data[origin.full_jid] or {}; -- get existing data, if any data[origin.full_jid] = { presence = presence, message = message, iq = iq }; -- store new data origin.send(st.reply(stanza)); -- send back IQ result if not existing.presence and not origin.presence and presence then -- TODO send probes end return true; end end); -- handle user disconnect module:hook("resource-unbind", function(event) data[event.session.full_jid] = nil; -- discard data end); -- IQ handler module:hook("iq/full", function(event) local origin, stanza = event.origin, event.stanza; local siftdata = data[stanza.attr.to]; if stanza.attr.type == "get" or stanza.attr.type == "set" then if siftdata and siftdata.iq then -- we seem to have an IQ filter local tag = stanza.tags[1]; -- the IQ child if not siftdata.iq[(tag.attr.xmlns or "jabber:client").."|"..tag.name] then -- element not allowed; sending back generic error origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return true; end end end end, 50); -- Message to full JID handler module:hook("message/full", function(event) local origin, stanza = event.origin, event.stanza; local siftdata = data[stanza.attr.to]; if siftdata and siftdata.message then -- we seem to have an message filter local allowed = false; for _, childtag in ipairs(stanza.tags) do if siftdata.message[(childtag.attr.xmlns or "jabber:client").."|"..childtag.name] then allowed = true; end end if not allowed then -- element not allowed; sending back generic error origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- FIXME maybe send to offline storage return true; end end end, 50); -- Message to bare JID handler module:hook("message/bare", function(event) local origin, stanza = event.origin, event.stanza; local user = bare_sessions[jid_bare(stanza.attr.to)]; local allowed = false; for _, session in pairs(user and user.sessions or {}) do local siftdata = data[session.full_jid]; if siftdata and siftdata.message then -- we seem to have an message filter for _, childtag in ipairs(stanza.tags) do if siftdata.message[(childtag.attr.xmlns or "jabber:client").."|"..childtag.name] then allowed = true; end end else allowed = true; end end if user and not allowed then -- element not allowed; sending back generic error origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- FIXME maybe send to offline storage return true; end end, 50); -- Presence to full JID handler module:hook("presence/full", function(event) local origin, stanza = event.origin, event.stanza; local siftdata = data[stanza.attr.to]; if siftdata and siftdata.presence then -- we seem to have an presence filter local allowed = false; for _, childtag in ipairs(stanza.tags) do if siftdata.presence[(childtag.attr.xmlns or "jabber:client").."|"..childtag.name] then allowed = true; end end if not allowed then -- element not allowed; sending back generic error --origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return true; end end end, 50); -- Presence to bare JID handler module:hook("presence/bare", function(event) local origin, stanza = event.origin, event.stanza; local user = bare_sessions[jid_bare(stanza.attr.to)]; local allowed = false; for _, session in pairs(user and user.sessions or {}) do local siftdata = data[session.full_jid]; if siftdata and siftdata.presence then -- we seem to have an presence filter for _, childtag in ipairs(stanza.tags) do if siftdata.presence[(childtag.attr.xmlns or "jabber:client").."|"..childtag.name] then allowed = true; end end else allowed = true; end end if user and not allowed then -- element not allowed; sending back generic error --origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return true; end end, 50); prosody-modules-c53cc1ae4788/mod_compat_bind/0002755000175500017550000000000013164216767020732 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_compat_bind/mod_compat_bind.lua0000644000175500017550000000067113163400720024534 0ustar debacledebacle-- Compatibility with clients that set 'to' on resource bind requests -- -- http://xmpp.org/rfcs/rfc3920.html#bind -- http://xmpp.org/rfcs/rfc6120.html#bind-servergen-success local st = require "util.stanza"; module:hook("iq/host/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event) local fixed_stanza = st.clone(event.stanza); fixed_stanza.attr.to = nil; prosody.core_process_stanza(event.origin, fixed_stanza); return true; end); prosody-modules-c53cc1ae4788/mod_vjud/0002755000175500017550000000000013164216767017423 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_vjud/vcard.lib.lua0000644000175500017550000002175613163400720021762 0ustar debacledebacle-- Copyright (C) 2011-2012 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- TODO -- Fix folding. local st = require "util.stanza"; local t_insert, t_concat = table.insert, table.concat; local type = type; local next, pairs, ipairs = next, pairs, ipairs; local from_text, to_text, from_xep54, to_xep54; local line_sep = "\n"; local vCard_dtd; -- See end of file local function fold_line() error "Not implemented" --TODO end local function unfold_line() error "Not implemented" -- gsub("\r?\n[ \t]([^\r\n])", "%1"); end local function vCard_esc(s) return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n"); end local function vCard_unesc(s) return s:gsub("\\?[\\nt:;,]", { ["\\\\"] = "\\", ["\\n"] = "\n", ["\\r"] = "\r", ["\\t"] = "\t", ["\\:"] = ":", -- FIXME Shouldn't need to espace : in values, just params ["\\;"] = ";", ["\\,"] = ",", [":"] = "\29", [";"] = "\30", [","] = "\31", }); end local function item_to_xep54(item) local t = st.stanza(item.name, { xmlns = "vcard-temp" }); local prop_def = vCard_dtd[item.name]; if prop_def == "text" then t:text(item[1]); elseif type(prop_def) == "table" then if prop_def.types and item.TYPE then if type(item.TYPE) == "table" then for _,v in pairs(prop_def.types) do for _,typ in pairs(item.TYPE) do if typ:upper() == v then t:tag(v):up(); break; end end end else t:tag(item.TYPE:upper()):up(); end end if prop_def.props then for _,v in pairs(prop_def.props) do if item[v] then t:tag(v):up(); end end end if prop_def.value then t:tag(prop_def.value):text(item[1]):up(); elseif prop_def.values then local prop_def_values = prop_def.values; local repeat_last = prop_def_values.behaviour == "repeat-last" and prop_def_values[#prop_def_values]; for i=1,#item do t:tag(prop_def.values[i] or repeat_last):text(item[i]):up(); end end end return t; end local function vcard_to_xep54(vCard) local t = st.stanza("vCard", { xmlns = "vcard-temp" }); for i=1,#vCard do t:add_child(item_to_xep54(vCard[i])); end return t; end function to_xep54(vCards) if not vCards[1] or vCards[1].name then return vcard_to_xep54(vCards) else local t = st.stanza("xCard", { xmlns = "vcard-temp" }); for i=1,#vCards do t:add_child(vcard_to_xep54(vCards[i])); end return t; end end function from_text(data) data = data -- unfold and remove empty lines :gsub("\r\n","\n") :gsub("\n ", "") :gsub("\n\n+","\n"); local vCards = {}; local c; -- current item for line in data:gmatch("[^\n]+") do local line = vCard_unesc(line); local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$"); value = value:gsub("\29",":"); if #params > 0 then local _params = {}; for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do k = k:upper(); local _vt = {}; for _p in v:gmatch("[^\31]+") do _vt[#_vt+1]=_p _vt[_p]=true; end if isval == "=" then _params[k]=_vt; else _params[k]=true; end end params = _params; end if name == "BEGIN" and value == "VCARD" then c = {}; vCards[#vCards+1] = c; elseif name == "END" and value == "VCARD" then c = nil; elseif vCard_dtd[name] then local dtd = vCard_dtd[name]; local p = { name = name }; c[#c+1]=p; --c[name]=p; local up = c; c = p; if dtd.types then for _, t in ipairs(dtd.types) do local t = t:lower(); if ( params.TYPE and params.TYPE[t] == true) or params[t] == true then c.TYPE=t; end end end if dtd.props then for _, p in ipairs(dtd.props) do if params[p] then if params[p] == true then c[p]=true; else for _, prop in ipairs(params[p]) do c[p]=prop; end end end end end if dtd == "text" or dtd.value then t_insert(c, value); elseif dtd.values then local value = "\30"..value; for p in value:gmatch("\30([^\30]*)") do t_insert(c, p); end end c = up; end end return vCards; end local function item_to_text(item) local value = {}; for i=1,#item do value[i] = vCard_esc(item[i]); end value = t_concat(value, ";"); local params = ""; for k,v in pairs(item) do if type(k) == "string" and k ~= "name" then params = params .. (";%s=%s"):format(k, type(v) == "table" and t_concat(v,",") or v); end end return ("%s%s:%s"):format(item.name, params, value) end local function vcard_to_text(vcard) local t={}; t_insert(t, "BEGIN:VCARD") for i=1,#vcard do t_insert(t, item_to_text(vcard[i])); end t_insert(t, "END:VCARD") return t_concat(t, line_sep); end function to_text(vCards) if vCards[1] and vCards[1].name then return vcard_to_text(vCards) else local t = {}; for i=1,#vCards do t[i]=vcard_to_text(vCards[i]); end return t_concat(t, line_sep); end end local function from_xep54_item(item) local prop_name = item.name; local prop_def = vCard_dtd[prop_name]; local prop = { name = prop_name }; if prop_def == "text" then prop[1] = item:get_text(); elseif type(prop_def) == "table" then if prop_def.value then --single item prop[1] = item:get_child_text(prop_def.value) or ""; elseif prop_def.values then --array local value_names = prop_def.values; if value_names.behaviour == "repeat-last" then for i=1,#item.tags do t_insert(prop, item.tags[i]:get_text() or ""); end else for i=1,#value_names do t_insert(prop, item:get_child_text(value_names[i]) or ""); end end elseif prop_def.names then local names = prop_def.names; for i=1,#names do if item:get_child(names[i]) then prop[1] = names[i]; break; end end end if prop_def.props_verbatim then for k,v in pairs(prop_def.props_verbatim) do prop[k] = v; end end if prop_def.types then local types = prop_def.types; prop.TYPE = {}; for i=1,#types do if item:get_child(types[i]) then t_insert(prop.TYPE, types[i]:lower()); end end if #prop.TYPE == 0 then prop.TYPE = nil; end end -- A key-value pair, within a key-value pair? if prop_def.props then local params = prop_def.props; for i=1,#params do local name = params[i] local data = item:get_child_text(name); if data then prop[name] = prop[name] or {}; t_insert(prop[name], data); end end end else return nil end return prop; end local function from_xep54_vCard(vCard) local tags = vCard.tags; local t = {}; for i=1,#tags do t_insert(t, from_xep54_item(tags[i])); end return t end function from_xep54(vCard) if vCard.attr.xmlns ~= "vcard-temp" then return nil, "wrong-xmlns"; end if vCard.name == "xCard" then -- A collection of vCards local t = {}; local vCards = vCard.tags; for i=1,#vCards do t[i] = from_xep54_vCard(vCards[i]); end return t elseif vCard.name == "vCard" then -- A single vCard return from_xep54_vCard(vCard) end end -- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd vCard_dtd = { VERSION = "text", --MUST be 3.0, so parsing is redundant FN = "text", N = { values = { "FAMILY", "GIVEN", "MIDDLE", "PREFIX", "SUFFIX", }, }, NICKNAME = "text", PHOTO = { props_verbatim = { ENCODING = { "b" } }, props = { "TYPE" }, value = "BINVAL", --{ "EXTVAL", }, }, BDAY = "text", ADR = { types = { "HOME", "WORK", "POSTAL", "PARCEL", "DOM", "INTL", "PREF", }, values = { "POBOX", "EXTADD", "STREET", "LOCALITY", "REGION", "PCODE", "CTRY", } }, LABEL = { types = { "HOME", "WORK", "POSTAL", "PARCEL", "DOM", "INTL", "PREF", }, value = "LINE", }, TEL = { types = { "HOME", "WORK", "VOICE", "FAX", "PAGER", "MSG", "CELL", "VIDEO", "BBS", "MODEM", "ISDN", "PCS", "PREF", }, value = "NUMBER", }, EMAIL = { types = { "HOME", "WORK", "INTERNET", "PREF", "X400", }, value = "USERID", }, JABBERID = "text", MAILER = "text", TZ = "text", GEO = { values = { "LAT", "LON", }, }, TITLE = "text", ROLE = "text", LOGO = "copy of PHOTO", AGENT = "text", ORG = { values = { behaviour = "repeat-last", "ORGNAME", "ORGUNIT", } }, CATEGORIES = { values = "KEYWORD", }, NOTE = "text", PRODID = "text", REV = "text", SORTSTRING = "text", SOUND = "copy of PHOTO", UID = "text", URL = "text", CLASS = { names = { -- The item.name is the value if it's one of these. "PUBLIC", "PRIVATE", "CONFIDENTIAL", }, }, KEY = { props = { "TYPE" }, value = "CRED", }, DESC = "text", }; vCard_dtd.LOGO = vCard_dtd.PHOTO; vCard_dtd.SOUND = vCard_dtd.PHOTO; return { from_text = from_text; to_text = to_text; from_xep54 = from_xep54; to_xep54 = to_xep54; -- COMPAT: lua_to_text = to_text; lua_to_xep54 = to_xep54; text_to_lua = from_text; text_to_xep54 = function (...) return to_xep54(from_text(...)); end; xep54_to_lua = from_xep54; xep54_to_text = function (...) return to_text(from_xep54(...)) end; }; prosody-modules-c53cc1ae4788/mod_vjud/README.markdown0000644000175500017550000000221713163400720022103 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: 'XEP-0055: Jabber Search' ... Introduction ============ Basic implementation of [XEP-0055: Jabber Search]. Details ======= This module has two modes. One mode requires users to opt-in to be searchable, then allows users to search the list of those users. The second mode allows search accross all users. Usage ===== First copy the module to the prosody plugins directory. Then add "vjud" to your modules\_enabled list: modules_enabled = { -- ... "vjud", -- ... } Alternatively, you can load it as a component: Component "search.example.com" "vjud" (Some old clients require this) Configuration ============= Option Default Description ------------ ---------- -------------------------------- vjud\_mode "opt-in" Defines how the module behaves Compatibility ============= ------- --------------------------------- 0.8 Works, but only the opt-in mode 0.9 Works trunk Works ------- --------------------------------- Note that the version for 0.8 and 0.9 are slightly different. prosody-modules-c53cc1ae4788/mod_vjud/mod_vjud.lua0000644000175500017550000001366213163400720021722 0ustar debacledebaclelocal dm_load = require "util.datamanager".load; local dm_store = require "util.datamanager".store; local usermanager = require "core.usermanager"; local dataforms_new = require "util.dataforms".new; 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"; local instructions = module:get_option_string("vjud_instructions", "Fill in one or more fields to search for any matching Jabber users."); local get_reply = template[[ {instructions} ]].apply({ instructions = instructions }); local item_template = template[[ {first} {last} {nick} {email} ]]; 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("^[^.]+%.","") or module.host); module:depends"disco"; if module:get_host_type() == "component" then module:add_identity("directory", "user", module:get_option_string("name", "User search")); end module:add_feature("jabber:iq:search"); local vCard_mt = { __index = function(t, k) if type(k) ~= "string" then return nil end for i=1,#t do local t_i = rawget(t, i); if t_i and t_i.name == k then rawset(t, k, t_i); return t_i; end end end }; local function get_user_vcard(user, host) local vCard, err = dm_load(user, host or base_host, "vcard"); if not vCard then return nil, err; end vCard = st.deserialize(vCard); vCard, err = vcard.from_xep54(vCard); if not vCard then return nil, err; end return setmetatable(vCard, vCard_mt); end 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 = 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 or too short")); return true; end local reply = st.reply(stanza):query("jabber:iq:search"); local username, hostname = jid_split(email); if hostname == base_host and username and usermanager.user_exists(username, hostname) then local vCard, err = get_user_vcard(username); if not vCard then module:log("debug", "Couldn't get vCard for user %s: %s", username, err or "unknown error"); else reply:add_child(item_template.apply{ jid = username..at_host; first = vCard.N and vCard.N[2] or nil; last = vCard.N and vCard.N[1] or nil; nick = vCard.NICKNAME and vCard.NICKNAME[1] or username; email = vCard.EMAIL and vCard.EMAIL[1] or nil; }); end else for username in users() do local vCard = get_user_vcard(username); 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; last = vCard.N and vCard.N[1] or nil; nick = vCard.NICKNAME and vCard.NICKNAME[1] or username; email = vCard.EMAIL and vCard.EMAIL[1] or nil; }); end end end origin.send(reply); end return true; 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 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 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 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 prosody-modules-c53cc1ae4788/mod_statistics_auth/0002755000175500017550000000000013164216767021666 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_statistics_auth/mod_statistics_auth.lua0000644000175500017550000000103313163400720026415 0ustar debacledebacle-- mod_statistics_auth module:set_global(); local auth_ok, auth_fail = 0, 0 function module.add_host(module) module:hook("authentication-success", function(event) auth_ok = auth_ok + 1 end); module:hook("authentication-failure", function(event) auth_fail = auth_fail + 1 end); end module:provides("statistics", { statistics = { c2s_auth = { get = function () return auth_ok; end; tostring = tostring; }; c2s_authfail = { get = function () return auth_fail; end; tostring = tostring; }; } }); prosody-modules-c53cc1ae4788/mod_mam_archive/0002755000175500017550000000000013164216767020726 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_mam_archive/mod_mam_archive.lua0000644000175500017550000002632613163400720024531 0ustar debacledebacle-- Prosody IM -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local get_prefs = module:require"mod_mam/mamprefs".get; local set_prefs = module:require"mod_mam/mamprefs".set; local rsm = require "util.rsm"; local jid_bare = require "util.jid".bare; local jid_prep = require "util.jid".prep; local date_parse = require "util.datetime".parse; local date_format = require "util.datetime".datetime; local st = require "util.stanza"; local archive_store = "archive2"; local archive = module:open_store(archive_store, "archive"); local global_default_policy = module:get_option("default_archive_policy", false); local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50); local conversation_interval = tonumber(module:get_option_number("archive_conversation_interval", 86400)); local resolve_relative_path = require "core.configmanager".resolve_relative_path; -- Feature discovery local xmlns_archive = "urn:xmpp:archive" local feature_archive = st.stanza("feature", {xmlns=xmlns_archive}):tag("optional"); if(global_default_policy) then feature_archive:tag("default"); end module:add_extension(feature_archive); module:add_feature("urn:xmpp:archive:auto"); module:add_feature("urn:xmpp:archive:manage"); module:add_feature("urn:xmpp:archive:pref"); module:add_feature("http://jabber.org/protocol/rsm"); -- -------------------------------------------------- local function prefs_to_stanza(prefs) local prefstanza = st.stanza("pref", { xmlns="urn:xmpp:archive" }); local default = prefs[false] ~= nil and prefs[false] or global_default_policy; prefstanza:tag("default", {otr="oppose", save=default and "true" or "false"}):up(); prefstanza:tag("method", {type="auto", use="concede"}):up(); prefstanza:tag("method", {type="local", use="concede"}):up(); prefstanza:tag("method", {type="manual", use="concede"}):up(); for jid, choice in pairs(prefs) do if jid then prefstanza:tag("item", {jid=jid, otr="prefer", save=choice and "message" or "false" }):up() end end return prefstanza; end local function prefs_from_stanza(stanza, username) local current_prefs = get_prefs(username); -- "default" | "item" | "session" | "method" for elem in stanza:children() do if elem.name == "default" then current_prefs[false] = elem.attr["save"] == "true"; elseif elem.name == "item" then current_prefs[elem.attr["jid"]] = not elem.attr["save"] == "false"; elseif elem.name == "session" then module:log("info", "element is not supported: " .. tostring(elem)); -- local found = false; -- for child in data:children() do -- if child.name == elem.name and child.attr["thread"] == elem.attr["thread"] then -- for k, v in pairs(elem.attr) do -- child.attr[k] = v; -- end -- found = true; -- break; -- end -- end -- if not found then -- data:tag(elem.name, elem.attr):up(); -- end elseif elem.name == "method" then module:log("info", "element is not supported: " .. tostring(elem)); -- local newpref = stanza.tags[1]; -- iq:pref -- for _, e in ipairs(newpref.tags) do -- -- if e.name ~= "method" then continue end -- local found = false; -- for child in data:children() do -- if child.name == "method" and child.attr["type"] == e.attr["type"] then -- child.attr["use"] = e.attr["use"]; -- found = true; -- break; -- end -- end -- if not found then -- data:tag(e.name, e.attr):up(); -- end -- end end end end ------------------------------------------------------------ -- Preferences ------------------------------------------------------------ local function preferences_handler(event) local origin, stanza = event.origin, event.stanza; local user = origin.username; local reply = st.reply(stanza); if stanza.attr.type == "get" then reply:add_child(prefs_to_stanza(get_prefs(user))); end if stanza.attr.type == "set" then local new_prefs = stanza:get_child("pref", xmlns_archive); if not new_prefs then return false; end local prefs = prefs_from_stanza(stanza, origin.username); local ok, err = set_prefs(user, prefs); if not ok then return origin.send(st.error_reply(stanza, "cancel", "internal-server-error", "Error storing preferences: "..tostring(err))); end end return origin.send(reply); end local function auto_handler(event) local origin, stanza = event.origin, event.stanza; if not stanza.attr["type"] == "set" then return false; end local user = origin.username; local prefs = get_prefs(user); local auto = stanza:get_child("auto", xmlns_archive); prefs[false] = auto.attr["save"] ~= nil and auto.attr["save"] == "true" or false; set_prefs(user, prefs); return origin.send(st.reply(stanza)); end -- excerpt from mod_storage_sql2 local function get_db() local mod_sql = module:require("sql"); local params = module:get_option("sql"); local engine; params = params or { driver = "SQLite3" }; if params.driver == "SQLite3" then params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite"); end assert(params.driver and params.database, "Both the SQL driver and the database need to be specified"); engine = mod_sql:create_engine(params); engine:set_encoding(); return engine; end ------------------------------------------------------------ -- Collections. In our case there is one conversation with each contact for the whole day for simplicity ------------------------------------------------------------ local function list_stanza_to_query(origin, list_el) local sql = "SELECT `with`, `when` / ".. conversation_interval .." as `day`, COUNT(0) FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=? "; local args = {origin.host, origin.username, archive_store}; local with = list_el.attr["with"]; if with ~= nil then sql = sql .. "AND `with` = ? "; table.insert(args, jid_bare(with)); end local after = list_el.attr["start"]; if after ~= nil then sql = sql .. "AND `when` >= ? "; table.insert(args, date_parse(after)); end local before = list_el.attr["end"]; if before ~= nil then sql = sql .. "AND `when` <= ? "; table.insert(args, date_parse(before)); end sql = sql .. "GROUP BY `with`, `when` / ".. conversation_interval .." ORDER BY `when` / ".. conversation_interval .." ASC "; local qset = rsm.get(list_el); local limit = math.min(qset and qset.max or default_max_items, max_max_items); sql = sql.."LIMIT ?"; table.insert(args, limit); table.insert(args, 1, sql); return args; end local function list_handler(event) local db = get_db(); local origin, stanza = event.origin, event.stanza; local reply = st.reply(stanza); local query = list_stanza_to_query(origin, stanza.tags[1]); local list = reply:tag("list", {xmlns=xmlns_archive}); for row in db:select(unpack(query)) do list:tag("chat", { xmlns=xmlns_archive, with=row[1], start=date_format(row[2] * conversation_interval), version=row[3] }):up(); end origin.send(reply); return true; end ------------------------------------------------------------ -- Message archive retrieval ------------------------------------------------------------ local function retrieve_handler(event) local origin, stanza = event.origin, event.stanza; local reply = st.reply(stanza); local retrieve = stanza:get_child("retrieve", xmlns_archive); local qwith = retrieve.attr["with"]; local qstart = retrieve.attr["start"]; module:log("debug", "Archive query, with %s from %s)", qwith or "anyone", qstart or "the dawn of time"); if qstart then -- Validate timestamps local vstart = (qstart and date_parse(qstart)); if (qstart and not vstart) then origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp")) return true end qstart = vstart; end if qwith then -- Validate the "with" jid local pwith = qwith and jid_prep(qwith); if pwith and not qwith then -- it failed prepping origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid JID")) return true end qwith = jid_bare(pwith); end -- RSM stuff local qset = rsm.get(retrieve); local qmax = math.min(qset and qset.max or default_max_items, max_max_items); local reverse = qset and qset.before or false; local before, after = qset and qset.before, qset and qset.after; if type(before) ~= "string" then before = nil; end -- Load all the data! local data, err = archive:find(origin.username, { start = qstart; ["end"] = qstart + conversation_interval; with = qwith; limit = qmax; before = before; after = after; reverse = reverse; total = true; }); if not data then return origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err)); end local count = err; local chat = reply:tag("chat", {xmlns=xmlns_archive, with=qwith, start=date_format(qstart), version=count}); local first, last; module:log("debug", "Count "..count); for id, item, when in data do if not getmetatable(item) == st.stanza_mt then item = st.deserialize(item); end module:log("debug", tostring(item)); local tag = jid_bare(item.attr["from"]) == jid_bare(origin.full_jid) and "to" or "from"; tag = chat:tag(tag, {secs = when - qstart}); tag:add_child(item:get_child("body")):up(); if not first then first = id; end last = id; end reply:add_child(rsm.generate{ first = first, last = last, count = count }) origin.send(reply); return true; end local function not_implemented(event) local origin, stanza = event.origin, event.stanza; local reply = st.reply(stanza):tag("error", {type="cancel"}); reply:tag("feature-not-implemented", {xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"}):up(); origin.send(reply); end -- Preferences module:hook("iq/self/urn:xmpp:archive:pref", preferences_handler); module:hook("iq/self/urn:xmpp:archive:auto", auto_handler); module:hook("iq/self/urn:xmpp:archive:itemremove", not_implemented); module:hook("iq/self/urn:xmpp:archive:sessionremove", not_implemented); -- Message Archive Management module:hook("iq/self/urn:xmpp:archive:list", list_handler); module:hook("iq/self/urn:xmpp:archive:retrieve", retrieve_handler); module:hook("iq/self/urn:xmpp:archive:remove", not_implemented); -- manual archiving module:hook("iq/self/urn:xmpp:archive:save", not_implemented); -- replication module:hook("iq/self/urn:xmpp:archive:modified", not_implemented); prosody-modules-c53cc1ae4788/mod_mam_archive/README.markdown0000644000175500017550000000231313163400720023403 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: 'XEP-0136: Message Archiving frontend for mod\_mam' ... Introduction ============ Implementation of [XEP-0136: Message Archiving](http://xmpp.org/extensions/xep-0136.html) for [mod\_mam](mod_mam.html). Details ======= See [mod\_mam] for details. Usage ===== First configure mod\_mam as specified in it's [wiki][mod\_mam]. Make sure it uses sql2 storage backend. Then add "mam\_archive" to your modules\_enabled list: modules_enabled = { -- ... "mam_archive", -- ... } Configuration ============= Because of the fact that [XEP-0136] defines a 'conversation' concept not present in [XEP-0313], we have to assume some periods of chat history as 'conversations'. Conversation interval defaults to one day, to provide for a convenient usage. archive_conversation_interval = 86400; -- defined in seconds. One day by default That is the only reason SQL database is required as well. Compatibility ============= ------ --------------- 0.10 Works 0.9 Does not work ------ --------------- ------------ ------------ PostgreSQL Tested MySQL Not tested SQLite Tested ------------ ------------ prosody-modules-c53cc1ae4788/mod_http_user_count/0002755000175500017550000000000013164216767021700 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_http_user_count/mod_http_user_count.lua0000644000175500017550000000215213163400720026444 0ustar debacledebaclelocal it = require "util.iterators"; local jid_split = require "util.jid".prepped_split; module:depends("http"); local function check_muc(jid) local room_name, host = jid_split(jid); if not hosts[host] then return nil, "No such host: "..host; elseif not hosts[host].modules.muc then return nil, "Host '"..host.."' is not a MUC service"; end return room_name, host; end module:provides("http", { route = { ["GET /sessions"] = function () return tostring(it.count(it.keys(prosody.full_sessions))); end; ["GET /users"] = function () return tostring(it.count(it.keys(prosody.bare_sessions))); end; ["GET /host"] = function () return tostring(it.count(it.keys(prosody.hosts[module.host].sessions))); end; ["GET /room/*"] = function (request, room_jid) local name, host = check_muc(room_jid); if not name then return "0"; end local room = prosody.hosts[host].modules.muc.rooms[name.."@"..host]; if not room then return "0"; end return tostring(it.count(it.keys(room._occupants))); end; }; }); prosody-modules-c53cc1ae4788/mod_mam_muc/0002755000175500017550000000000013164216767020071 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_mam_muc/mod_mam_muc.lua0000644000175500017550000003117713163400720023037 0ustar debacledebacle-- XEP-0313: Message Archive Management for Prosody MUC -- Copyright (C) 2011-2017 Kim Alvefur -- -- This file is MIT/X11 licensed. if module:get_host_type() ~= "component" then module:log("error", "mod_%s should be loaded only on a MUC component, not normal hosts", module.name); return; end local xmlns_mam = "urn:xmpp:mam:2"; local xmlns_delay = "urn:xmpp:delay"; local xmlns_forward = "urn:xmpp:forward:0"; local xmlns_st_id = "urn:xmpp:sid:0"; local xmlns_muc_user = "http://jabber.org/protocol/muc#user"; local muc_form_enable = "muc#roomconfig_enablearchiving" local st = require "util.stanza"; local rsm = require "util.rsm"; local jid_bare = require "util.jid".bare; local jid_split = require "util.jid".split; local jid_prep = require "util.jid".prep; local dataform = require "util.dataforms".new; local it = require"util.iterators"; -- Support both old and new MUC code local mod_muc = module:depends"muc"; local rooms = rawget(mod_muc, "rooms"); local each_room = rawget(mod_muc, "each_room") or function() return it.values(rooms); end; local new_muc = not rooms; if new_muc then rooms = module:shared"muc/rooms"; end local get_room_from_jid = rawget(mod_muc, "get_room_from_jid") or function (jid) return rooms[jid]; end local is_stanza = st.is_stanza; local tostring = tostring; local time_now = os.time; local m_min = math.min; local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse; local default_history_length = 20; local max_history_length = module:get_option_number("max_history_messages", math.huge); local function get_historylength(room) return math.min(room._data.history_length or default_history_length, max_history_length); end local log_all_rooms = module:get_option_boolean("muc_log_all_rooms", false); local log_by_default = module:get_option_boolean("muc_log_by_default", true); local archive_store = "muc_log"; local archive = module:open_store(archive_store, "archive"); if archive.name == "null" or not archive.find then if not archive.find then module:log("error", "Attempt to open archive storage returned a driver without archive API support"); module:log("error", "mod_%s does not support archiving", archive._provided_by or archive.name and "storage_"..archive.name.."(?)" or ""); else module:log("error", "Attempt to open archive storage returned null driver"); end module:log("info", "See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information"); return false; end local function archiving_enabled(room) if log_all_rooms then return true; end local enabled = room._data.archiving; if enabled == nil then return log_by_default; end return enabled; end local send_history, save_to_history; -- Override history methods for all rooms. if not new_muc then -- 0.10 or older module:hook("muc-room-created", function (event) local room = event.room; if archiving_enabled(room) then room.send_history = send_history; room.save_to_history = save_to_history; end end); function module.load() for room in each_room() do if archiving_enabled(room) then room.send_history = send_history; room.save_to_history = save_to_history; end end end function module.unload() for room in each_room() do if room.send_history == send_history then room.send_history = nil; room.save_to_history = nil; end end end end if not log_all_rooms then module:hook("muc-config-form", function(event) local room, form = event.room, event.form; table.insert(form, { name = muc_form_enable, type = "boolean", label = "Enable archiving?", value = archiving_enabled(room), } ); end); module:hook("muc-config-submitted", function(event) local room, fields, changed = event.room, event.fields, event.changed; local new = fields[muc_form_enable]; if new ~= room._data.archiving then room._data.archiving = new; if type(changed) == "table" then changed[muc_form_enable] = true; else event.changed = true; end if new then room.send_history = send_history; room.save_to_history = save_to_history; else room.send_history = nil; room.save_to_history = nil; end end end); end -- Note: We ignore the 'with' field as this is internally used for stanza types local query_form = dataform { { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam; }; { name = "with"; type = "jid-single"; }; { name = "start"; type = "text-single" }; { name = "end"; type = "text-single"; }; }; -- Serve form module:hook("iq-get/bare/"..xmlns_mam..":query", function(event) local origin, stanza = event.origin, event.stanza; origin.send(st.reply(stanza):add_child(query_form:form())); return true; end); -- Handle archive queries module:hook("iq-set/bare/"..xmlns_mam..":query", function(event) local origin, stanza = event.origin, event.stanza; local room_jid = stanza.attr.to; local room_node = jid_split(room_jid); local orig_from = stanza.attr.from; local query = stanza.tags[1]; local room = get_room_from_jid(room_jid); if not room then origin.send(st.error_reply(stanza, "cancel", "item-not-found")) return true; end local from = jid_bare(orig_from); -- Banned or not a member of a members-only room? local from_affiliation = room:get_affiliation(from); if from_affiliation == "outcast" -- banned or room:get_members_only() and not from_affiliation then -- members-only, not a member origin.send(st.error_reply(stanza, "auth", "forbidden")) return true; end local qid = query.attr.queryid; -- Search query parameters local qstart, qend; local form = query:get_child("x", "jabber:x:data"); if form then local err; form, err = query_form:data(form); if err then origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err)))); return true; end qstart, qend = form["start"], form["end"]; end if qstart or qend then -- Validate timestamps local vstart, vend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend)) if (qstart and not vstart) or (qend and not vend) then origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp")) return true; end qstart, qend = vstart, vend; end module:log("debug", "Archive query id %s from %s until %s)", tostring(qid), qstart and timestamp(qstart) or "the dawn of time", qend and timestamp(qend) or "now"); -- RSM stuff local qset = rsm.get(query); local qmax = m_min(qset and qset.max or 20, 20); local reverse = qset and qset.before or false; local before, after = qset and qset.before, qset and qset.after; if type(before) ~= "string" then before = nil; end -- Load all the data! local data, err = archive:find(room_node, { start = qstart; ["end"] = qend; -- Time range limit = qmax + 1; before = before; after = after; reverse = reverse; with = "message qmax then complete = nil; break; end local fwd_st = st.message(msg_reply_attr) :tag("result", { xmlns = xmlns_mam, queryid = qid, id = id }) :tag("forwarded", { xmlns = xmlns_forward }) :tag("delay", { xmlns = xmlns_delay, stamp = timestamp(when) }):up(); if room:get_whois() ~= "anyone" then item:maptags(function (tag) if tag.name == "x" and tag.attr.xmlns == xmlns_muc_user then return nil; end return tag; end); end if not is_stanza(item) then item = st.deserialize(item); end item.attr.xmlns = "jabber:client"; fwd_st:add_child(item); if not first then first = id; end last = id; if reverse then results[count] = fwd_st; else origin.send(fwd_st); end end if reverse then for i = #results, 1, -1 do origin.send(results[i]); end first, last = last, first; end -- That's all folks! module:log("debug", "Archive query %s completed", tostring(qid)); origin.send(st.reply(stanza) :tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete }) :add_child(rsm.generate { first = first, last = last })); return true; end); module:hook("muc-get-history", function (event) local room = event.room; if not archiving_enabled(room) then return end local room_jid = room.jid; local maxstanzas = event.maxstanzas or math.huge; local maxchars = event.maxchars; local since = event.since; local to = event.to; -- Load all the data! local query = { limit = math.min(maxstanzas, get_historylength(room)); start = since; reverse = true; with = "message that claim to be from us stanza:maptags(function (tag) if tag.name == "stanza-id" and tag.attr.xmlns == xmlns_st_id and jid_prep(tag.attr.by) == self.jid then return nil; end if tag.name == "x" and tag.attr.xmlns == xmlns_muc_user then return nil; end return tag; end); local stored_stanza = stanza; if stanza.name == "message" and self:get_whois() == "anyone" then stored_stanza = st.clone(stanza); local actor = jid_bare(self._occupants[stanza.attr.from].jid); local affiliation = self:get_affiliation(actor) or "none"; local role = self:get_role(actor) or self:get_default_role(affiliation); stored_stanza:add_direct_child(st.stanza("x", { xmlns = xmlns_muc_user }) :tag("item", { affiliation = affiliation; role = role; jid = actor })); end -- Policy check if not archiving_enabled(self) then return end -- Don't log -- And stash it local with = stanza.name if stanza.attr.type then with = with .. "<" .. stanza.attr.type end local id = archive:append(room_node, nil, stored_stanza, time_now(), with); if id then stanza:add_direct_child(st.stanza("stanza-id", { xmlns = xmlns_st_id, by = self.jid, id = id })); end end module:hook("muc-broadcast-message", function (event) local room, stanza = event.room, event.stanza; if stanza:get_child("body") then save_to_history(room, stanza); end end); if module:get_option_boolean("muc_log_presences", true) then module:hook("muc-occupant-joined", function (event) save_to_history(event.room, st.stanza("presence", { from = event.nick })); end); module:hook("muc-occupant-left", function (event) save_to_history(event.room, st.stanza("presence", { type = "unavailable", from = event.nick })); end); end module:hook("muc-room-destroyed", function(event) local room_node = jid_split(event.room.jid); archive:delete(room_node); end); -- And role/affiliation changes? module:add_feature(xmlns_mam); module:hook("muc-disco#info", function(event) event.reply:tag("feature", {var=xmlns_mam}):up(); end); prosody-modules-c53cc1ae4788/mod_mam_muc/README.markdown0000644000175500017550000000275513163400720022560 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: 'XEP-0313: Message Archive Management for MUC' ... Introduction ============ This module logs the conversation of chatrooms running on the server to Prosody's archive storage. To access them you will need a client with support for [XEP-0313: Message Archive Management] or a module such as [mod\_http\_muc\_log]. Usage ===== First copy the module to the prosody plugins directory. Then add "mam\_muc" to your modules\_enabled list: ``` {.lua} Component "conference.example.org" "muc" modules_enabled = { "mam_muc", } ``` mod\_mam\_muc needs an archive-capable storage module, see [Prosodys storage documentation][doc:storage] for how to select one. The store is called "muc\_log". Configuration ============= Logging needs to be enabled for each room in the room configuration dialog. ``` {.lua} muc_log_by_default = true; -- Enable logging by default (can be disabled in room config) muc_log_all_rooms = false; -- set to true to force logging of all rooms -- This is the largest number of messages that are allowed to be retrieved when joining a room. max_history_messages = 20; ``` Compatibility ============= ------- ----------------- trunk Works best 0.10 Works partially 0.9 Does not work 0.8 Does not work ------- ----------------- Prosody trunk (after April 2014) has a major rewrite of the MUC module, allowing easier integration. Without this (0.10), some features do not work, such as correct advertising and join/part logging. prosody-modules-c53cc1ae4788/misc/0002755000175500017550000000000013164216767016547 5ustar debacledebacleprosody-modules-c53cc1ae4788/misc/systemd/0002755000175500017550000000000013164216767020237 5ustar debacledebacleprosody-modules-c53cc1ae4788/misc/systemd/socket-activation.lua0000644000175500017550000000176313163400720024355 0ustar debacledebacle-- Monkeypatch to support socket activation -- -- Requires LuaSocket after "agnostic" changes merged -- -- To enable: -- RunScript "socket-activation.lua" local socket = require"socket"; local tcp_serv_mt = debug.getregistry()["tcp{server}"]; local socket_bind = socket.bind; local SD_LISTEN_FDS_START = 3; local fds = tonumber(os.getenv"LISTEN_FDS") or 0; if fds < SD_LISTEN_FDS_START then return; end local servs = {}; for i = 1, fds do local serv = socket.tcp(); if serv:getfd() >= 0 then return; -- This won't work, we will leak the old FD end debug.setmetatable(serv, tcp_serv_mt); serv:setfd(SD_LISTEN_FDS_START + i - 1); local ip, port = serv:getsockname(); servs [ ip .. ":" .. port ] = serv; end function socket.bind( ip, port, backlog ) local sock = servs [ ip .. ":" .. port ]; if sock then servs [ ip .. ":" .. port ] = nil; return sock; end if next(servs) == nil then -- my work here is done socket.bind = socket_bind; end return socket_bind( ip, port, backlog ); end prosody-modules-c53cc1ae4788/misc/systemd/prosody-c2s.socket0000644000175500017550000000022413163400720023610 0ustar debacledebacle[Install] WantedBy=sockets.target [Socket] ListenStream=0.0.0.0:5222 ListenStream=5222 BindIPv6Only=ipv6-only Accept=false Service=prosody.service prosody-modules-c53cc1ae4788/misc/systemd/prosody.service0000644000175500017550000000323013163400720023273 0ustar debacledebacle[Unit] ### see man systemd.unit Description=Prosody XMPP Server Documentation=https://prosody.im/doc [Service] ### See man systemd.service ### # With this configuration, systemd takes care of daemonization # so Prosody should be configured with daemonize = false Type=simple # Not sure if this is needed for 'simple' PIDFile=/var/run/prosody/prosody.pid # Start by executing the main executable ExecStart=/usr/bin/prosody ExecReload=/bin/kill -HUP $MAINPID # Restart on crashes Restart=on-abnormal # Set O_NONBLOCK flag on sockets passed via socket activation NonBlocking=true ### See man systemd.exec ### WorkingDirectory=/var/lib/prosody User=prosody Group=prosody Umask=0027 # Nice=0 # Set stdin to /dev/null since Prosody does not need it StandardInput=null # Direct stdout/-err to journald for use with log = "*stdout" StandardOutput=journal StandardError=inherit # This usually defaults to 4k or so # LimitNOFILE=1M ## Interesting protection methods # Finding a useful combo of these settings would be nice # # Needs read access to /etc/prosody for config # Needs write access to /var/lib/prosody for storing data (for internal storage) # Needs write access to /var/log/prosody for writing logs (depending on config) # Needs read access to code and libraries loaded # ReadWriteDirectories=/var/lib/prosody /var/log/prosody # InaccessibleDirectories=/boot /home /media /mnt /root /srv # ReadOnlyDirectories=/usr /etc/prosody # PrivateTmp=true # PrivateDevices=true # PrivateNetwork=false # ProtectSystem=full # ProtectHome=true # ProtectKernelTunables=true # ProtectControlGroups=true # SystemCallFilter= # This should break LuaJIT # MemoryDenyWriteExecute=true prosody-modules-c53cc1ae4788/misc/systemd/prosody-s2s.socket0000644000175500017550000000022413163400720023630 0ustar debacledebacle[Install] WantedBy=sockets.target [Socket] ListenStream=0.0.0.0:5269 ListenStream=5269 BindIPv6Only=ipv6-only Accept=false Service=prosody.service prosody-modules-c53cc1ae4788/misc/sasl/0002755000175500017550000000000013164216767017511 5ustar debacledebacleprosody-modules-c53cc1ae4788/misc/sasl/example.lua0000644000175500017550000000161413163400720021626 0ustar debacledebaclelocal method = {} local method_mt = { __index = method } -- This should return a set of supported mechanisms function method:mechanisms() return { ["OAUTH-SOMETHING"] = true; } end -- Called when a mechanism is selecetd function method:select(mechanism) return mechanism == "OAUTH-SOMETHING"; end -- Called for each message received function method:process(message) -- parse the message if false then -- To send a SASL challenge: return "challenge", "respond-to-this"; end if false then -- To fail, send: return "failure", "not-authorized", "Helpful error message here"; end self.username = "someone"; return "success"; end local function new_sasl() return setmetatable({}, method_mt); end function method:clean_clone() return setmetatable({}, method_mt); end local provider = {} function provider.get_sasl_handler() return new_sasl(); end module:provides("auth", provider); prosody-modules-c53cc1ae4788/misc/README.markdown0000644000175500017550000000016113163400720021223 0ustar debacledebacleMiscellaneous modules ===================== Things that aren't really Prosody plugins go here, e.g. RunScripts. prosody-modules-c53cc1ae4788/misc/upstart/0002755000175500017550000000000013164216767020251 5ustar debacledebacleprosody-modules-c53cc1ae4788/misc/upstart/prosody.conf0000644000175500017550000000106113163400720022572 0ustar debacledebacleauthor "Kim Alvefur " description "Prosody XMPP server" # Normal runlevel based start and stop start on runlevel [2345] stop on runlevel [!2345] # Alternate method for starting and stopping # when a network interface comes and goes # # start on net-device-up IFACE=eth0 # stop on net-device-down IFACE=eth0 # Change user so Prosdy doesn't have to setgid prosody setuid prosody # Set a sensible umask umask 0027 # Run prosody exec /usr/bin/prosody # Collect stdout into a log file console log # Restart on crashes respawn respawn limit 5 10 prosody-modules-c53cc1ae4788/misc/munin/0002755000175500017550000000000013164216767017675 5ustar debacledebacleprosody-modules-c53cc1ae4788/misc/munin/prosody_.lua0000644000175500017550000000660213163400720022217 0ustar debacledebacle#!/usr/bin/env lua -- Script for use with mod_statistics -- Create symlinks to this named eg prosody_c2s in /etc/munin/plugins local print = print; local pairs = pairs; local socket = require"socket"; local stats = {}; stats.c2s = { graph_title = "Prosody C2S Connections"; graph_vlabel = "users"; graph_category = "Prosody"; all_client_connections = { label = "client connections"; _key = "total_c2s"; } } stats.s2s = { graph_title = "Prosody S2S Connections"; graph_vlabel = "servers"; graph_category = "Prosody"; outgoing_connections = { label = "outgoing connections"; _key = "total_s2sout"; }; incoming_connections = { label = "incoming connections"; _key = "total_s2sin"; } } stats.mem = { graph_title = "Prosody Memory Usage"; graph_vlabel = "Bytes"; graph_args = "--base 1024 -l 0"; graph_category = "Prosody"; --memory_unused graph_order = "memory_total memory_rss memory_allocated memory_used memory_lua memory_returnable"; memory_allocated = { label = "Allocated", draw = "AREA" }; memory_lua = { label = "Lua", draw = "AREA" }; memory_rss = { label = "RSS", draw = "AREA" }; memory_total = { label = "Total", draw = "AREA" }; -- memory_unused = { label = "Unused", draw = "AREA" }; memory_used = { label = "Used", draw = "AREA" }; memory_returnable = { label = "Returnable", draw = "AREA" }; } stats.cpu = { graph_title = "Prosody CPU Usage"; graph_category = "Prosody"; graph_args = "-l 0"; graph_vlabel = "CPU time used in milliseconds"; cpu_total = { label = "CPU"; type = "DERIVE"; min = 0; }; } stats.auth = { graph_title = "Prosody Authentications"; graph_category = "Prosody"; graph_args = "--base 1000"; c2s_auth = { label = "Logins"; type = "DERIVE"; min = 0; }; c2s_authfail = { label = "Failed logins"; type = "DERIVE"; min = 0; }; } local function onerror(msg, err, exit) io.stderr:write(msg, '\n'); if err then io.stderr:write(err, '\n'); end os.exit(exit or 1); end local function connect() local conn, err = socket.connect(os.getenv"host" or "localhost", os.getenv"port" or 5782); if not conn then onerror("Could not connect to prosody", err); end conn:settimeout(1); return conn; end local function get_config(item) for k,v in pairs(item) do if type(v) == "string" then print(k .. " " .. v); elseif type(v) == "table" then for sk,v in pairs(v) do if not sk:match("^_") then print(k.."."..sk.." "..v); end end end end end local function get_stats(item) local labels = {}; for key, val in pairs(item) do if type(val) == "table" and val.label then labels[val._key or key] = key; end end local conn = connect(); local line, err = conn:receive("*l"); local stat, value, label; while line and line ~= "" and next(labels) ~= nil do stat, value = line:match('^STAT%s+"([^"]*)"%s*(%b())'); label = stat and labels[stat]; if label then print(label..".value "..tonumber(value:sub(2,-2))); labels[stat] = nil; end line, err = conn:receive("*l"); end if err then onerror(err); end end local function main(stat, mode) if mode == "suggest" then for available_stat in pairs(stats) do print(available_stat); end elseif mode == "config" then return get_config(stats[stat]); elseif stats[stat] then return get_stats(stats[stat]); end end if arg then return main(arg[0]:match("prosody_(%w*)"), ...); end return { stats = stats, get_stats = get_stats, get_config = get_config, } prosody-modules-c53cc1ae4788/mod_compat_vcard/0002755000175500017550000000000013164216767021115 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_compat_vcard/mod_compat_vcard.lua0000644000175500017550000000125613163400720025102 0ustar debacledebacle-- Compatibility with clients and servers (i.e. ejabberd) that send vcard -- requests to the full JID -- -- https://support.process-one.net/browse/EJAB-1045 local jid_bare = require "util.jid".bare; local st = require "util.stanza"; local core_process_stanza = prosody.core_process_stanza; module:hook("iq/full", function(event) local stanza = event.stanza; local payload = stanza.tags[1]; if payload and stanza.attr.type == "get" and payload.name == "vCard" and payload.attr.xmlns == "vcard-temp" then local fixed_stanza = st.clone(event.stanza); fixed_stanza.attr.to = jid_bare(stanza.attr.to); core_process_stanza(event.origin, fixed_stanza); return true; end end, 1); prosody-modules-c53cc1ae4788/mod_dwd/0002755000175500017550000000000013164216767017231 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_dwd/README.markdown0000644000175500017550000000113213163400720021704 0ustar debacledebacle--- summary: 'Dialback-without-Dialback' ... Introduction ============ This module implements an optimization of the Dialback protocol, by skipping the dialback step for servers presenting a valid certificate. Configuration ============= Simply add the module to the `modules_enabled` list. modules_enabled = { ... "dwd"; } Compatibility ============= ------------------ -------------------------- 0.10 Built into mod\_dialback 0.9 + LuaSec 0.5 Works 0.8 Doesn't work ------------------ -------------------------- prosody-modules-c53cc1ae4788/mod_dwd/mod_dwd.lua0000644000175500017550000000242413163400720021330 0ustar debacledebaclelocal hosts = _G.hosts; local st = require "util.stanza"; local nameprep = require "util.encodings".stringprep.nameprep; local cert_verify_identity = require "util.x509".verify_identity; module:hook("stanza/jabber:server:dialback:result", function(event) local origin, stanza = event.origin, event.stanza; if origin.cert_chain_status == "valid" and origin.type == "s2sin_unauthed" or origin.type == "s2sin" then local attr = stanza.attr; local to, from = nameprep(attr.to), nameprep(attr.from); local conn = origin.conn:socket() local cert; if conn.getpeercertificate then cert = conn:getpeercertificate() end if cert and hosts[to] and cert_verify_identity(from, "xmpp-server", cert) then -- COMPAT: ejabberd, gmail and perhaps others do not always set 'to' and 'from' -- on streams. We fill in the session's to/from here instead. if not origin.from_host then origin.from_host = from; end if not origin.to_host then origin.to_host = to; end module:log("info", "Accepting Dialback without Dialback for %s", from); module:fire_event("s2s-authenticated", { session = origin, host = from }); origin.sends2s( st.stanza("db:result", { from = attr.to, to = attr.from, id = attr.id, type = "valid" })); return true; end end end, 100); prosody-modules-c53cc1ae4788/mod_proctitle/0002755000175500017550000000000013164216767020460 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_proctitle/mod_proctitle.lua0000644000175500017550000000046113163400720024005 0ustar debacledebacle-- Changes the process name to 'prosody' rather than 'lua'/'lua5.1' -- Copyright (C) 2015 Rob Hoelz -- -- This file is MIT/X11 licensed. -- To use this module, you'll need the proctitle Lua library: -- https://github.com/hoelzro/lua-proctitle local proctitle = require 'proctitle'; proctitle 'prosody'; prosody-modules-c53cc1ae4788/mod_proctitle/README.markdown0000644000175500017550000000041113163400720023132 0ustar debacledebacle--- summary: Set process name to prosody ... This module sets the process name to `prosody` so it shows up as such instead of `lua` in process management tools. To use this module, you'll need the proctitle Lua library: prosody-modules-c53cc1ae4788/mod_auth_http_async/0002755000175500017550000000000013164216767021650 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_auth_http_async/README.markdown0000644000175500017550000000110113163400720024317 0ustar debacledebacle--- labels: - Stage-Alpha ... Introduction ============ This is an experimental authentication module that does an asynchronous HTTP call to verify username and password. Details ======= When a user attempts to authenticate to Prosody, this module takes the username and password and does a HTTP GET request with [Basic authentication][rfc7617] to the configured `http_auth_url`. Configuration ============= ``` lua VirtualHost "example.com" authentication = "http_async" http_auth_url = "http://example.com/auth" ``` Compatibility ============= Requires Prosody trunk prosody-modules-c53cc1ae4788/mod_auth_http_async/mod_auth_http_async.lua0000644000175500017550000000635713163400720026377 0ustar debacledebacle-- Prosody IM -- Copyright (C) 2008-2013 Matthew Wild -- Copyright (C) 2008-2013 Waqas Hussain -- Copyright (C) 2014 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local new_sasl = require "util.sasl".new; local base64 = require "util.encodings".base64.encode; local have_async, async = pcall(require, "util.async"); local log = module._log; local host = module.host; local api_base = module:get_option_string("http_auth_url", ""):gsub("$host", host); if api_base == "" then error("http_auth_url required") end local provider = {}; -- globals required by socket.http if rawget(_G, "PROXY") == nil then rawset(_G, "PROXY", false) end if rawget(_G, "base_parsed") == nil then rawset(_G, "base_parsed", false) end local function async_http_auth(url, username, password) module:log("debug", "async_http_auth()"); local http = require "net.http"; local wait, done = async.waiter(); local content, code, request, response; local ex = { headers = { Authorization = "Basic "..base64(username..":"..password); }; } local function cb(content_, code_, request_, response_) content, code, request, response = content_, code_, request_, response_; done(); end http.request(url, ex, cb); wait(); if code >= 200 and code <= 299 then module:log("debug", "HTTP auth provider confirmed valid password"); return true; else module:log("debug", "HTTP auth provider returned status code %d", code); end return nil, "Auth failed. Invalid username or password."; end local function sync_http_auth(url,username, password) module:log("debug", "sync_http_auth()"); local http = require "socket.http"; local https = require "ssl.https"; local request; if string.sub(url, 1, string.len('https')) == 'https' then request = https.request; else request = http.request; end local _, code, headers, status = request{ url = url, headers = { Authorization = "Basic "..base64(username..":"..password); } }; if type(code) == "number" and code >= 200 and code <= 299 then module:log("debug", "HTTP auth provider confirmed valid password"); return true; else module:log("debug", "HTTP auth provider returned status code: "..code); end return nil, "Auth failed. Invalid username or password."; end function provider.test_password(username, password) local url = api_base:gsub("$user", username):gsub("$password", password); log("debug", "Testing password for user %s at host %s with URL %s", username, host, url); if (have_async) then return async_http_auth(url, username, password); else return sync_http_auth(url, username, password); end end function provider.users() return function() return nil; end end function provider.set_password(username, password) return nil, "Changing passwords not supported"; end function provider.user_exists(username) return true; end function provider.create_user(username, password) return nil, "User creation not supported"; end function provider.delete_user(username) return nil , "User deletion not supported"; end function provider.get_sasl_handler() return new_sasl(host, { plain_test = function(sasl, username, password, realm) return provider.test_password(username, password), true; end }); end module:provides("auth", provider);prosody-modules-c53cc1ae4788/mod_e2e_policy/0002755000175500017550000000000013164216767020505 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_e2e_policy/mod_e2e_policy.lua0000644000175500017550000000652013163400720024061 0ustar debacledebaclelocal st = require "util.stanza"; local host = module.host; local e2e_policy_chat = module:get_option_string("e2e_policy_chat", "optional"); -- possible values: none, optional and required local e2e_policy_muc = module:get_option_string("e2e_policy_muc", "optional"); -- possible values: none, optional and required local e2e_policy_whitelist = module:get_option_set("e2e_policy_whitelist", { }); -- make this module ignore messages sent to and from this JIDs or MUCs local e2e_policy_message_optional_chat = module:get_option_string("e2e_policy_message_optional_chat", "For security reasons, OMEMO, OTR or PGP encryption is STRONGLY recommended for conversations on this server."); local e2e_policy_message_required_chat = module:get_option_string("e2e_policy_message_required_chat", "For security reasons, OMEMO, OTR or PGP encryption is required for conversations on this server."); local e2e_policy_message_optional_muc = module:get_option_string("e2e_policy_message_optional_muc", "For security reasons, OMEMO, OTR or PGP encryption is STRONGLY recommended for MUC on this server."); local e2e_policy_message_required_muc = module:get_option_string("e2e_policy_message_required_muc", "For security reasons, OMEMO, OTR or PGP encryption is required for MUC on this server."); function warn_on_plaintext_messages(event) -- check if JID is whitelisted if e2e_policy_whitelist:contains(event.stanza.attr.from) or e2e_policy_whitelist:contains(event.stanza.attr.to) then return nil; end local body = event.stanza:get_child_text("body"); -- do not warn for status messages if not body or event.stanza.attr.type == "error" then return nil; end -- check otr if body and body:sub(1,4) == "?OTR" then return nil; end -- check omemo https://xmpp.org/extensions/inbox/omemo.html if event.stanza:get_child("encrypted", "eu.siacs.conversations.axolotl") or event.stanza:get_child("encrypted", "urn:xmpp:omemo:0") then return nil; end -- check xep27 pgp https://xmpp.org/extensions/xep-0027.html if event.stanza:get_child("x", "jabber:x:encrypted") then return nil; end -- check xep373 pgp (OX) https://xmpp.org/extensions/xep-0373.html if event.stanza:get_child("openpgp", "urn:xmpp:openpgp:0") then return nil; end -- no valid encryption found if e2e_policy_chat == "optional" and event.stanza.attr.type ~= "groupchat" then event.origin.send(st.message({ from = host, type = "headline" }, e2e_policy_message_optional_chat)); end if e2e_policy_chat == "required" and event.stanza.attr.type ~= "groupchat" then return event.origin.send(st.error_reply(event.stanza, "modify", "policy-violation", e2e_policy_message_required_chat)); end if e2e_policy_muc == "optional" and event.stanza.attr.type == "groupchat" then event.origin.send(st.message({ from = host, type = "headline" }, e2e_policy_message_optional_muc)); end if e2e_policy_muc == "required" and event.stanza.attr.type == "groupchat" then return event.origin.send(st.error_reply(event.stanza, "modify", "policy-violation", e2e_policy_message_required_muc)); end end module:hook("pre-message/bare", warn_on_plaintext_messages, 300); module:hook("pre-message/full", warn_on_plaintext_messages, 300); module:hook("pre-message/host", warn_on_plaintext_messages, 300); prosody-modules-c53cc1ae4788/mod_e2e_policy/README.markdown0000644000175500017550000000466013163400720023171 0ustar debacledebacleIntroduction ============ This module was written to encourage usage of End-to-end encryption for chat and MUC messages. It can be configured to warn the sender after every plaintext/unencrypted message or to block all plaintext/unencrypted messages. It also supports MUC and JID whitelisting, so administrators can for example whitelist public support MUCs ;-) Configuration ============= Enable the module as any other: modules_enabled = { "mod_e2e_policy"; } You can then set some options to configure your desired policy: Option Default Description ------------------------------------ ------------ ------------------------------------------------------------------------------------------------------------------------------------------------- e2e\_policy\_chat `"optional"` Policy for chat messages. Possible values: `"none"`, `"optional"` and `"required"`. e2e\_policy\_muc `"optional"` Policy for MUC messages. Possible values: `"none"`, `"optional"` and `"required"`. e2e\_policy\_whitelist `{ }` Make this module ignore messages sent to and from this JIDs or MUCs. e2e\_policy\_message\_optional\_chat `""` Set a custom warning message for chat messages. e2e\_policy\_message\_required\_chat `""` Set a custom error message for chat messages. e2e\_policy\_message\_optional\_muc `""` Set a custom warning message for MUC messages. e2e\_policy\_message\_required\_muc `""` Set a custom error message for MUC messages. Some examples: e2e_policy_chat = "optional" e2e_policy_muc = "optional" e2e_policy_whitelist = { "admin@example.com", "prosody@conference.prosody.im" } e2e_policy_message_optional_chat = "For security reasons, OMEMO, OTR or PGP encryption is STRONGLY recommended for conversations on this server." e2e_policy_message_required_chat = "For security reasons, OMEMO, OTR or PGP encryption is required for conversations on this server." e2e_policy_message_optional_muc = "For security reasons, OMEMO, OTR or PGP encryption is STRONGLY recommended for MUC on this server." e2e_policy_message_required_muc = "For security reasons, OMEMO, OTR or PGP encryption is required for MUC on this server." Compatibility ============= ----- ------------- trunk Works 0.10 Should work 0.9 Should work ----- ------------- prosody-modules-c53cc1ae4788/mod_ipcheck/0002755000175500017550000000000013164216767020061 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_ipcheck/README.markdown0000644000175500017550000000065313163400720022543 0ustar debacledebacle--- labels: - 'Stage-Stable' summary: 'XEP-0279: Server IP Check' ... Introduction ============ Sometimes for various reasons a client might want to know its IP address as it appears to the server. This [simple XEP](http://xmpp.org/extensions/xep-0279.html) allows the client to ask the server for the IP address it is connected from. Compatibility ============= ----- ------- 0.7 Works 0.6 Works ----- ------- prosody-modules-c53cc1ae4788/mod_ipcheck/mod_ipcheck.lua0000644000175500017550000000336013163400720023010 0ustar debacledebacle -- mod_ipcheck.lua -- Implementation of XEP-0279: Server IP Check local st = require "util.stanza"; module:add_feature("urn:xmpp:sic:0"); module:hook("iq/bare/urn:xmpp:sic:0:ip", function(event) local origin, stanza = event.origin, event.stanza; if stanza.attr.type == "get" then if stanza.attr.to then origin.send(st.error_reply(stanza, "auth", "forbidden", "You can only ask about your own IP address")); elseif origin.ip then origin.send(st.reply(stanza):tag("ip", {xmlns='urn:xmpp:sic:0'}):text(origin.ip)); else -- IP addresses should normally be available, but in case they are not origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "IP address for this session is not available")); end return true; end end); module:add_feature("urn:xmpp:sic:1"); module:hook("iq/bare/urn:xmpp:sic:1:address", function(event) local origin, stanza = event.origin, event.stanza; if stanza.attr.type == "get" then if stanza.attr.to then origin.send(st.error_reply(stanza, "auth", "forbidden", "You can only ask about your own IP address")); elseif origin.ip then local reply = st.reply(stanza):tag("address", {xmlns='urn:xmpp:sic:1'}) :tag("ip"):text(origin.ip):up() if origin.conn and origin.conn.port then -- server_event reply:tag("port"):text(tostring(origin.conn:port())) elseif origin.conn and origin.conn.clientport then -- server_select reply:tag("port"):text(tostring(origin.conn:clientport())) end origin.send(reply); else -- IP addresses should normally be available, but in case they are not origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "IP address for this session is not available")); end return true; end end); prosody-modules-c53cc1ae4788/mod_storage_mongodb/0002755000175500017550000000000013164216767021624 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_storage_mongodb/README.markdown0000644000175500017550000000177413163400720024313 0ustar debacledebacle--- labels: - 'Type-Storage' - 'Stage-Alpha' summary: MongoDB Storage Module ... Introduction ============ This is a storage backend that uses MongoDB. Depends on [luamongo bindings](https://github.com/mwild1/luamongo) This module is not under active development and has a number of issues related to limitations in MongoDB. It is not suitable for production use. Configuration ============= Copy the module to the prosody modules/plugins directory. In Prosody's configuration file, set: storage = "mongodb" MongoDB options are: Name Description ------------ ------------------------------------------------------------------- server hostname:port username your username for the given database password your password for the given database (either raw or pre-digested) is\_digest whether the password field has been pre-digested Compatibility ============= ------- --------------------------- trunk Untested, but should work ------- --------------------------- prosody-modules-c53cc1ae4788/mod_storage_mongodb/mod_storage_mongodb.lua0000644000175500017550000000327413163400720026322 0ustar debacledebaclelocal next = next; local setmetatable = setmetatable; local params = assert ( module:get_option("mongodb") , "mongodb configuration not found" ); prosody.unlock_globals(); local mongo = require "mongo"; prosody.lock_globals(); local json = require "util.json"; local conn local keyval_store = {}; keyval_store.__index = keyval_store; function keyval_store:get(username) local host = module.host or "_global"; local store = self.store; -- The database name can't have a period in it (hence it can't be a host/ip) local namespace = params.dbname .. "." .. host; local v = { _id = { store = store ; username = username } }; local cursor , err = conn:query ( namespace , v ); if not cursor then return nil , err end; local r , err = cursor:next ( ); if not r then return nil , err end; return r.data; end function keyval_store:set(username, data) local host = module.host or "_global"; local store = self.store; -- The database name can't have a period in it (hence it can't be a host/ip) local namespace = params.dbname .. "." .. host; local v = { _id = { store = store ; username = username } }; if next(data) ~= nil then -- set data v.data = data; return conn:insert ( namespace , json.encode(v) ); else -- delete data return conn:remove ( namespace , v ); end; end local driver = {}; function driver:open(store, typ) if not conn then conn = assert ( mongo.Connection.New ( true ) ); assert ( conn:connect ( params.server ) ); if params.username then assert ( conn:auth ( params ) ); end end if not typ then -- default key-value store return setmetatable({ store = store }, keyval_store); end; return nil, "unsupported-store"; end module:provides("storage", driver); prosody-modules-c53cc1ae4788/mod_http_stats_stream/0002755000175500017550000000000013164216767022223 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_http_stats_stream/mod_http_stats_stream.lua0000644000175500017550000000176513163400720027323 0ustar debacledebaclelocal statsman = require "core.statsmanager"; local json = require "util.json"; local sessions = {}; local function updates_client_closed(response) module:log("debug", "Streamstats client closed"); sessions[response] = nil; end local function get_updates(event) local request, response = event.request, event.response; response.on_destroy = updates_client_closed; response.conn:write(table.concat({ "HTTP/1.1 200 OK"; "Content-Type: text/event-stream"; "X-Accel-Buffering: no"; -- For nginx maybe? ""; "event: stats-full"; "data: "..json.encode(statsman.get_stats()); ""; ""; }, "\r\n")); sessions[response] = request; return true; end module:hook_global("stats-updated", function (event) local data = table.concat({ "event: stats-updated"; "data: "..json.encode(event.changed_stats); ""; ""; }, "\r\n") for response in pairs(sessions) do response.conn:write(data); end end); module:depends("http"); module:provides("http", { route = { GET = get_updates; } }); prosody-modules-c53cc1ae4788/mod_http_stats_stream/README.markdown0000644000175500017550000000154313163400720024704 0ustar debacledebacle--- depends: - 'mod\_http' provides: - http --- # Introduction This module provides a streaming interface to [Prosodys internal statistics][doc:statistics] via [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). ## Example ```js var evtSource = new EventSource("/stats_stream"); /* * An event with all current statistics in the form of a JSON object. * Normally sent only once, when first connected to the stream. */ evtSource.addEventListener("stats-full", function(e) { var initial_stats = JSON.parse(e.data); console.log(initial_stats); }, false); /* * An event containing only statistics that have changed since the last * 'stats-full' or 'stats-updated' event. */ evtSource.addEventListener("stats-updated", function(e) { var updated_stats = JSON.parse(e.data); console.log(updated_stats); }, false); ``` prosody-modules-c53cc1ae4788/mod_http_stats_stream/example.html0000644000175500017550000000074513163400720024527 0ustar debacledebacle Stats

Glorious statistics!

prosody-modules-c53cc1ae4788/mod_log_sasl_mech/0002755000175500017550000000000013164216767021252 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_log_sasl_mech/mod_log_sasl_mech.lua0000644000175500017550000000036213163400720025371 0ustar debacledebacle module:hook("authentication-success", function (event) local session = event.session; local sasl_handler = session.sasl_handler; session.log("info", "Authenticated with %s", sasl_handler and sasl_handler.selected or "legacy auth"); end); prosody-modules-c53cc1ae4788/mod_s2s_auth_dane/0002755000175500017550000000000013164216767021172 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_s2s_auth_dane/mod_s2s_auth_dane.lua0000644000175500017550000003566713163400720025251 0ustar debacledebacle-- mod_s2s_auth_dane -- Copyright (C) 2013-2014 Kim Alvefur -- -- This file is MIT/X11 licensed. -- -- Implements DANE and Secure Delegation using DNS SRV as described in -- http://tools.ietf.org/html/draft-miller-xmpp-dnssec-prooftype -- -- Known issues: -- Could be done much cleaner if mod_s2s was using util.async -- -- TODO Things to test/handle: -- Negative or bogus answers -- No encryption offered -- Different hostname before and after STARTTLS - mod_s2s should complain -- Interaction with Dialback -- -- luacheck: ignore module module:set_global(); local have_async, async = pcall(require, "util.async"); local noop = function () end local type = type; local t_insert = table.insert; local set = require"util.set"; local dns_lookup = require"net.adns".lookup; local hashes = require"util.hashes"; local base64 = require"util.encodings".base64; local idna_to_ascii = require "util.encodings".idna.to_ascii; local idna_to_unicode = require"util.encodings".idna.to_unicode; local nameprep = require"util.encodings".stringprep.nameprep; local cert_verify_identity = require "util.x509".verify_identity; do local net_dns = require"net.dns"; if not net_dns.types or not net_dns.types[52] then module:log("error", "No TLSA support available, DANE will not be supported"); return end end local pat = "%-%-%-%-%-BEGIN ([A-Z ]+)%-%-%-%-%-\r?\n".. "([0-9A-Za-z=+/\r\n]*)\r?\n%-%-%-%-%-END %1%-%-%-%-%-"; local function pem2der(pem) local typ, data = pem:match(pat); if typ and data then return base64.decode(data), typ; end end local use_map = { ["DANE-EE"] = 3; ["DANE-TA"] = 2; ["PKIX-EE"] = 1; ["PKIX-CA"] = 0 } local implemented_uses = set.new { "DANE-EE", "PKIX-EE" }; do local cert_mt = debug.getregistry()["SSL:Certificate"]; if cert_mt and cert_mt.__index.issued then -- Need cert:issued() for these implemented_uses:add("DANE-TA"); implemented_uses:add("PKIX-CA"); else module:log("debug", "The cert:issued() method is unavailable, DANE-TA and PKIX-CA can't be enabled"); end if not cert_mt.__index.pubkey then module:log("debug", "The cert:pubkey() method is unavailable, the SPKI usage can't be supported"); end end local configured_uses = module:get_option_set("dane_uses", { "DANE-EE", "DANE-TA" }); local enabled_uses = set.intersection(implemented_uses, configured_uses) / function(use) return use_map[use] end; local unsupported = configured_uses - implemented_uses; if not unsupported:empty() then module:log("warn", "Unable to support DANE uses %s", tostring(unsupported)); end -- Find applicable TLSA records -- Takes a s2sin/out and a callback local function dane_lookup(host_session, cb) cb = cb or noop; local log = host_session.log or module._log; if host_session.dane ~= nil then return end -- Has already done a lookup if host_session.direction == "incoming" then if not host_session.from_host then log("debug", "Session doesn't have a 'from' host set"); return; end -- We don't know what hostname or port to use for Incoming connections -- so we do a SRV lookup and then request TLSA records for each SRV -- Most servers will probably use the same certificate on outgoing -- and incoming connections, so this should work well local name = host_session.from_host and idna_to_ascii(host_session.from_host); if not name then log("warn", "Could not convert '%s' to ASCII for DNS lookup", tostring(host_session.from_host)); return; end log("debug", "Querying SRV records from _xmpp-server._tcp.%s.", name); host_session.dane = dns_lookup(function (answer, err) host_session.dane = false; -- Mark that we already did the lookup if not answer then log("debug", "Resolver error: %s", tostring(err)); return cb(host_session); end if answer.bogus then log("warn", "Results are bogus!"); -- Bad sign, probably not a good idea to do any fallback here host_session.dane = answer; elseif not answer.secure then log("debug", "Results are not secure"); return cb(host_session); end local n = answer.n or #answer; if n == 0 then -- No SRV records, synthesize fallback host and port -- this may behave oddly for connections in the other direction if -- mod_s2s doesn't keep the answer around answer[1] = { srv = { target = name, port = 5269 } }; n = 1; elseif n == 1 and answer[1].srv.target == '.' then return cb(host_session); -- No service ... This shouldn't happen? end local srv_hosts = { answer = answer }; host_session.srv_hosts = srv_hosts; local dane; for _, record in ipairs(answer) do t_insert(srv_hosts, record.srv); log("debug", "Querying TLSA record for %s:%d", record.srv.target, record.srv.port); dns_lookup(function(dane_answer) log("debug", "Got answer for %s:%d", record.srv.target, record.srv.port); n = n - 1; -- There are three kinds of answers -- Insecure, Secure and Bogus -- -- We collect Secure answers for later use -- -- Insecure (legacy) answers are simply ignored -- -- If we get a Bogus (dnssec error) reply, keep the -- status around. If there were only bogus replies, the -- connection will be aborted. If there were at least -- one non-Bogus reply, we proceed. If none of the -- replies matched, we consider the connection insecure. if (dane_answer.bogus or dane_answer.secure) and not dane then -- The first answer we care about -- For services with only one SRV record, this will be the only one log("debug", "First secure (or bogus) TLSA") dane = dane_answer; elseif dane_answer.bogus then log("debug", "Got additional bogus TLSA") dane.bogus = dane_answer.bogus; elseif dane_answer.secure then log("debug", "Got additional secure TLSA") for _, dane_record in ipairs(dane_answer) do t_insert(dane, dane_record); end end if n == 0 then if dane then host_session.dane = dane; if #dane > 0 and dane.bogus then -- Got at least one non-bogus reply, -- This should trigger a failure if one of them did not match log("warn", "Ignoring bogus replies"); dane.bogus = nil; end if #dane == 0 and dane.bogus == nil then -- Got no usable data host_session.dane = false; end end return cb(host_session); end end, ("_%d._tcp.%s."):format(record.srv.port, record.srv.target), "TLSA"); end end, "_xmpp-server._tcp."..name..".", "SRV"); return true; elseif host_session.direction == "outgoing" then -- Prosody has already done SRV lookups for outgoing session, so check if those are secure local srv_hosts = host_session.srv_hosts; if not ( srv_hosts and srv_hosts.answer and srv_hosts.answer.secure ) then return; -- No secure SRV records, fall back to non-DANE mode -- Empty response were not kept by older mod_s2s/s2sout end -- Do TLSA lookup for currently selected SRV record local srv_choice = srv_hosts[host_session.srv_choice or 0] or { target = idna_to_ascii(host_session.to_host), port = 5269 }; log("debug", "Querying TLSA record for %s:%d", srv_choice.target, srv_choice.port); host_session.dane = dns_lookup(function(answer) if answer and ((answer.secure and #answer > 0) or answer.bogus) then srv_choice.dane = answer; else srv_choice.dane = false; end host_session.dane = srv_choice.dane; return cb(host_session); end, ("_%d._tcp.%s."):format(srv_choice.port, srv_choice.target), "TLSA"); return true; end end local function pause(host_session) host_session.log("debug", "Pausing connection until DANE lookup is completed"); host_session.conn:pause() end local function resume(host_session) host_session.log("debug", "DANE lookup completed, resuming connection"); host_session.conn:resume() end if have_async then function pause(host_session) host_session.log("debug", "Pausing connection until DANE lookup is completed"); local wait, done = async.waiter(); host_session._done_waiting_for_dane = done; wait(); end local function _resume(_, host_session) if host_session._done_waiting_for_dane then host_session.log("debug", "DANE lookup completed, resuming connection"); host_session._done_waiting_for_dane(); host_session._done_waiting_for_dane = nil; end end function resume(host_session) -- Something about the way luaunbound calls callbacks is messed up if host_session._done_waiting_for_dane then module:add_timer(0, _resume, host_session); end end end function module.add_host(module) local function on_new_s2s(event) local host_session = event.origin; if host_session.type == "s2sout" or host_session.type == "s2sin" then return; -- Already authenticated end if host_session.dane ~= nil then return; -- Already done DANE lookup end dane_lookup(host_session, resume); -- Let it run in parallell until we need to check the cert end -- New outgoing connections module:hook("stanza/http://etherx.jabber.org/streams:features", on_new_s2s, 501); module:hook("s2sout-authenticate-legacy", on_new_s2s, 200); -- New incoming connections module:hook("s2s-stream-features", on_new_s2s, 10); module:hook("s2s-authenticated", function(event) local session = event.session; if session.dane and type(session.dane) == "table" and next(session.dane) ~= nil and not session.secure then -- TLSA record but no TLS, not ok. -- TODO Optional? -- Bogus replies should trigger this path -- How does this interact with Dialback? session:close({ condition = "policy-violation", text = "Encrypted server-to-server communication is required but was not " ..((session.direction == "outgoing" and "offered") or "used") }); return false; end -- Cleanup session.srv_hosts = nil; end); end -- Compare one TLSA record against a certificate local function one_dane_check(tlsa, cert, log) local select, match, certdata = tlsa.select, tlsa.match; if select == 0 then certdata = pem2der(cert:pem()); elseif select == 1 and cert.pubkey then certdata = pem2der(cert:pubkey()); else log("warn", "DANE selector %s is unsupported", tlsa:getSelector() or select); return; end if match == 1 then certdata = hashes.sha256(certdata); elseif match == 2 then certdata = hashes.sha512(certdata); elseif match ~= 0 then log("warn", "DANE match rule %s is unsupported", tlsa:getMatchType() or match); return; end if #certdata ~= #tlsa.data then log("warn", "Length mismatch: Cert: %d, TLSA: %d", #certdata, #tlsa.data); end return certdata == tlsa.data; end module:hook("s2s-check-certificate", function(event) local session, cert, host = event.session, event.cert, event.host; if not cert then return end local log = session.log or module._log; local dane = session.dane; if type(dane) ~= "table" then if dane == nil and dane_lookup(session, resume) then pause(session); dane = session.dane; end end if type(dane) == "table" then local match_found, supported_found; for i = 1, #dane do local tlsa = dane[i].tlsa; log("debug", "TLSA #%d: %s", i, tostring(tlsa)) local use = tlsa.use; if enabled_uses:contains(use) then -- DANE-EE or PKIX-EE if use == 3 or use == 1 then -- Should we check if the cert subject matches? local is_match = one_dane_check(tlsa, cert, log); if is_match ~= nil then supported_found = true; end if is_match and use == 1 and session.cert_chain_status ~= "valid" then -- for usage 1, PKIX-EE, the chain has to be valid already log("debug", "PKIX-EE TLSA matches untrusted certificate"); is_match = false; end if is_match then log("info", "DANE validated ok for %s using %s", host, tlsa:getUsage()); session.cert_identity_status = "valid"; if use == 3 then -- DANE-EE, chain status equals DNSSEC chain status session.cert_chain_status = "valid"; end match_found = true; dane.matching = tlsa; break; end -- DANE-TA or PKIX-CA elseif use == 2 or use == 0 then supported_found = true; local chain = session.conn:socket():getpeerchain(); for c = 1, #chain do local cacert = chain[c]; local is_match = one_dane_check(tlsa, cacert, log); if is_match ~= nil then supported_found = true; end if is_match and not cacert:issued(cert, unpack(chain)) then is_match = false; end if is_match and use == 0 and session.cert_chain_status ~= "valid" then -- for usage 0, PKIX-CA, identity and chain has to be valid already is_match = false; end if is_match then log("info", "DANE validated ok for %s using %s", host, tlsa:getUsage()); if use == 2 then -- DANE-TA session.cert_identity_status = "valid"; if cert_verify_identity(host, "xmpp-server", cert) then session.cert_chain_status = "valid"; -- else -- TODO Check against SRV target? end end match_found = true; dane.matching = tlsa; break; end end if match_found then break end end end end if supported_found and not match_found or dane.bogus then -- No TLSA matched or response was bogus local why = "No TLSA matched certificate"; if dane.bogus then why = "Bogus: "..tostring(dane.bogus); end log("warn", "DANE validation failed for %s: %s", host, why); session.cert_identity_status = "invalid"; session.cert_chain_status = "invalid"; end else if session.cert_chain_status == "valid" and session.cert_identity_status ~= "valid" and session.srv_hosts and session.srv_hosts.answer and session.srv_hosts.answer.secure then local srv_hosts, srv_choice, srv_target = session.srv_hosts, session.srv_choice; for i = srv_choice or 1, srv_choice or #srv_hosts do srv_target = session.srv_hosts[i].target:gsub("%.?$",""); log("debug", "Comparing certificate with Secure SRV target %s", srv_target); srv_target = nameprep(idna_to_unicode(srv_target)); if srv_target and cert_verify_identity(srv_target, "xmpp-server", cert) then log("info", "Certificate for %s matches Secure SRV target %s", host, srv_target); session.cert_identity_status = "valid"; return; end end end end end); -- Telnet command if module:get_option_set("modules_enabled", {}):contains("admin_telnet") then module:depends("admin_telnet"); -- Make sure the env is there local def_env = module:shared("admin_telnet/env"); local function annotate(session, line) line = line or {}; table.insert(line, "--"); if session.dane == nil then table.insert(line, "No DANE attempted, probably insecure SRV response"); elseif session.dane == false then table.insert(line, "DANE failed or response was insecure"); elseif type(session.dane) ~= "table" then table.insert(line, "Waiting for DANE records..."); elseif session.dane.matching then table.insert(line, "Matching DANE record:\n| " .. tostring(session.dane.matching)); else table.insert(line, "DANE records:\n| " .. tostring(session.dane)); end return table.concat(line, " "); end function def_env.s2s:show_dane(...) return self:show(..., annotate); end end prosody-modules-c53cc1ae4788/mod_s2s_auth_dane/README.markdown0000644000175500017550000000544413163400720023657 0ustar debacledebacle--- labels: - 'Stage-Alpha' - 'Type-S2SAuth' summary: S2S authentication using DANE ... Introduction ============ This module implements DANE as described in [Using DNS Security Extensions (DNSSEC) and DNS-based Authentication of Named Entities (DANE) as a Prooftype for XMPP Domain Name Associations](http://tools.ietf.org/html/draft-miller-xmpp-dnssec-prooftype). Dependencies ============ This module requires a DNSSEC aware DNS resolver. Prosodys internal DNS module does not support DNSSEC. Therefore, to use this module, a replacement is needed, such as [this one](https://www.zash.se/luaunbound.html). LuaSec 0.5 or later is also required. Configuration ============= After [installing the module][doc:installing\_modules], just add it to `modules_enabled`; modules_enabled = { ... "s2s_auth_dane"; } DANE Uses --------- By default, only DANE uses are enabled. dane_uses = { "DANE-EE", "DANE-TA" } Use flag Description ----------- ------------------------------------------------------------------------------------------------------- `DANE-EE` Most simple use, usually a fingerprint of the full certificate or public key used the service `DANE-TA` Fingerprint of a certificate or public key that has been used to issue the service certificate `PKIX-EE` Like `DANE-EE` but the certificate must also pass normal PKIX trust checks (ie standard certificates) `PKIX-TA` Like `DANE-TA` but must also pass normal PKIX trust checks (ie standard certificates) DNS Setup ========= In order for other services to verify your site using using this plugin, you need to publish TLSA records (and they need to have this plugin). Here's an example using `DANE-EE Cert SHA2-256` for a host named `xmpp.example.com` serving the domain `example.com`. $ORIGIN example.com. ; Your standard SRV record _xmpp-server._tcp.example.com IN SRV 0 0 5269 xmpp.example.com. ; IPv4 and IPv6 addresses xmpp.example.com. IN A 192.0.2.68 xmpp.example.com. IN AAAA 2001:0db8:0000:0000:4441:4e45:544c:5341 ; The DANE TLSA records. _5269._tcp.xmpp.example.com. 300 IN TLSA 3 0 1 E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 ; If your zone file tooling does not support TLSA records, you can try the raw binary format: _5269._tcp.xmpp.example.com. 300 IN TYPE52 \# 35 030001E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 [List of DNSSEC and DANE tools](http://www.internetsociety.org/deploy360/dnssec/tools/) Further reading =============== - [DANE Operational Guidance][rfc7671] Compatibility ============= Requires 0.9 or above. Known issues ============ - A race condition between the DANE lookup and completion of the TLS handshake may cause a crash. This does not happen in **trunk** thanks to better async support. prosody-modules-c53cc1ae4788/mod_auto_accept_subscriptions/0002755000175500017550000000000013164216767023731 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_auto_accept_subscriptions/mod_auto_accept_subscriptions.lua0000644000175500017550000000416413163400720032533 0ustar debacledebaclelocal rostermanager = require "core.rostermanager"; local jid = require "util.jid"; local st = require "util.stanza"; local core_post_stanza = prosody.core_post_stanza; local function handle_inbound_subscription_request(origin, stanza) local to_bare, from_bare = jid.bare(stanza.attr.to), jid.bare(stanza.attr.from); local node, host = jid.split(to_bare); stanza.attr.from, stanza.attr.to = from_bare, to_bare; module:log("info", "Auto-accepting inbound subscription request from %s to %s", tostring(from_bare), tostring(to_bare)); if not rostermanager.is_contact_subscribed(node, host, from_bare) then core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt module:log("debug", "receipt acknowledged"); if rostermanager.set_contact_pending_in(node, host, from_bare) then module:log("debug", "set pending in"); if rostermanager.subscribed(node, host, from_bare) then module:log("debug", "set subscribed"); rostermanager.roster_push(node, host, to_bare); module:log("debug", "pushed roster item"); local subscribed_stanza = st.reply(stanza); subscribed_stanza.attr.type = "subscribed"; core_post_stanza(hosts[host], subscribed_stanza); module:log("debug", "sent subscribed"); hosts[host].modules.presence.send_presence_of_available_resources(node, host, to_bare, origin); module:log("debug", "sent available presence of all resources"); -- Add return subscription from user to contact local subscribe_stanza = st.reply(stanza); subscribe_stanza.attr.type = "subscribe"; if rostermanager.set_contact_pending_out(node, host, from_bare) then rostermanager.roster_push(node, host, from_bare); end core_post_stanza(hosts[host], subscribe_stanza); return true; end end end module:log("warn", "Failed to auto-accept subscription request from %s to %s", tostring(from_bare), tostring(to_bare)); end module:hook("presence/bare", function (event) local stanza = event.stanza; if stanza.attr.type == "subscribe" then handle_inbound_subscription_request(event.origin, stanza); return true; end end, 0.1); prosody-modules-c53cc1ae4788/mod_auto_accept_subscriptions/README.markdown0000644000175500017550000000157713163400720026421 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: Automatically accept incoming subscription requests on behalf of users ... Introduction ============ In some environments where all users on the system have mutual trust in each other, it's sometimes fine to skip the usual authorization process to add someone to your contact list and see their status. This module sets Prosody to automatically accept incoming subscription authorization requests, and add the contact to the user's contact list, without intervention from the user. Configuration ============= Simply add the module to your modules\_enabled list like any other module: modules_enabled = { ... "auto_accept_subscriptions"; ... } This module has no further configuration. Compatibility ============= ------- ------- trunk Works 0.9 Works 0.8 Works ------- ------- prosody-modules-c53cc1ae4788/mod_http_hostaliases/0002755000175500017550000000000013164216767022031 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_http_hostaliases/mod_http_hostaliases.lua0000644000175500017550000000101213163400720026720 0ustar debacledebaclemodule:set_global(); local host_map = { }; module:wrap_object_event(require "net.http.server"._events, false, function (handlers, event_name, event_data) local verb, host, path = event_name:match("^(%w+ )(.-)(/.*)"); host = host_map[host]; event_name = verb .. host .. path; return handlers(event_name, event_data); end); function module.add_host(module) local http_host = module:get_option_string("http_host"); for host in module:get_option_set("http_host_aliases", {}) do host_map[host] = http_host; end end prosody-modules-c53cc1ae4788/mod_adhoc_blacklist/0002755000175500017550000000000013164216767021561 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_adhoc_blacklist/mod_adhoc_blacklist.lua0000644000175500017550000000474613163400720026221 0ustar debacledebacle-- mod_adhoc_blacklist -- -- http://xmpp.org/extensions/xep-0133.html#edit-blacklist -- -- Copyright (C) 2015 Kim Alvefur -- -- This file is MIT/X11 licensed. -- module:depends("adhoc"); local adhoc = module:require "adhoc"; local st = require"util.stanza"; local set = require"util.set"; local dataform = require"util.dataforms"; local adhoc_inital_data = require "util.adhoc".new_initial_data_form; local blocklist_form = dataform.new { title = "Editing the Blacklist"; instructions = "Fill out this form to edit the list of entities with whom communications are disallowed."; { type = "hidden"; name = "FORM_TYPE"; value = "http://jabber.org/protocol/admin"; }; { type = "jid-multi"; name = "blacklistjids"; label = "The blacklist"; }; } local blocklists = module:open_store("blocklist"); local blocklist_handler = adhoc_inital_data(blocklist_form, function () local blacklistjids = {}; local blacklist = blocklists:get(); if blacklist then for jid in pairs(blacklist) do table.insert(blacklistjids, jid); end end return { blacklistjids = blacklistjids }; end, function(fields, form_err) if form_err then return { status = "completed", error = { message = "Problem in submitted form" } }; end local blacklistjids = set.new(fields.blacklistjids); local ok, err = blocklists:set(nil, blacklistjids._items); if ok then return { status = "completed", info = "Blacklist updated" }; else return { status = "completed", error = { message = "Error saving blacklist: "..err } }; end end); module:add_item("adhoc", adhoc.new("Edit Blacklist", "http://jabber.org/protocol/admin#edit-blacklist", blocklist_handler, "admin")); local function is_blocked(host) local blacklistjids = blocklists:get(); return blacklistjids and blacklistjids[host]; end module:hook("route/remote", function (event) local origin, stanza = event.origin, event.stanza; if is_blocked(event.to_host) then if origin and stanza then origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Communication with this domain is not allowed")); return true; end return false; end end, 1000); module:hook("s2s-stream-features", function (event) local session = event.origin; if is_blocked(session.from_host) then session:close("policy-violation"); return false; end end, 1000); module:hook("stanza/http://etherx.jabber.org/streams:features", function (event) local session = event.origin; if is_blocked(session.to_host) then session:close("policy-violation"); return true; end end, 1000); prosody-modules-c53cc1ae4788/mod_adhoc_blacklist/README.markdown0000644000175500017550000000076013163400720024242 0ustar debacledebacle--- summary: 'Block remote servers via ad-hoc command' ... Introduction ============ This module provides the *Edit Blacklist* ad-hoc command described in [XEP-0133](http://xmpp.org/extensions/xep-0133.html#edit-blacklist) and also performs the actual blocking of incoming and outgoing server-to-server connections. Using ===== In your client, simply select the Edit Blacklist command from the list of adhoc commands. E.g. in Pidgin, this is under *Accounts -\> (your account)* in the menu. prosody-modules-c53cc1ae4788/mod_register_dnsbl/0002755000175500017550000000000013164216767021461 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_register_dnsbl/README.markdown0000644000175500017550000000047413163400720024144 0ustar debacledebacleIntroduction ============ This module checks the IP address of newly registered users against a DNS block list. If a positive match is found, it gets logged. Configuration ============= Option Type Default ------------------- -------- ------------ registration\_rbl string *Required* prosody-modules-c53cc1ae4788/mod_register_dnsbl/mod_register_dnsbl.lua0000644000175500017550000000137713163400720026016 0ustar debacledebaclelocal adns = require "net.adns"; local rbl = module:get_option_string("registration_rbl"); local function reverse(ip, suffix) local a,b,c,d = ip:match("^(%d+).(%d+).(%d+).(%d+)$"); if not a then return end return ("%d.%d.%d.%d.%s"):format(d,c,b,a, suffix); end -- TODO async -- module:hook("user-registering", function (event) end); module:hook("user-registered", function (event) local session = event.session; local ip = session and session.ip; local rbl_ip = ip and reverse(ip, rbl); if rbl_ip then local log = session.log; adns.lookup(function (reply) if reply and reply[1] then log("warn", "Account %s@%s registered from IP %s found in RBL (%s)", event.username, event.host or module.host, ip, reply[1].a); end end, rbl_ip); end end); prosody-modules-c53cc1ae4788/mod_disable_tls/0002755000175500017550000000000013164216767020740 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_disable_tls/mod_disable_tls.lua0000644000175500017550000000052413163400720024545 0ustar debacledebaclelocal disable_tls_ports = module:get_option_set("disable_tls_ports", {}); module:hook("stream-features", function (event) if disable_tls_ports:contains(event.origin.conn:serverport()) then module:log("error", "Disabling TLS for client on port %d", event.origin.conn:serverport()); event.origin.conn.starttls = false; end end, 1000); prosody-modules-c53cc1ae4788/mod_disable_tls/README.markdown0000644000175500017550000000113413163400720023415 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: Disable TLS on certain client ports ... Introduction ============ This module can be used to prevent Prosody from offering TLS on client ports that you specify. This can be useful to work around buggy clients when transport security is not required. Configuration ============= Load the module, and set `disable_tls_ports` to a list of ports: disable_tls_ports = { 5322 } Don't forget to add any extra ports to c2s\_ports, so that Prosody is actually listening for connections! Compatibility ============= ----- ------- 0.9 Works ----- ------- prosody-modules-c53cc1ae4788/mod_pep_vcard_png_avatar/0002755000175500017550000000000013164216767022620 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_pep_vcard_png_avatar/README.markdown0000644000175500017550000000142413163400720025277 0ustar debacledebacleIntroduction ============ Conversations (an XMPP client for Android) is publishing PEP avatars in the webp file format. However Pidgin and other XMPP desktop clients can only show vcard avatars, that are in the PNG file format. This module is the [mod_pep_vcard_avatar](https://modules.prosody.im/mod_pep_vcard_avatar.html) module extended to also change the avatar file format to PNG. This module needs `dwebp` from `webp` package as an additional dependency. Configuration ============= Enable the module as any other: modules_enabled = { "mod_pep_vcard_png_avatar"; } You MUSTN'T load mod\_pep\_vcard\_avatar if this module is loaded. Compatibility ============= ----- ------------- trunk Works 0.10 Should work 0.9 Should work ----- ------------- prosody-modules-c53cc1ae4788/mod_pep_vcard_png_avatar/mod_pep_vcard_png_avatar.lua0000644000175500017550000001241313163400720030305 0ustar debacledebacle-- Prosody IM -- Copyright (C) 2008-2014 Matthew Wild -- Copyright (C) 2008-2014 Waqas Hussain -- Copyright (C) 2014 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "util.stanza" local jid = require "util.jid"; local base64 = require"util.encodings".base64; local sha1 = require"util.hashes".sha1; local mod_pep = module:depends"pep"; local pep_data = mod_pep.module.save().data; module:add_feature("http://prosody.im/protocol/vcard-pep-integration"); module:depends"vcard"; local vcard_storage = module:open_store("vcard"); local function get_vcard(username) local vcard, err = vcard_storage:get(username); if vcard then vcard = st.deserialize(vcard); end if not vcard then vcard = st.stanza("vCard", { xmlns = "vcard-temp" }); end return vcard, err; end local function replace_tag(s, replacement) local once = false; s:maptags(function (tag) if tag.name == replacement.name and tag.attr.xmlns == replacement.attr.xmlns then if not once then once = true; return replacement; else return nil; end end return tag; end); if not once then s:add_child(replacement); end end local function set_vcard(username, vcard) if vcard then vcard = st.preserialize(st.clone(vcard)); end return vcard_storage:set(username, vcard); end local function publish(session, node, id, item) return module:fire_event("pep-publish-item", { actor = true, user = jid.bare(session.full_jid), session = session, node = node, id = id, item = item; }); end -- vCard -> PEP local function update_pep(session, vcard) if not vcard then return end local nickname = vcard:get_child_text("NICKNAME"); if nickname then publish(session, "http://jabber.org/protocol/nick", "current", st.stanza("item", {id="current"}) :tag("nick", { xmlns="http://jabber.org/protocol/nick" }):text(nickname)); end local photo = vcard:get_child("PHOTO"); if photo then local photo_type = photo:get_child_text("TYPE"); local photo_b64 = photo:get_child_text("BINVAL"); local photo_raw = photo_b64 and base64.decode(photo_b64); if photo_raw and photo_type then -- Else invalid data or encoding local photo_hash = sha1(photo_raw, true); publish(session, "urn:xmpp:avatar:data", photo_hash, st.stanza("item", {id=photo_hash}) :tag("data", { xmlns="urn:xmpp:avatar:data" }):text(photo_b64)); publish(session, "urn:xmpp:avatar:metadata", photo_hash, st.stanza("item", {id=photo_hash}) :tag("metadata", { xmlns="urn:xmpp:avatar:metadata" }) :tag("info", { id = photo_hash, bytes = tostring(#photo_raw), type = photo_type,})); end end end local function handle_vcard(event) local session, stanza = event.origin, event.stanza; if not stanza.attr.to and stanza.attr.type == "set" then return update_pep(session, stanza:get_child("vCard", "vcard-temp")); end end module:hook("iq/bare/vcard-temp:vCard", handle_vcard, 1); -- PEP Avatar -> vCard local function on_publish_metadata(event) local username = event.session.username; local metadata = event.item:find("{urn:xmpp:avatar:metadata}metadata/info"); if not metadata then module:log("error", "No info found"); module:log("debug", event.item:top_tag()); return; end module:log("debug", metadata:top_tag()); local user_data = pep_data[username.."@"..module.host]; local pep_photo = user_data["urn:xmpp:avatar:data"]; pep_photo = pep_photo and pep_photo[1] == metadata.attr.id and pep_photo[2]; if not pep_photo then module:log("error", "No photo found"); return; end -- Publishing in the wrong order? local image=pep_photo:get_child_text("data", "urn:xmpp:avatar:data"); if pep_photo and metadata.attr.type == "image/webp" then local file_webp = io.open("/tmp/Prosody_temp_avatar.webp", "w"); file_webp:write(base64.decode(pep_photo:get_child_text("data", "urn:xmpp:avatar:data"))); file_webp:close(); os.execute("dwebp /tmp/Prosody_temp_avatar.webp -o /tmp/Prosody_temp_avatar.png"); local file_png = io.open("/tmp/Prosody_temp_avatar.png", "r"); if file_png ~= nil then image=base64.encode(file_png:read("*a")); file_png:close(); else module:log("error", "Couldn't access /tmp/Prosody_temp_avatar.png. Are you sure that /tmp is readable and writable and that Prosody can execute the dwebp command?"); end os.remove("/tmp/Prosody_temp_avatar.webp"); os.remove("/tmp/Prosody_temp_avatar.png"); end local vcard = get_vcard(username); local new_photo = st.stanza("PHOTO", { xmlns = "vcard-temp" }) :tag("TYPE"):text(metadata.attr.type):up() :tag("BINVAL"):text(image); replace_tag(vcard, new_photo); set_vcard(username, vcard); end -- PEP Nickname -> vCard local function on_publish_nick(event) local username = event.session.username; local vcard = get_vcard(username); local new_nick = st.stanza("NICKNAME", { xmlns = "vcard-temp" }) :text(event.item:get_child_text("nick", "http://jabber.org/protocol/nick")); replace_tag(vcard, new_nick); set_vcard(username, vcard); end local function on_publish(event) if event.actor == true then return end -- Not from a client local node = event.node; if node == "urn:xmpp:avatar:metadata" then return on_publish_metadata(event); elseif node == "http://jabber.org/protocol/nick" then return on_publish_nick(event); end end module:hook("pep-publish-item", on_publish, 1); prosody-modules-c53cc1ae4788/mod_group_bookmarks/0002755000175500017550000000000013164216767021657 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_group_bookmarks/mod_group_bookmarks.lua0000644000175500017550000000632713163400720026412 0ustar debacledebacle-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "util.stanza" local datamanager = require "util.datamanager" local jid, datamanager = require "util.jid", require "util.datamanager"; local jid_bare, jid_prep, jid_split = jid.bare, jid.prep, jid.split; local module_host = module:get_host(); local rooms; local members; local bookmarks_file; module:add_feature("jabber:iq:private"); function inject_bookmarks(username, host, data) local jid = username.."@"..host; data:reset(); if members[jid] then for _, room in ipairs(members[jid]) do data:tag("conference", { name = room; jid = room; autojoin = "1"; }); local nick = rooms[room][jid]; if nick then data:tag("nick"):text(nick):up(); end data:up(); end end return data; end module:hook("iq/self/jabber:iq:private:query", function(event) local origin, stanza = event.origin, event.stanza; local type = stanza.attr.type; local query = stanza.tags[1]; if #query.tags == 1 then local tag = query.tags[1]; local key = tag.name..":"..tag.attr.xmlns; local data, err = datamanager.load(origin.username, origin.host, "private"); if err then origin.send(st.error_reply(stanza, "wait", "internal-server-error")); return true; end if stanza.attr.type == "get" then local data = data and data[key]; if (not data) and key == "storage:storage:bookmarks" then data = st.stanza("storage", { xmlns = "storage:bookmarks" }); end if data then data = st.deserialize(data); if key == "storage:storage:bookmarks" then data = inject_bookmarks(origin.username, origin.host, data); end origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"}) :add_child(data)); else origin.send(st.reply(stanza):add_child(stanza.tags[1])); end return true; end end end, 1); function module.load() bookmarks_file = module:get_option_string("group_bookmarks_file"); rooms = { default = {} }; members = { }; if not bookmarks_file then module:log("error", "Please specify group_bookmarks_file in your configuration"); return; end local curr_room; for line in io.lines(bookmarks_file) do if line:match("^%s*%[.-%]%s*$") then curr_room = line:match("^%s*%[(.-)%]%s*$"); if curr_room:match("^%+") then curr_room = curr_room:gsub("^%+", ""); if not members[false] then members[false] = {}; end members[false][#members[false]+1] = curr_room; -- Is a public group end module:log("debug", "New group: %s", tostring(curr_room)); rooms[curr_room] = rooms[curr_room] or {}; elseif curr_room then -- Add JID local entryjid, name = line:match("([^=]*)=?(.*)"); module:log("debug", "entryjid = '%s', name = '%s'", entryjid, name); local jid; jid = jid_prep(entryjid:match("%S+")); if jid then module:log("debug", "New member of %s: %s", tostring(curr_room), tostring(jid)); rooms[curr_room][jid] = name or false; members[jid] = members[jid] or {}; members[jid][#members[jid]+1] = curr_room; end end end module:log("info", "Group bookmarks loaded successfully"); end prosody-modules-c53cc1ae4788/mod_group_bookmarks/README.markdown0000644000175500017550000000354413163400720024343 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: 'mod\_groups for chatrooms' ... Introduction ============ [mod\_groups](http://prosody.im/doc/modules/mod_groups) allows you to insert contacts into users' contact lists. Well mod\_group\_bookmarks allows you to insert chatrooms into the user's bookmarks. These are fetched by their client and automatically joined when the log in. In short, if you want to automatically join users to rooms when they sign in, this is the module you want. Details ======= Most clients support storing a private list of room "bookmarks" on the server. When they log in, they fetch this list and join any that are marked as "autojoin". Without affecting normal usage of the bookmarks store this module dynamically inserts custom rooms into users' bookmarks lists. Usage ===== Similar to [mod\_groups](http://prosody.im/doc/modules/mod_groups), you need to make a text file in this format: [room@conferenceserver] user1@example.com=User 1 user2@example.com=User 2 [otherroom@conferenceserver] user3@example.net=User 3 Add "group\_bookmarks" to your modules\_enabled list: modules_enabled = { -- ...other modules here... -- "group_bookmarks"; -- ...maybe some more here... -- } Configuration ============= ------------------------ --------------------------------------------------- group\_bookmarks\_file The path to the text file you created (as above). ------------------------ --------------------------------------------------- Compatibility ============= ----- ------------- 0.8 Works 0.7 Should work 0.6 Should work ----- ------------- Todo ==== - Support for injecting into ALL users bookmarks, without needing a list - Allow turning off the autojoin flag - Perhaps support a friendly name for the bookmark (currently uses the room address) prosody-modules-c53cc1ae4788/mod_s2soutinjection/0002755000175500017550000000000013164216767021615 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_s2soutinjection/mod_s2soutinjection.lua0000644000175500017550000000340613163400720026301 0ustar debacledebaclelocal st = require"util.stanza"; local new_ip = require"util.ip".new_ip; local new_outgoing = require"core.s2smanager".new_outgoing; local bounce_sendq = module:depends"s2s".route_to_new_session.bounce_sendq; local s2sout = module:depends"s2s".route_to_new_session.s2sout; local injected = module:get_option("s2s_connect_overrides"); local function isip(addr) return not not (addr and addr:match("^%d+%.%d+%.%d+%.%d+$") or addr:match("^[%x:]*:[%x:]-:[%x:]*$")); end module:hook("route/remote", function(event) local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza; local inject = injected and injected[to_host]; if not inject then return end log("debug", "opening a new outgoing connection for this stanza"); local host_session = new_outgoing(from_host, to_host); -- Store in buffer host_session.bounce_sendq = bounce_sendq; host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} }; log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name)); local ip_hosts, srv_hosts = {}, {}; host_session.srv_hosts = srv_hosts; host_session.srv_choice = 0; if type(inject) == "string" then inject = { inject } end for _, item in ipairs(inject) do local host, port = item[1] or item, tonumber(item[2]) or 5269; if isip(host) then ip_hosts[#ip_hosts+1] = { ip = new_ip(host), port = port } else srv_hosts[#srv_hosts+1] = { target = host, port = port } end end if #ip_hosts > 0 then host_session.ip_hosts = ip_hosts; host_session.ip_choice = 0; -- Incremented by try_next_ip s2sout.try_next_ip(host_session); return true; end return s2sout.try_connect(host_session, host_session.srv_hosts[1].target, host_session.srv_hosts[1].port); end, -2); prosody-modules-c53cc1ae4788/mod_s2soutinjection/README.markdown0000644000175500017550000000100213163400720024264 0ustar debacledebacle--- summary: S2S connection override ... # Introduction This module is similar to [mod\_srvinjection] but less of an hack. # Configuration ``` lua -- In the global section modules_enabled = { --- your other modules "s2soutinjection"; } s2s_connect_overrides = { -- This one will use the default port, 5269 ["example.com"] = "xmpp.server.local"; -- To set a different port: ["another.example"] = { "non-standard-port.example", 9999 }; } ``` # Compatibility Requires 0.9.x or later. prosody-modules-c53cc1ae4788/mod_muc_log_http/0002755000175500017550000000000013164216767021137 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/0002755000175500017550000000000013164216767023623 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/0002755000175500017550000000000013164216767025110 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/0002755000175500017550000000000013164216767026607 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/doc.html0000644000175500017550000000761713163400720030232 0ustar debacledebacle muc_log ###BODY_STUFF### prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/month_emptyDay.html0000644000175500017550000000003713163400720032453 0ustar debacledebacle  prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_title.html0000644000175500017550000000001413163400720031423 0ustar debacledebacle###TITLE### prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/month_weekDay.html0000644000175500017550000000010513163400720032244 0ustar debacledebacle ###DAY###././@LongLink0000644000000000000000000000015100000000000011600 Lustar rootrootprosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_presence_statusChange.htmlprosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_presence_statusChange.0000644000175500017550000000024213163400720033735 0ustar debacledebacle
###TIME_STUFF### *** ###NICK### shows as "###SHOW###"###STATUS_STUFF###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/year_title.html0000644000175500017550000000010113163400720031603 0ustar debacledebacle prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/days_rooms_bit.html0000644000175500017550000000005613163400720032470 0ustar debacledebacle###ROOM###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_bann.html0000644000175500017550000000014213163400720031222 0ustar debacledebacle###TIME_STUFF### *** ###VICTIM### got banned###REASON_STUFF###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_messageMe.html0000644000175500017550000000011313163400720032210 0ustar debacledebacle###TIME_STUFF###*###NICK### ###MSG###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_message.html0000644000175500017550000000012413163400720031730 0ustar debacledebacle###TIME_STUFF###<###NICK###> ###MSG###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_reason.html0000644000175500017550000000003713163400720031576 0ustar debacledebacle, the reason was "###REASON###"prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_kick.html0000644000175500017550000000014213163400720031225 0ustar debacledebacle###TIME_STUFF### *** ###VICTIM### got kicked###REASON_STUFF###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/rooms_body.html0000644000175500017550000000012213163400720031621 0ustar debacledebacle

Available rooms on ###COMPONENT###:


###ROOMS_STUFF###


prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/month_day.html0000644000175500017550000000010113163400720031424 0ustar debacledebacle ###DAY###prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/days_body.html0000644000175500017550000000110313163400720031422 0ustar debacledebacle
###SINCE### - ###TO###
###JID###
List of rooms for this component
###ROOMS###
###ROOMTOPIC###
###DAYS_STUFF###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/days_bit.html0000644000175500017550000000005513163400720031250 0ustar debacledebacle###DAY###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_bodynp.html0000644000175500017550000000105513163400720031603 0ustar debacledebacle
###DATE###
###JID###
###CALENDAR###
###TITLE_STUFF###

###DAY_STUFF###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_presence_join.html0000644000175500017550000000020313163400720033125 0ustar debacledebacle
###TIME_STUFF### *** ###NICK### has joined the room
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/month_header.html0000644000175500017550000000020013163400720032077 0ustar debacledebacle###WEEKDAYS### prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/rooms_bit.html0000644000175500017550000000005313163400720031445 0ustar debacledebacle###ROOM###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_time.html0000644000175500017550000000016313163400720031245 0ustar debacledebacle[###TIME###] prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_body.html0000644000175500017550000000160613163400720031247 0ustar debacledebacle
###DATE###
###JID###
###CALENDAR###
###TITLE_STUFF###

###DAY_STUFF###
././@LongLink0000644000000000000000000000014700000000000011605 Lustar rootrootprosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_presence_statusText.htmlprosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_presence_statusText.ht0000644000175500017550000000005113163400720034026 0ustar debacledebacle and his status message is "###STATUS###"prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_dayLink.html0000644000175500017550000000006313163400720031701 0ustar debacledebacle###TEXT### prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_presence_leave.html0000644000175500017550000000020213163400720033261 0ustar debacledebacle
###TIME_STUFF### *** ###NICK### has left the room
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/month_footer.html0000644000175500017550000000002013163400720032145 0ustar debacledebacle
###TITLE###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/components_bit.html0000644000175500017550000000006413163400720032475 0ustar debacledebacle###COMPONENT###
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/day_titleChange.html0000644000175500017550000000015513163400720032537 0ustar debacledebacle###TIME_STUFF### *** ###NICK### changed the title to "###TITLE###"
prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/themes/prosody/components_body.html0000644000175500017550000000011713163400720032653 0ustar debacledebacle

Available Multi-User Chats:


###COMPONENTS_STUFF###


prosody-modules-c53cc1ae4788/mod_muc_log_http/muc_log_http/mod_muc_log_http.lua0000644000175500017550000005344713163400720027643 0ustar debacledebaclemodule:depends("http"); local prosody = prosody; local hosts = prosody.hosts; local my_host = module:get_host(); local strchar = string.char; local strformat = string.format; local split_jid = require "util.jid".split; local config_get = require "core.configmanager".get; local urldecode = require "net.http".urldecode; local http_event = require "net.http.server".fire_event; local datamanager = require"core.storagemanager".olddm; local data_load, data_getpath = datamanager.load, datamanager.getpath; local datastore = "muc_log"; local url_base = "muc_log"; local config = nil; local table, tostring, tonumber = table, tostring, tonumber; local os_date, os_time = os.date, os.time; local str_format = string.format; local io_open = io.open; local themes_parent = (module.path and module.path:gsub("[/\\][^/\\]*$", "") or (prosody.paths.plugins or "./plugins") .. "/muc_log_http") .. "/themes"; local lom = require "lxp.lom"; local lfs = require "lfs"; local html = {}; local theme; -- Helper Functions local p_encode = datamanager.path_encode; local function store_exists(node, host, today) if lfs.attributes(data_getpath(node, host, datastore .. "/" .. today), "mode") then return true; else return false; end end -- Module Definitions local function html_escape(t) if t then t = t:gsub("<", "<"); t = t:gsub(">", ">"); t = t:gsub("(http://[%a%d@%.:/&%?=%-_#%%~]+)", function(h) h = urlunescape(h) return "" .. h .. ""; end); t = t:gsub("\n", "
"); t = t:gsub("%%", "%%%%"); else t = ""; end return t; end function create_doc(body, title) if not body then return "" end body = body:gsub("%%", "%%%%"); return html.doc:gsub("###BODY_STUFF###", body) :gsub("muc_log", ""..(title and html_escape(title) or "Chatroom logs")..""); end function urlunescape (url) url = url:gsub("+", " ") url = url:gsub("%%(%x%x)", function(h) return strchar(tonumber(h,16)) end) url = url:gsub("\r\n", "\n") return url end local function urlencode(s) return s and (s:gsub("[^a-zA-Z0-9.~_-]", function (c) return ("%%%02x"):format(c:byte()); end)); end local function get_room_from_jid(jid) local node, host = split_jid(jid); local component = hosts[host]; if component then local muc = component.modules.muc if muc and rawget(muc,"rooms") then -- We're running 0.9.x or 0.10 (old MUC API) return muc.rooms[jid]; elseif muc and rawget(muc,"get_room_from_jid") then -- We're running >0.10 (new MUC API) return muc.get_room_from_jid(jid); else return end end end local function get_room_list(host) local component = hosts[host]; local list = {}; if component then local muc = component.modules.muc if muc and rawget(muc,"rooms") then -- We're running 0.9.x or 0.10 (old MUC API) for _, room in pairs(muc.rooms) do list[room.jid] = room; end return list; elseif muc and rawget(muc,"each_room") then -- We're running >0.10 (new MUC API) for room, _ in muc.each_room() do list[room.jid] = room; end return list; end end end local function generate_room_list(host) local rooms; for jid, room in pairs(get_room_list(host)) do local node = split_jid(jid); if not room._data.hidden and room._data.logging and node then rooms = (rooms or "") .. html.rooms.bit:gsub("###ROOM###", urlencode(node)):gsub("###COMPONENT###", host); end end if rooms then return html.rooms.body:gsub("###ROOMS_STUFF###", rooms):gsub("###COMPONENT###", host), "Chatroom logs for "..host; end end -- Calendar stuff local function get_days_for_month(month, year) if month == 2 then local is_leap_year = (year % 4 == 0 and year % 100 ~= 0) or year % 400 == 0; return is_leap_year and 29 or 28; elseif (month < 8 and month%2 == 1) or (month >= 8 and month%2 == 0) then return 31; end return 30; end local function create_month(month, year, callback) local html_str = html.month.header; local days = get_days_for_month(month, year); local time = os_time{year=year, month=month, day=1}; local dow = tostring(os_date("%a", time)) local title = tostring(os_date("%B", time)); local week_days = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; local week_day = 0; local weeks = 1; local _available_for_one_day = false; local week_days_html = ""; for _, tmp in ipairs(week_days) do week_days_html = week_days_html .. html.month.weekDay:gsub("###DAY###", tmp) .. "\n"; end html_str = html_str:gsub("###TITLE###", title):gsub("###WEEKDAYS###", week_days_html); for i = 1, 31 do week_day = week_day + 1; if week_day == 1 then html_str = html_str .. "\n"; end if i == 1 then for _, tmp in ipairs(week_days) do if dow ~= tmp then html_str = html_str .. html.month.emptyDay .. "\n"; week_day = week_day + 1; else break; end end end if i < days + 1 then local tmp = tostring(i); if callback and callback.callback then tmp = callback.callback(callback.path, i, month, year, callback.room, callback.webpath); end if tmp == nil then tmp = tostring(i); else _available_for_one_day = true; end html_str = html_str .. html.month.day:gsub("###DAY###", tmp) .. "\n"; end if i >= days then break; end if week_day == 7 then week_day = 0; weeks = weeks + 1; html_str = html_str .. "\n"; end end if week_day + 1 < 8 or weeks < 6 then week_day = week_day + 1; if week_day > 7 then week_day = 1; end if week_day == 1 then weeks = weeks + 1; end for y = weeks, 6 do if week_day == 1 then html_str = html_str .. "\n"; end for i = week_day, 7 do html_str = html_str .. html.month.emptyDay .. "\n"; end week_day = 1 html_str = html_str .. "\n"; end end html_str = html_str .. html.month.footer; if _available_for_one_day then return html_str; end end local function create_year(year, callback) local year = year; local tmp; if tonumber(year) <= 99 then year = year + 2000; end local html_str = ""; for i=1, 12 do tmp = create_month(i, year, callback); if tmp then html_str = html_str .. "
\n" .. tmp .. "
\n"; end end if html_str ~= "" then return "
" .. html.year.title:gsub("###YEAR###", tostring(year)) .. html_str .. "

\n"; end return ""; end local function day_callback(path, day, month, year, room, webpath) local webpath = webpath or "" local year = year; if year > 2000 then year = year - 2000; end local bare_day = str_format("20%.02d-%.02d-%.02d", year, month, day); room = p_encode(room); local attributes, err = lfs.attributes(path.."/"..str_format("%.02d%.02d%.02d", year, month, day).."/"..room..".dat"); if attributes ~= nil and attributes.mode == "file" then local s = html.days.bit; s = s:gsub("###BARE_DAY###", webpath .. bare_day); s = s:gsub("###DAY###", day); return s; end return; end local function generate_day_room_content(bare_room_jid) local days = ""; local days_array = {}; local tmp; local node, host = split_jid(bare_room_jid); local path = data_getpath(node, host, datastore); local room = nil; local next_room = ""; local previous_room = ""; local rooms = ""; local attributes = nil; local since = ""; local to = ""; local topic = ""; local component = hosts[host]; if not(get_room_from_jid(bare_room_jid)) then return; end path = path:gsub("/[^/]*$", ""); attributes = lfs.attributes(path); do local found = 0; module:log("debug", generate_room_list(host)); for jid, room in pairs(get_room_list(host)) do local node = split_jid(jid) if not room._data.hidden and room._data.logging and node then if found == 0 then previous_room = node elseif found == 1 then next_room = node found = -1 end if jid == bare_room_jid then found = 1 end rooms = rooms .. html.days.rooms.bit:gsub("###ROOM###", urlencode(node)); end end room = get_room_from_jid(bare_room_jid); if room._data.hidden or not room._data.logging then room = nil; end end if attributes and room then local already_done_years = {}; topic = room._data.subject or "(no subject)" if topic:len() > 135 then topic = topic:sub(1, topic:find(" ", 120)) .. " ..." end local folders = {}; for folder in lfs.dir(path) do table.insert(folders, folder); end table.sort(folders); for _, folder in ipairs(folders) do local year, month, day = folder:match("^(%d%d)(%d%d)(%d%d)"); if year then to = tostring(os_date("%B %Y", os_time({ day=tonumber(day), month=tonumber(month), year=2000+tonumber(year) }))); if since == "" then since = to; end if not already_done_years[year] then module:log("debug", "creating overview for: %s", to); days = create_year(year, {callback=day_callback, path=path, room=node}) .. days; already_done_years[year] = true; end end end end tmp = html.days.body:gsub("###DAYS_STUFF###", days); tmp = tmp:gsub("###PREVIOUS_ROOM###", previous_room == "" and node or previous_room); tmp = tmp:gsub("###NEXT_ROOM###", next_room == "" and node or next_room); tmp = tmp:gsub("###ROOMS###", rooms); tmp = tmp:gsub("###ROOMTOPIC###", topic); tmp = tmp:gsub("###SINCE###", since); tmp = tmp:gsub("###TO###", to); return tmp:gsub("###JID###", bare_room_jid), "Chatroom logs for "..bare_room_jid; end local function parse_iq(stanza, time, nick) local text = nil; local victim = nil; if(stanza.attr.type == "set") then for _,tag in ipairs(stanza) do if tag.tag == "query" then for _,item in ipairs(tag) do if item.tag == "item" and item.attr.nick ~= nil and item.attr.role == 'none' then victim = item.attr.nick; for _,reason in ipairs(item) do if reason.tag == "reason" then text = reason[1]; break; end end break; end end break; end end if victim then if text then text = html.day.reason:gsub("###REASON###", html_escape(text)); else text = ""; end return html.day.kick:gsub("###TIME_STUFF###", time):gsub("###VICTIM###", victim):gsub("###REASON_STUFF###", text); end end return; end local function parse_presence(stanza, time, nick) local ret = ""; local show_join = "block" if config and not config.show_join then show_join = "none"; end if stanza.attr.type == nil then local show_status = "block" if config and not config.show_status then show_status = "none"; end local show, status = nil, ""; local already_joined = false; for _, tag in ipairs(stanza) do if tag.tag == "alreadyJoined" then already_joined = true; elseif tag.tag == "show" then show = tag[1]; elseif tag.tag == "status" and tag[1] ~= nil then status = tag[1]; end end if already_joined == true then if show == nil then show = "online"; end ret = html.day.presence.statusChange:gsub("###TIME_STUFF###", time); if status ~= "" then status = html.day.presence.statusText:gsub("###STATUS###", html_escape(status)); end ret = ret:gsub("###SHOW###", show):gsub("###NICK###", nick):gsub("###SHOWHIDE###", show_status):gsub("###STATUS_STUFF###", status); else ret = html.day.presence.join:gsub("###TIME_STUFF###", time):gsub("###SHOWHIDE###", show_join):gsub("###NICK###", nick); end elseif stanza.attr.type == "unavailable" then ret = html.day.presence.leave:gsub("###TIME_STUFF###", time):gsub("###SHOWHIDE###", show_join):gsub("###NICK###", nick); end return ret; end local function parse_message(stanza, time, nick) local body, title, ret = nil, nil, ""; for _,tag in ipairs(stanza) do if tag.tag == "body" then body = tag[1]; if nick then break; end elseif tag.tag == "nick" and nick == nil then nick = html_escape(tag[1]); if body or title then break; end elseif tag.tag == "subject" then title = tag[1]; if nick then break; end end end if nick and body then body = html_escape(body); local me = body:find("^/me"); local template = ""; if not me then template = html.day.message; else template = html.day.messageMe; body = body:gsub("^/me ", ""); end ret = template:gsub("###TIME_STUFF###", time):gsub("###NICK###", nick):gsub("###MSG###", body); elseif nick and title then title = html_escape(title); ret = html.day.titleChange:gsub("###TIME_STUFF###", time):gsub("###NICK###", nick):gsub("###TITLE###", title); end return ret; end local function increment_day(bare_day) local year, month, day = bare_day:match("^20(%d%d)-(%d%d)-(%d%d)$"); local leapyear = false; module:log("debug", tostring(day).."/"..tostring(month).."/"..tostring(year)) day = tonumber(day); month = tonumber(month); year = tonumber(year); if year%4 == 0 and year%100 == 0 then if year%400 == 0 then leapyear = true; else leapyear = false; -- turn of the century but not a leapyear end elseif year%4 == 0 then leapyear = true; end if (month == 2 and leapyear and day + 1 > 29) or (month == 2 and not leapyear and day + 1 > 28) or (month < 8 and month%2 == 1 and day + 1 > 31) or (month < 8 and month%2 == 0 and day + 1 > 30) or (month >= 8 and month%2 == 0 and day + 1 > 31) or (month >= 8 and month%2 == 1 and day + 1 > 30) then if month + 1 > 12 then year = year + 1; month = 1; day = 1; else month = month + 1; day = 1; end else day = day + 1; end return strformat("20%.02d-%.02d-%.02d", year, month, day); end local function find_next_day(bare_room_jid, bare_day) local node, host = split_jid(bare_room_jid); local day = increment_day(bare_day); local max_trys = 7; module:log("debug", day); while(not store_exists(node, host, day)) do max_trys = max_trys - 1; if max_trys == 0 then break; end day = increment_day(day); end if max_trys == 0 then return nil; else return day; end end local function decrement_day(bare_day) local year, month, day = bare_day:match("^20(%d%d)-(%d%d)-(%d%d)$"); local leapyear = false; module:log("debug", tostring(day).."/"..tostring(month).."/"..tostring(year)) day = tonumber(day); month = tonumber(month); year = tonumber(year); if year%4 == 0 and year%100 == 0 then if year%400 == 0 then leapyear = true; else leapyear = false; -- turn of the century but not a leapyear end elseif year%4 == 0 then leapyear = true; end if day - 1 == 0 then if month - 1 == 0 then year = year - 1; month = 12; day = 31; else month = month - 1; if (month == 2 and leapyear) then day = 29 elseif (month == 2 and not leapyear) then day = 28 elseif (month < 8 and month%2 == 1) or (month >= 8 and month%2 == 0) then day = 31 else day = 30 end end else day = day - 1; end return strformat("20%.02d-%.02d-%.02d", year, month, day); end local function find_previous_day(bare_room_jid, bare_day) local node, host = split_jid(bare_room_jid); local day = decrement_day(bare_day); local max_trys = 7; module:log("debug", day); while(not store_exists(node, host, day)) do max_trys = max_trys - 1; if max_trys == 0 then break; end day = decrement_day(day); end if max_trys == 0 then return nil; else return day; end end local function parse_day(bare_room_jid, room_subject, bare_day) local ret = ""; local year; local month; local day; local tmp; local node, host = split_jid(bare_room_jid); local year, month, day = bare_day:match("^20(%d%d)-(%d%d)-(%d%d)$"); local previous_day = find_previous_day(bare_room_jid, bare_day); local next_day = find_next_day(bare_room_jid, bare_day); local temptime = {day=0, month=0, year=0}; local path = data_getpath(node, host, datastore); path = path:gsub("/[^/]*$", ""); local calendar = "" if tonumber(year) <= 99 then year = year + 2000; end temptime.day = tonumber(day) temptime.month = tonumber(month) temptime.year = tonumber(year) calendar = create_month(temptime.month, temptime.year, {callback=day_callback, path=path, room=node, webpath="../"}) or "" if bare_day then local data = data_load(node, host, datastore .. "/" .. bare_day:match("^20(.*)"):gsub("-", "")); if data then for i=1, #data, 1 do local stanza = lom.parse(data[i]); if stanza and stanza.attr and stanza.attr.time then local timeStuff = html.day.time:gsub("###TIME###", stanza.attr.time):gsub("###UTC###", stanza.attr.utc or stanza.attr.time); if stanza[1] ~= nil then local nick; local tmp; -- grep nick from "from" resource if stanza[1].attr.from then -- presence and messages nick = html_escape(stanza[1].attr.from:match("/(.+)$")); elseif stanza[1].attr.to then -- iq nick = html_escape(stanza[1].attr.to:match("/(.+)$")); end if stanza[1].tag == "presence" and nick then tmp = parse_presence(stanza[1], timeStuff, nick); elseif stanza[1].tag == "message" then tmp = parse_message(stanza[1], timeStuff, nick); elseif stanza[1].tag == "iq" then tmp = parse_iq(stanza[1], timeStuff, nick); else module:log("info", "unknown stanza subtag in log found. room: %s; day: %s", bare_room_jid, year .. "/" .. month .. "/" .. day); end if tmp then ret = ret .. tmp tmp = nil; end end end end end if ret ~= "" then if next_day then next_day = html.day.dayLink:gsub("###DAY###", next_day):gsub("###TEXT###", ">") end if previous_day then previous_day = html.day.dayLink:gsub("###DAY###", previous_day):gsub("###TEXT###", "<"); end ret = ret:gsub("%%", "%%%%"); if config.show_presences then tmp = html.day.body:gsub("###DAY_STUFF###", ret):gsub("###JID###", bare_room_jid); else tmp = html.day.bodynp:gsub("###DAY_STUFF###", ret):gsub("###JID###", bare_room_jid); end tmp = tmp:gsub("###CALENDAR###", calendar); tmp = tmp:gsub("###DATE###", tostring(os_date("%A, %B %d, %Y", os_time(temptime)))); tmp = tmp:gsub("###TITLE_STUFF###", html.day.title:gsub("###TITLE###", room_subject)); tmp = tmp:gsub("###STATUS_CHECKED###", config.show_status and "checked='checked'" or ""); tmp = tmp:gsub("###JOIN_CHECKED###", config.show_join and "checked='checked'" or ""); tmp = tmp:gsub("###NEXT_LINK###", next_day or ""); tmp = tmp:gsub("###PREVIOUS_LINK###", previous_day or ""); return tmp, "Chatroom logs for "..bare_room_jid.." ("..tostring(os_date("%A, %B %d, %Y", os_time(temptime)))..")"; end end end local function handle_error(code, err) return http_event("http-error", { code = code, message = err }); end function handle_request(event) local response = event.response; local request = event.request; local room; local node, day, more = request.url.path:match("^/"..url_base.."/+([^/]*)/*([^/]*)/*(.*)$"); if more ~= "" then response.status_code = 404; return response:send(handle_error(response.status_code, "Unknown URL.")); end if node == "" then node = nil; end if day == "" then day = nil; end node = urldecode(node); if not html.doc then response.status_code = 500; return response:send(handle_error(response.status_code, "Muc Theme is not loaded.")); end if node then room = get_room_from_jid(node.."@"..my_host); end if node and not room then response.status_code = 404; return response:send(handle_error(response.status_code, "Room doesn't exist.")); end if room and (room._data.hidden or not room._data.logging) then response.status_code = 404; return response:send(handle_error(response.status_code, "There're no logs for this room.")); end if not node then -- room list for component return response:send(create_doc(generate_room_list(my_host))); elseif not day then -- room's listing return response:send(create_doc(generate_day_room_content(node.."@"..my_host))); else if not day:match("^20(%d%d)-(%d%d)-(%d%d)$") then local y,m,d = day:match("^(%d%d)(%d%d)(%d%d)$"); if not y then response.status_code = 404; return response:send(handle_error(response.status_code, "No entries for that year.")); end response.status_code = 301; response.headers = { ["Location"] = request.url.path:match("^/"..url_base.."/+[^/]*").."/20"..y.."-"..m.."-"..d.."/" }; return response:send(); end local body = create_doc(parse_day(node.."@"..my_host, room._data.subject or "", day)); if body == "" then response.status_code = 404; return response:send(handle_error(response.status_code, "Day entry doesn't exist.")); end return response:send(body); end end local function read_file(filepath) local f,err = io_open(filepath, "r"); if not f then return f,err; end local t = f:read("*all"); f:close() return t; end local function load_theme(path) for file in lfs.dir(path) do if file:match("%.html$") then module:log("debug", "opening theme file: " .. file); local content,err = read_file(path .. "/" .. file); if not content then return content,err; end -- html.a.b.c = content of a_b_c.html local tmp = html; for idx in file:gmatch("([^_]*)_") do tmp[idx] = tmp[idx] or {}; tmp = tmp[idx]; end tmp[file:match("([^_]*)%.html$")] = content; end end return true; end function module.load() config = module:get_option("muc_log_http", {}); if module:get_option_boolean("muc_log_presences", true) then config.show_presences = true end if config.show_status == nil then config.show_status = true; end if config.show_join == nil then config.show_join = true; end if config.url_base and type(config.url_base) == "string" then url_base = config.url_base; end theme = config.theme or "prosody"; local theme_path = themes_parent .. "/" .. tostring(theme); local attributes, err = lfs.attributes(theme_path); if attributes == nil or attributes.mode ~= "directory" then module:log("error", "Theme folder of theme \"".. tostring(theme) .. "\" isn't existing. expected Path: " .. theme_path); return false; end local themeLoaded,err = load_theme(theme_path); if not themeLoaded then module:log("error", "Theme \"%s\" is missing something: %s", tostring(theme), err); return false; end module:provides("http", { default_path = url_base, route = { ["GET /*"] = handle_request; } }); end prosody-modules-c53cc1ae4788/mod_muc_log_http/README.markdown0000644000175500017550000000216413163400720023620 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: Provides a web interface to stored chatroom logs ... Introduction ============ This module provides a built-in web interface to view chatroom logs stored by [mod\_muc\_log](mod_muc_log.html). Installation ============ Just copy the folder muc\_log\_http as it is, into the modules folder of your Prosody installation. Configuration Details ===================== Example configuration: Component "conference.example.com" "muc" modules_enabled = { ..... "muc_log"; "muc_log_http"; ..... } muc_log_http = { -- These are the defaults show_join = true; show_presences = true; show_status = true; theme = "prosody"; url_base = "muc_log"; } **show\_join** sets the default for showing joins or leaves. **show\_status** sets the default for showing status changes. The web interface would then be reachable at the address: http://conference.example.com:5280/muc_log/ TODO ==== - Log bans correctly - Quota \~ per day ?! - Testing testing :) prosody-modules-c53cc1ae4788/mod_measure_cpu/0002755000175500017550000000000013164216767020763 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_measure_cpu/README.markdown0000644000175500017550000000021013163400720023432 0ustar debacledebacle--- labels: summary: Measure CPU usage ... Description =========== This module measures CPU usage and reports using Prosody 0.10 APIs prosody-modules-c53cc1ae4788/mod_measure_cpu/mod_measure_cpu.lua0000644000175500017550000000206313163400720024613 0ustar debacledebaclemodule:set_global(); local measure = require"core.statsmanager".measure; local mt = require"util.multitable"; local get_time = require "socket".gettime; local get_clock = os.clock; local measure_cpu_now = measure("amount", "cpu.percent"); -- Current percentage local last_cpu_wall, last_cpu_clock; module:hook("stats-update", function () local new_wall, new_clock = get_time(), get_clock(); local pc = 0; if last_cpu_wall then pc = 100/((new_wall-last_cpu_wall)/(new_clock-last_cpu_clock)); end last_cpu_wall, last_cpu_clock = new_wall, new_clock; measure_cpu_now(pc); end); -- Some metadata for mod_munin local munin_meta = mt.new(); munin_meta.data = module:shared"munin/meta"; local key = "global_cpu_amount"; munin_meta:set(key, "", "graph_args", "--base 1000 -r --lower-limit 0 --upper-limit 100"); munin_meta:set(key, "", "graph_title", "Prosody CPU Usage"); munin_meta:set(key, "", "graph_vlabel", "%"); munin_meta:set(key, "", "graph_category", "cpu"); munin_meta:set(key, "percent", "label", "CPU Usage"); munin_meta:set(key, "percent", "min", "0"); prosody-modules-c53cc1ae4788/mod_checkcerts/0002755000175500017550000000000013164216767020571 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_checkcerts/mod_checkcerts.lua0000644000175500017550000000562313163400720024234 0ustar debacledebaclelocal ssl = require"ssl"; local datetime_parse = require"util.datetime".parse; local load_cert = ssl.loadcertificate; local st = require"util.stanza" -- These are in days. local nag_time = module:get_option_number("checkcerts_notify", 7) * 86400; if not load_cert then module:log("error", "This version of LuaSec (%s) does not support certificate checking", ssl._VERSION); return end local pat = "^([JFMAONSD][ceupao][glptbvyncr]) ?(%d%d?) (%d%d):(%d%d):(%d%d) (%d%d%d%d) GMT$"; local months = {Jan=1,Feb=2,Mar=3,Apr=4,May=5,Jun=6,Jul=7,Aug=8,Sep=9,Oct=10,Nov=11,Dec=12}; local function parse_x509_datetime(s) local month, day, hour, min, sec, year = s:match(pat); month = months[month]; return datetime_parse(("%04d-%02d-%02dT%02d:%02d:%02dZ"):format(year, month, day, hour, min, sec)); end local timeunits = {"minute",60,"hour",3600,"day",86400,"week",604800,"month",2629746,"year",31556952,}; local function humantime(timediff) local ret = {}; for i=#timeunits,2,-2 do if timeunits[i] < timediff then local n = math.floor(timediff / timeunits[i]); if n > 0 and #ret < 2 then ret[#ret+1] = ("%d %s%s"):format(n, timeunits[i-1], n ~= 1 and "s" or ""); timediff = timediff - n*timeunits[i]; end end end return table.concat(ret, " and ") end local function check_certs_validity() local now = os.time(); -- First, let's find out what certificate this host uses. local ssl_config = config.rawget(module.host, "ssl"); if not ssl_config or not ssl_config.certificate then ssl_config = config.get(module.host:match("%.(.*)"), "ssl"); end if not ssl_config or not ssl_config.certificate then ssl_config = config.get("*", "ssl"); end if not ssl_config or not ssl_config.certificate then log("warn", "Could not find a certificate to check"); return; end local certfile = ssl_config.certificate; local fh, ferr = io.open(certfile); -- Load the file. if not fh then log("warn", "Could not open certificate %s", ferr); return; end local cert, lerr = load_cert(fh:read("*a")); -- And parse fh:close(); if not cert then log("warn", "Could not parse certificate %s: %s", certfile, lerr or ""); return; end local expires_at = parse_x509_datetime(cert:notafter()); local expires_in = os.difftime(expires_at, now); local fmt = "Certificate %s expires in %s" local nag_admin = expires_in < nag_time; local log_warn = expires_in < nag_time * 2; local timediff = expires_in; if expires_in < 0 then fmt = "Certificate %s expired %s ago"; timediff = -timediff; end timediff = humantime(timediff); module:log(log_warn and "warn" or "info", fmt, certfile, timediff); if nag_admin then local body = fmt:format("for host ".. module.host, timediff); for _,admin in ipairs(module:get_option_array("admins", {})) do module:send(st.message({ from = module.host, to = admin, type = "chat" }, body)); end end return math.max(86400, expires_in / 3); end module:add_timer(1, check_certs_validity); prosody-modules-c53cc1ae4788/mod_checkcerts/README.markdown0000644000175500017550000000116113163400720023246 0ustar debacledebacle--- labels: summary: Certificate expiry reminder ... Introduction ============ This module periodically checks your certificate to see if it is about to expire soon. The time before expiry is printed in the logs. About a week before a certificate expires, reminder messages will be sent to admins. Configuration ============= Simply add the module to the `modules_enabled` list. You can optionally configure how long before expiry to start sending messages to admins. modules_enabled = { ... "checkcerts" } checkcerts_notify = 7 -- ( in days ) Compatibility ============= Needs LuaSec 0.5+ prosody-modules-c53cc1ae4788/mod_pubsub_twitter/0002755000175500017550000000000013164216767021535 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_pubsub_twitter/mod_pubsub_twitter.lua0000644000175500017550000001015613163400720026141 0ustar debacledebacle-- Publishes Twitter search results over pubsub -- -- Config: -- Component "pubsub.example.com" "pubsub" -- modules_enabled = { -- "pubsub_twitter"; -- } -- twitter_searches = { -- node -> query -- prosody = "prosody xmpp"; -- } -- twitter_pull_interval = 20 -- minutes -- local pubsub = module:depends"pubsub"; local json = require "util.json"; local http = require "net.http"; local set = require "util.set"; local it = require "util.iterators"; local array = require "util.array"; local st = require "util.stanza"; --local dump = require"util.serialization".serialize; local xmlns_atom = "http://www.w3.org/2005/Atom"; local twitter_searches = module:get_option("twitter_searches", {}); local refresh_interval = module:get_option_number("twitter_pull_interval", 20) * 60; local api_url = module:get_option_string("twitter_search_url", "http://search.twitter.com/search.json"); local month_number = { Jan = "01", Feb = "02", Mar = "03"; Apr = "04", May = "05", Jun = "06"; Jul = "07", Aug = "08", Sep = "09"; Oct = "10", Nov = "11", Dec = "12"; }; local active_searches = {}; local function publish_result(search_name, result) local node, id = search_name, result.id_str; --"Tue, 02 Apr 2013 15:40:54 +0000" local timestamp_date, timestamp_month, timestamp_year, timestamp_time = result.created_at:match(" (%d+) (%a+) (%d+) (%d%d:%d%d:%d%d)"); local timestamp = ("%s-%s-%sT%sZ"):format(timestamp_year, month_number[timestamp_month], timestamp_date, timestamp_time); local item = st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = id }) :tag("entry", { xmlns = xmlns_atom }) :tag("id"):text(id):up() :tag("author") :tag("name"):text(result.from_user_name.." (@"..result.from_user..")"):up() :tag("uri"):text("http://twitter.com/"..result.from_user):up() :up() :tag("published"):text(timestamp):up() :tag("title"):text(result.text):up() :tag("link", { rel = "alternate" , href = "https://twitter.com/"..result.from_user.."/status/"..id}):up(); module:log("debug", "Publishing Twitter result: %s", tostring(item)); local ok, err = pubsub.service:publish(node, true, id, item); if not ok then if err == "item-not-found" then -- try again local ok, err = pubsub.service:create(node, true); if not ok then module:log("error", "could not create node %s: %s", node, err); return; end local ok, err = pubsub.service:publish(node, true, id, item); if not ok then module:log("error", "could not create or publish node %s: %s", node, err); return end else module:log("error", "publishing %s failed: %s", node, err); end end end local function is_retweet(tweet) return not not tweet.text:match("^RT "); end function update_all() module:log("debug", "Updating all searches"); for name, search in pairs(active_searches) do module:log("debug", "Fetching new results for '%s'", name); http.request(search.refresh_url or search.url, nil, function (result_json, code) if code ~= 200 then module:log("warn", "Twitter search query '%s' failed with code %d", name, code); return; end local response = json.decode(result_json); module:log("debug", "Processing %d results for %s", #response.results, name); search.refresh_url = api_url..response.refresh_url; for _, result in ipairs(response.results) do if not is_retweet(result) then publish_result(name, result); end end end); end return refresh_interval; end function module.load() local config_searches = set.new(array.collect(it.keys(twitter_searches))); local current_searches = set.new(array.collect(it.keys(active_searches))); local disable_searches = current_searches - config_searches; local new_searches = config_searches - current_searches; for search_name in disable_searches do module:log("debug", "Disabled old Twitter search '%s'", search_name); active_searches[search_name] = nil; end for search_name in new_searches do module:log("debug", "Created new Twitter search '%s'", search_name); local query = twitter_searches[search_name]; active_searches[search_name] = { url = api_url.."?q="..http.urlencode(query); }; end end module:add_timer(5, update_all); prosody-modules-c53cc1ae4788/mod_pubsub_twitter/README.markdown0000644000175500017550000000255413163400720024221 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: Subscribe to Twitter search queries over pubsub ... Introduction ------------ Twitter has an open 'realtime' search API, but it requires polling (within their rate limits). This module allows Prosody to poll for you, and push new results to subscribers over XMPP. Configuration ------------- This module must be loaded on a Prosody pubsub component. Add it to `modules_enabled` and configure like so: Component "pubsub.example.com" "pubsub" modules_enabled = { "pubsub_twitter" } twitter_searches = { realtime = "xmpp OR realtime"; prosody = "prosody xmpp"; } This example creates two nodes, 'realtime' and 'prosody' that clients can subscribe to using [XEP-0060](http://xmpp.org/extensions/xep-0060.html). Results are in [ATOM 1.0 format](http://atomenabled.org/) for easy consumption. Option Description ------------------------- -------------------------------------------------------------------------------- twitter\_searches A list of virtual nodes to create and their associated Twitter search queries. twitter\_pull\_interval Number of minutes between polling for new results (default 20) twitter\_search\_url URL of the JSON search API, default: "http://search.twitter.com/search.json" Compatibility ------------- ----- ------- 0.9 Works ----- ------- prosody-modules-c53cc1ae4788/mod_muc_access_control/0002755000175500017550000000000013164216767022320 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_muc_access_control/mod_muc_access_control.lua0000644000175500017550000000336713163400720027515 0ustar debacledebaclelocal st = require "util.stanza"; local jid = require "util.jid"; local nodeprep = require "util.encodings".stringprep.nodeprep; local unprepped_access_lists = module:get_option("muc_access_lists", {}); local access_lists = {}; -- Make sure all input is prepped for unprepped_room_name, unprepped_list in pairs(unprepped_access_lists) do local prepped_room_name = nodeprep(unprepped_room_name); if not prepped_room_name then module:log("error", "Invalid room name: %s", unprepped_room_name); else local prepped_list = {}; for _, unprepped_jid in ipairs(unprepped_list) do local prepped_jid = jid.prep(jid); if not prepped_jid then module:log("error", "Invalid JID: %s", unprepped_jid); else table.insert(prepped_list, jid.pep(jid)); end end end end local function is_restricted(room, who) local allowed = access_lists[room]; if allowed == nil or allowed[who] or allowed[select(2, jid.split(who))] then return nil; end return "forbidden"; end module:hook("presence/full", function(event) local stanza = event.stanza; if stanza.name == "presence" and stanza.attr.type == "unavailable" then -- Leaving events get discarded return; end -- Get the room local room = jid.split(stanza.attr.to); if not room then return; end -- Get who has tried to join it local who = jid.bare(stanza.attr.from) -- Checking whether room is restricted local check_restricted = is_restricted(room, who) if check_restricted ~= nil then event.allowed = false; event.stanza.attr.type = 'error'; return event.origin.send(st.error_reply(event.stanza, "cancel", "forbidden", "You're not allowed to enter this room: " .. check_restricted)); end end, 10); prosody-modules-c53cc1ae4788/mod_auth_dovecot/0002755000175500017550000000000013164216767021137 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_auth_dovecot/README.markdown0000644000175500017550000000414713163400720023623 0ustar debacledebacle--- labels: - 'Stage-Alpha' - 'Type-Auth' summary: Dovecot authentication module ... Introduction ============ This is a Prosody authentication plugin which uses Dovecot as the backend. Configuration ============= As with all auth modules, there is no need to add this to modules\_enabled. Simply add in the global section, or for the relevant hosts: authentication = "dovecot" These options are used by mod\_auth\_dovecot: Name Description Default value ----------------------- ----------------------------------------- ------------------------------- dovecot\_auth\_socket Path to the Dovecot auth socket "/var/run/dovecot/auth-login" auth\_append\_host If true, sends the bare JID as authzid. false The Dovecot user and group must have access to connect to this socket. You can create a new dedicated socket for Prosody too. Add the below to the *socket listen* section of /etc/dovecot/dovecot.conf, and match the socket path in Prosody's dovecot\_auth\_socket setting. socket listen { ... client { path = /var/spool/prosody/private/auth-client mode = 0660 user = prosody group = prosody } Make sure the socket directories exist and are owned by the Prosody user. Note: Dovecot uses UNIX sockets by default. luasocket is compiled with UNIX socket on debian/ubuntu by default, but is not on many other platforms. If you run into this issue, you would need to either recompile luasocket with UNIX socket support, or use Dovecot 2.x's TCP socket support. TCP socket support for Dovecot 2.x ---------------------------------- Dovecot 2.x includes TCP socket support. These are the relevant mod\_auth\_dovecot options: Name Description Default value --------------------- ------------------------- ---------------------------- dovecot\_auth\_host Hostname to connect to. "127.0.0.1" dovecot\_auth\_port Port to connect to. *(this value is required)* Compatibility ============= ------- ------- trunk Works 0.8 Works ------- ------- prosody-modules-c53cc1ae4788/mod_auth_dovecot/auth_dovecot/0002755000175500017550000000000013164216767023623 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_auth_dovecot/auth_dovecot/mod_auth_dovecot.lua0000644000175500017550000000563613163400720027640 0ustar debacledebacle-- Dovecot authentication backend for Prosody -- -- Copyright (C) 2010-2011 Waqas Hussain -- Copyright (C) 2011 Kim Alvefur -- local name = "Dovecot SASL"; local log = require "util.logger".init("auth_dovecot"); local socket_path = module:get_option_string("dovecot_auth_socket", "/var/run/dovecot/auth-login"); local socket_host = module:get_option_string("dovecot_auth_host", "127.0.0.1"); local socket_port = module:get_option_string("dovecot_auth_port"); local service_realm = module:get_option("realm"); local service_name = module:get_option("service_name"); local append_host = module:get_option_boolean("auth_append_host"); --assert(not append_host, "auth_append_host does not work"); local validate_domain = module:get_option_boolean("validate_append_host"); local handle_appended = module:get_option_string("handle_appended"); local util_sasl_new = require "util.sasl".new; local new_dovecot_sasl = module:require "sasl_dovecot".new; local new_sasl = function(realm) return new_dovecot_sasl( service_realm or realm, service_name or "xmpp", socket_port and { socket_host, socket_port } or socket_path, { --config handle_domain = handle_appended or (append_host and "split" or "escape"), validate_domain = validate_domain, } ); end do local s, err = new_sasl(module.host) if not s then log("error", "%s", tostring(err)); end assert(s, "Could not create a new SASL object"); assert(s.mechanisms, "SASL object has no mechanims method"); local m, _m = {}, s:mechanisms(); assert(not append_host or _m.PLAIN, "auth_append_host requires PLAIN, but it is unavailable"); for k in pairs(_m) do table.insert(m, k); end log("debug", "Mechanims found: %s", table.concat(m, ", ")); end provider = {}; function provider.test_password(username, password) return new_sasl(module.host):plain_test(username, password); end function provider.get_password(username) return nil, "Passwords unavailable for "..name; end function provider.set_password(username, password) return nil, "Passwords unavailable for "..name; end function provider.user_exists(username) return true -- FIXME --[[ This, sadly, doesn't work. local user_test = new_sasl(module.host); user_test:select("PLAIN"); user_test:process(("\0%s\0"):format(username)); return user_test.username == username; --]] end function provider.create_user(username, password) return nil, "Account creation/modification not available with "..name; end function provider.get_sasl_handler() return new_sasl(module.host); end if append_host then function provider.test_password(username, password) return new_sasl(module.host):plain_test(username .. "@".. (service_realm or module.host), password) == "success"; end function provider.get_sasl_handler() return util_sasl_new(module.host, { plain_test = function(sasl, username, password, realm) return provider.test_password(username, password), true end; }); end end module:provides("auth", provider); prosody-modules-c53cc1ae4788/mod_auth_dovecot/auth_dovecot/sasl_dovecot.lib.lua0000644000175500017550000002163113163400720027540 0ustar debacledebacle-- Dovecot authentication backend for Prosody -- -- Copyright (C) 2008-2009 Tobias Markmann -- Copyright (C) 2010 Javier Torres -- Copyright (C) 2010-2011 Matthew Wild -- Copyright (C) 2010-2011 Waqas Hussain -- Copyright (C) 2011 Kim Alvefur -- -- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -- -- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -- -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -- This code is based on util.sasl_cyrus and the old mod_auth_dovecot local log = require "util.logger".init("sasl_dovecot"); local setmetatable = setmetatable; local s_match, s_gmatch = string.match, string.gmatch local t_concat = table.concat; local m_random = math.random; local tostring, tonumber = tostring, tonumber; local socket = require "socket" pcall(require, "socket.unix"); local base64 = require "util.encodings".base64; local b64, unb64 = base64.encode, base64.decode; local jid_escape = require "util.jid".escape; local prepped_split = require "util.jid".prepped_split; local nodeprep = require "util.encodings".stringprep.nodeprep; local pposix = require "util.pposix"; --module "sasl_dovecot" local _M = {}; local request_id = 0; local method = {}; method.__index = method; local conn, supported_mechs, pid; local function connect(socket_info) --log("debug", "connect(%q)", socket_path); if conn then conn:close(); pid = nil; end local socket_type = (type(socket_info) == "string") and "UNIX" or "TCP"; local ok, err, socket_path; if socket_type == "TCP" then local socket_host, socket_port = unpack(socket_info); conn = socket.tcp(); ok, err = conn:connect(socket_host, socket_port); socket_path = ("%s:%d"):format(socket_host, socket_port); elseif socket.unix then socket_path = socket_info; conn = socket.unix(); ok, err = conn:connect(socket_path); else err = "luasocket was not compiled with UNIX sockets support"; end if not ok then return false, "error connecting to dovecot "..tostring(socket_type).." socket at '" ..tostring(socket_path or socket_info).."'. error was '"..tostring(err).."'"; end -- Send our handshake pid = pposix.getpid(); log("debug", "sending handshake to dovecot. version 1.1, cpid '%d'", pid); local success,err = conn:send("VERSION\t1\t1\n"); if not success then return false, "Unable to send version data to socket: "..tostring(err); end local success,err = conn:send("CPID\t" .. pid .. "\n"); if not success then return false, "Unable to send PID to socket: "..tostring(err); end -- Parse Dovecot's handshake local done = false; supported_mechs = {}; while (not done) do local line, err = conn:receive(); if not line then return false, "No data read from socket: "..tostring(err); end --log("debug", "dovecot handshake: '%s'", line); local parts = line:gmatch("[^\t]+"); local first = parts(); if first == "VERSION" then -- Version should be 1.1 local major_version = parts(); if major_version ~= "1" then conn:close(); return false, "dovecot server version is not 1.x. it is "..tostring(major_version)..".x"; end elseif first == "MECH" then local mech = parts(); supported_mechs[mech] = true; elseif first == "DONE" then done = true; end end return conn, supported_mechs; end -- create a new SASL object which can be used to authenticate clients function _M.new(realm, service_name, socket_info, config) --log("debug", "new(%q, %q, %q)", realm or "", service_name or "", socket_info or ""); local sasl_i = { realm = realm, service_name = service_name, socket_info = socket_info, config = config or {} }; request_id = request_id + 1; sasl_i.request_id = request_id; local conn, mechs = conn, supported_mechs; if not conn then conn, mechs = connect(socket_info); if not conn then return nil, "Dovecot connection failure: "..tostring(mechs); end end sasl_i.conn, sasl_i.mechs = conn, mechs; return setmetatable(sasl_i, method); end -- [[ function method:send(...) local msg = t_concat({...}, "\t"); if msg:sub(-1) ~= "\n" then msg = msg .. "\n" end module:log("debug", "sending %q", msg:sub(1,-2)); local ok, err = self.conn:send(msg); if not ok then log("error", "Could not write to socket: %s", err); if err == "closed" then conn = nil; end return nil, err; end return true; end function method:recv() --log("debug", "Sent %d bytes to socket", ok); local line, err = self.conn:receive(); if not line then log("error", "Could not read from socket: %s", err); if err == "closed" then conn = nil; end return nil, err; end module:log("debug", "received %q", line); return line; end -- ]] function method:plain_test(username, password, realm) if self:select("PLAIN") then return self:process(("\0%s\0%s"):format(username, password)); end end -- get a fresh clone with the same realm and service name function method:clean_clone() --log("debug", "method:clean_clone()"); return _M.new(self.realm, self.service_name, self.socket_info, self.config) end -- get a list of possible SASL mechanims to use function method:mechanisms() --log("debug", "method:mechanisms()"); return self.mechs; end -- select a mechanism to use function method:select(mechanism) --log("debug", "method:select(%q)", mechanism); if not self.selected and self.mechs[mechanism] then self.selected = mechanism; return true; end end -- feed new messages to process into the library function method:process(message) --log("debug", "method:process"..(message and "(%q)" or "()"), message); --if not message then --return "challenge"; --return "failure", "malformed-request"; --end local request_id = self.request_id; local authmsg; local ok, err; if not self.started then self.started = true; ok, err = self:send( "AUTH", request_id, self.selected, "service="..self.service_name, "resp="..(message and b64(message) or "=") ); else ok, err = self:send( "CONT", request_id, (message and b64(message) or "=") ); end --log("debug", "Sending %d bytes: %q", #authmsg, authmsg); if not ok then log("error", "Could not write to socket: %s", err); return "failure", "internal-server-error", err end --log("debug", "Sent %d bytes to socket", ok); local line, err = self:recv(); if not line then log("error", "Could not read from socket: %s", err); return "failure", "internal-server-error", err end --log("debug", "Received %d bytes from socket: %s", #line, line); local parts = line:gmatch("[^\t]+"); local resp = parts(); local id = tonumber(parts()); if id ~= request_id then return "failure", "internal-server-error", "Unexpected request id" end local data = {}; for param in parts do data[#data+1]=param; local k,v = param:match("^([^=]*)=?(.*)$"); if k and #k>0 then data[k]=v or true; end end if data.user then local handle_domain = self.config.handle_domain; local validate_domain = self.config.validate_domain; if handle_domain == "split" then local domain; self.username, domain = prepped_split(data.user); if validate_domain and domain ~= self.realm then return "failure", "not-authorized", "Domain mismatch"; end elseif handle_domain == "escape" then self.username = nodeprep(jid_escape(data.user)); else self.username = nodeprep(data.user); end if not self.username then return "failure", "not-authorized", "Username failed NODEprep" end end if resp == "FAIL" then if data.temp then return "failure", "temporary-auth-failure", data.reason; elseif data.authz then return "failure", "invalid-authzid", data.reason; else return "failure", "not-authorized", data.reason; end elseif resp == "CONT" then return "challenge", unb64(data[1]); elseif resp == "OK" then return "success", data.resp and unb64(data.resp) or nil; end end return _M; prosody-modules-c53cc1ae4788/mod_compact_resource/0002755000175500017550000000000013164216767022010 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_compact_resource/mod_compact_resource.lua0000644000175500017550000000055213163400720026666 0ustar debacledebacle local base64_encode = require"util.encodings".base64.encode; local random_bytes = require"util.random".bytes; local b64url = { ["+"] = "-", ["/"] = "_", ["="] = "" }; local function random_resource() return base64_encode(random_bytes(8)):gsub("[+/=]", b64url); end module:hook("pre-resource-bind", function (event) event.resource = random_resource(); end); prosody-modules-c53cc1ae4788/mod_measure_memory/0002755000175500017550000000000013164216767021504 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_measure_memory/mod_measure_memory.lua0000644000175500017550000000154113163400720026055 0ustar debacledebaclemodule:set_global(); local measure = require"core.statsmanager".measure; local measures = {}; setmetatable(measures, { __index = function (t, k) local m = measure("sizes", "memory."..k); t[k] = m; return m; end }); module:hook("stats-update", function () measures.lua(collectgarbage("count")*1024); end); if require"lfs".attributes("/proc/self/statm", "mode") == "file" then local pagesize = module:get_option_number("memory_pagesize", 4096); -- getconf PAGESIZE module:hook("stats-update", function () local statm, err = io.open("/proc/self/statm"); if not statm then module:log("error", tostring(err)); return; end -- virtual memory (caches, opened librarys, everything) measures.total(statm:read("*n") * pagesize); -- resident set size (actually used memory) measures.rss(statm:read("*n") * pagesize); statm:close(); end); end prosody-modules-c53cc1ae4788/mod_measure_memory/README.markdown0000644000175500017550000000021613163400720024161 0ustar debacledebacle--- labels: summary: Measure memory usage ... Description =========== This module measures memory usage and reports using Prosody 0.10 APIs prosody-modules-c53cc1ae4788/.luacheckrc0000644000175500017550000000332213163400720017676 0ustar debacledebaclecache = true read_globals = { "prosody", "hosts", "import", -- Module instance "module.name", "module.host", "module._log", "module.log", "module.event_handlers", "module.reloading", "module.saved_state", "module.environment", "module.global", "module.path", -- Module API "module.add_extension", "module.add_feature", "module.add_identity", "module.add_item", "module.add_timer", "module.broadcast", "module.context", "module.depends", "module.fire_event", "module.get_directory", "module.get_host", "module.get_host_items", "module.get_host_type", "module.get_name", "module.get_option", "module.get_option_array", "module.get_option_boolean", "module.get_option_inherited_set", "module.get_option_number", "module.get_option_path", "module.get_option_set", "module.get_option_string", "module.handle_items", "module.has_feature", "module.has_identity", "module.hook", "module.hook_global", "module.hook_object_event", "module.hook_tag", "module.load_resource", "module.measure", "module.measure_event", "module.measure_global_event", "module.measure_object_event", "module.open_store", "module.provides", "module.remove_item", "module.require", "module.send", "module.set_global", "module.shared", "module.unhook", "module.unhook_object_event", "module.wrap_event", "module.wrap_global", "module.wrap_object_event", -- mod_http API "module.http_url", } globals = { "_M", -- Methods that can be set on module API "module.unload", "module.add_host", "module.load", "module.add_host", "module.save", "module.restore", "module.command", } allow_defined_top = true unused_secondaries = false codes = true ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV" }; prosody-modules-c53cc1ae4788/mod_http_upload_external/0002755000175500017550000000000013164216767022700 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_http_upload_external/mod_http_upload_external.lua0000644000175500017550000000611313163400720030445 0ustar debacledebacle-- mod_http_upload_external -- -- Copyright (C) 2015-2016 Kim Alvefur -- -- This file is MIT/X11 licensed. -- -- imports local st = require"util.stanza"; local uuid = require"util.uuid".generate; local http = require "util.http"; local dataform = require "util.dataforms".new; local HMAC = require "util.hashes".hmac_sha256; -- config local file_size_limit = module:get_option_number(module.name .. "_file_size_limit", 100 * 1024 * 1024); -- 100 MB local base_url = assert(module:get_option_string(module.name .. "_base_url"), module.name .. "_base_url is a required option"); local secret = assert(module:get_option_string(module.name .. "_secret"), module.name .. "_secret is a required option"); -- depends module:depends("disco"); -- namespace local xmlns_http_upload = "urn:xmpp:http:upload"; -- identity and feature advertising module:add_identity("store", "file", module:get_option_string("name", "HTTP File Upload")) module:add_feature(xmlns_http_upload); module:add_extension(dataform { { name = "FORM_TYPE", type = "hidden", value = xmlns_http_upload }, { name = "max-file-size", type = "text-single" }, }:form({ ["max-file-size"] = tostring(file_size_limit) }, "result")); local function magic_crypto_dust(random, filename, filesize) local message = string.format("%s/%s %d", random, filename, filesize); local digest = HMAC(secret, message, true); random, filename = http.urlencode(random), http.urlencode(filename); return base_url .. random .. "/" .. filename, "?v=" .. digest; end -- hooks module:hook("iq/host/"..xmlns_http_upload..":request", function (event) local stanza, origin = event.stanza, event.origin; local request = stanza.tags[1]; -- local clients only if origin.type ~= "c2s" then module:log("debug", "Request for upload slot from a %s", origin.type); origin.send(st.error_reply(stanza, "cancel", "not-authorized")); return true; end -- validate local filename = request:get_child_text("filename"); if not filename or filename:find("/") then module:log("debug", "Filename %q not allowed", filename or ""); origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid filename")); return true; end local filesize = tonumber(request:get_child_text("size")); if not filesize then module:log("debug", "Missing file size"); origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing or invalid file size")); return true; elseif filesize > file_size_limit then module:log("debug", "File too large (%d > %d)", filesize, file_size_limit); origin.send(st.error_reply(stanza, "modify", "not-acceptable", "File too large", st.stanza("file-too-large", {xmlns=xmlns_http_upload}) :tag("max-size"):text(tostring(file_size_limit)))); return true; end local reply = st.reply(stanza); reply:tag("slot", { xmlns = xmlns_http_upload }); local random = uuid(); local get_url, verify = magic_crypto_dust(random, filename, filesize); reply:tag("get"):text(get_url):up(); reply:tag("put"):text(get_url .. verify):up(); module:log("info", "Handed out upload slot %s to %s@%s", get_url, origin.username, origin.host); origin.send(reply); return true; end); prosody-modules-c53cc1ae4788/mod_http_upload_external/share.php0000644000175500017550000001044613163400720024475 0ustar debacledebacle Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/ /* CONFIGURATION OPTIONS */ /*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/ /* Change this to a directory that is writable by your web server, but is outside your web root */ $CONFIG_STORE_DIR = '/tmp'; /* This must be the same as 'http_upload_external_secret' that you set in Prosody's config file */ $CONFIG_SECRET = 'this is your secret string'; /* For people who need options to tweak that they don't understand... here you are */ $CONFIG_CHUNK_SIZE = 4096; /*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/ /* END OF CONFIGURATION */ /*\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\*/ /* Do not edit below this line unless you know what you are doing (spoiler: nobody does) */ $upload_file_name = substr($_SERVER['PHP_SELF'], strlen($_SERVER['SCRIPT_NAME'])+1); $store_file_name = $CONFIG_STORE_DIR . '/store-' . hash('sha256', $upload_file_name); $request_method = $_SERVER['REQUEST_METHOD']; if(array_key_exists('v', $_GET) === TRUE && $request_method === 'PUT') { $headers = getallheaders(); $upload_file_size = $headers['Content-Length']; $upload_token = $_GET['v']; $calculated_token = hash_hmac('sha256', "$upload_file_name $upload_file_size", $CONFIG_SECRET); if($upload_token !== $calculated_token) { header('HTTP/1.0 403 Forbidden'); exit; } /* Open a file for writing */ $store_file = fopen($store_file_name, 'x'); if($store_file === FALSE) { header('HTTP/1.0 409 Conflict'); exit; } /* PUT data comes in on the stdin stream */ $incoming_data = fopen('php://input', 'r'); /* Read the data a chunk at a time and write to the file */ while ($data = fread($incoming_data, $CONFIG_CHUNK_SIZE)) { fwrite($store_file, $data); } /* Close the streams */ fclose($incoming_data); fclose($store_file); } else if($request_method === 'GET' || $request_method === 'HEAD') { // Send file (using X-Sendfile would be nice here...) if(file_exists($store_file_name)) { header('Content-Disposition: attachment'); header('Content-Type: application/octet-stream'); header('Content-Length: '.filesize($store_file_name)); if($request_method !== 'HEAD') { readfile($store_file_name); } } else { header('HTTP/1.0 404 Not Found'); } } else { header('HTTP/1.0 400 Bad Request'); } exit; prosody-modules-c53cc1ae4788/mod_http_upload_external/README.markdown0000644000175500017550000000647013163400720025365 0ustar debacledebacle--- description: HTTP File Upload (external service) labels: 'Stage-Alpha' --- Introduction ============ This module implements [XEP-0363], which lets clients upload files over HTTP to an external web server. This module generates URLs that are signed using a HMAC. Any web service that can authenticate these URLs can be used. There is a PHP implementation available [here](https://hg.prosody.im/prosody-modules/raw-file/tip/mod_http_upload_external/share.php). To implement your own service compatible with this module, check out the implementation notes below (and if you publish your implementation - let us know!). Configuration ============= Add `"http_upload_external"` to modules_enabled in your global section, or under the host(s) you wish to use it on. External URL ------------ You need to provide the path to the external service. Ensure it ends with '/'. For example, to use the PHP implementation linked above, you might set it to: ``` {.lua} http_upload_external_base_url = "https://your.example.com/path/to/share.php/" ``` Secret ------ Set a long and unpredictable string as your secret. This is so the upload service can verify that the upload comes from mod_http_upload_external, and random strangers can't upload to your server. ``` {.lua} http_upload_external_secret = "this is a secret string!" ``` You need to set exactly the same secret string in your external service. Limits ------ A maximum file size can be set by: ``` {.lua} http_upload_external_file_size_limit = 123 -- bytes ``` Default is 100MB (100\*1024\*1024). Compatibility ============= Works with Prosody 0.9.x and later. Implementation ============== To implement your own external service that is compatible with this module, you need to expose a simple API that allows the HTTP GET, HEAD and PUT methods on arbitrary URLs located on your service. For example, if http_upload_external_base_url is set to `https://example.com/upload/` then your service might receive the following requests: Upload a new file: ``` PUT https://example.com/upload/foo/bar.jpg?v=49e9309ff543ace93d25be90635ba8e9965c4f23fc885b2d86c947a5d59e55b2 ``` Recipient checks the file size and other headers: ``` HEAD https://example.com/upload/foo/bar.jpg ``` Recipient downloads the file: ``` GET https://example.com/upload/foo/bar.jpg ``` The only tricky logic is in validation of the PUT request. Firstly, don't overwrite existing files (return 409 Conflict). Then you need to validate the auth token. This will be in the URL query parameter 'v'. If it is absent, fail with 403 Forbidden. Calculate the expected auth token by reading the value of the Content-Length header of the PUT request. E.g. for a 1MB file will have a Content-Length of '1048576'. Append this to the uploaded file name, separated by a space (0x20) character. For the above example, you would end up with the following string: "foo/bar.jpg 1048576" The auth token is a SHA256 HMAC of this string, using the configured secret as the key. E.g. ``` calculated_auth_token = hmac_sha256("foo/bar.jpg 1048576", "secret string") ``` If this is not equal to the 'v' parameter provided in the upload URL, reject the upload with 403 Forbidden. Note: your language/environment may provide a function for doing a constant-time comparison of these, to guard against timing attacks that may be used to discover the secret key. prosody-modules-c53cc1ae4788/mod_pubsub_github/0002755000175500017550000000000013164216767021315 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_pubsub_github/mod_pubsub_github.lua0000644000175500017550000000265713163400720025510 0ustar debacledebaclemodule:depends("http"); local st = require "util.stanza"; local json = require "util.json"; local formdecode = require "net.http".formdecode; local pubsub_service = module:depends("pubsub").service; local node = module:get_option("github_node", "github"); function handle_POST(event) local data = json.decode(event.request.body); if not data then return "Invalid JSON. From you of all people..."; end for _, commit in ipairs(data.commits) do local ok, err = pubsub_service:publish(node, true, data.repository.name, st.stanza("item", { id = data.repository.name, xmlns = "http://jabber.org/protocol/pubsub" }) :tag("entry", { xmlns = "http://www.w3.org/2005/Atom" }) :tag("id"):text(commit.id):up() :tag("title"):text(commit.message):up() :tag("link", { rel = "alternate", href = commit.url }):up() :tag("published"):text(commit.timestamp):up() :tag("author") :tag("name"):text(commit.author.name):up() :tag("email"):text(commit.author.email):up() :up() ); end module:log("debug", "Handled POST: \n%s\n", tostring(event.request.body)); return "Thank you Github!"; end module:provides("http", { route = { POST = handle_POST; }; }); function module.load() if not pubsub_service.nodes[node] then local ok, err = pubsub_service:create(node, true); if not ok then module:log("error", "Error creating node: %s", err); else module:log("debug", "Node %q created", node); end end end prosody-modules-c53cc1ae4788/mod_pubsub_github/README.markdown0000644000175500017550000000205713163400720023777 0ustar debacledebacle--- labels: 'Stage-Beta' summary: Publish Github commits over pubsub ... Introduction ------------ This module accepts Github web hooks and publishes them to a local pubsub component for XMPP clients to subscribe to. Entries are pushed as Atom payloads. Configuration ------------- Load the module on a pubsub component: Component "pubsub.example.com" "pubsub" modules_enabled = { "pubsub_github" } The module also takes the following config options: Name Default Description -------------- ---------- ---------------------------------------- github\_node "github" The pubsub node to publish commits on. The URL for Github to post to would be either: - http://pubsub.example.com:5280/pubsub\_github - https://pubsub.example.com:5281/pubsub\_github If your HTTP host doesn't match the pubsub component's address, you will need to inform Prosody. For more info see Prosody's [HTTP server documentation](https://prosody.im/doc/http#virtual_hosts). Compatibility ------------- ----- ------- 0.9 Works ----- ------- prosody-modules-c53cc1ae4788/mod_secure_interfaces/0002755000175500017550000000000013164216767022144 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_secure_interfaces/mod_secure_interfaces.lua0000644000175500017550000000132313163400720027153 0ustar debacledebaclelocal secure_interfaces = module:get_option_set("secure_interfaces", { "127.0.0.1", "::1" }); module:hook("stream-features", function (event) local session = event.origin; if session.type ~= "c2s_unauthed" then return; end local socket = session.conn:socket(); if not socket.getsockname then module:log("debug", "Unable to determine local address of incoming connection"); return; end local localip = socket:getsockname(); if secure_interfaces:contains(localip) then module:log("debug", "Marking session from %s to %s as secure", session.ip or "[?]", localip); session.secure = true; else module:log("debug", "Not marking session from %s to %s as secure", session.ip or "[?]", localip); end end, 2500); prosody-modules-c53cc1ae4788/mod_secure_interfaces/README.markdown0000644000175500017550000000206013163400720024620 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: 'Mark some network interfaces (e.g. loopback/LAN) as always secure' ... Introduction ============ Sometimes you might run clients without encryption on the same machine or LAN as Prosody - and you want Prosody to treat them as secure (e.g. allowing plaintext authentication) even though they are not encrypted. This module allows you to tell Prosody which of the current server's interfaces (IP addresses) that you consider to be on secure networks. Configuration ============= Configuration is simple, just load the module like any other by adding it to your modules\_enabled list: modules_enabled = { ... "secure_interfaces"; ... } Then set the list of secure interfaces (just make sure it is set in the global section of your config file, and **not** under a VirtualHost or Component): secure_interfaces = { "127.0.0.1", "::1", "192.168.1.54" } Compatibility ============= ------- --------- 0.9 Works 0.8 Unknown trunk Works ------- --------- prosody-modules-c53cc1ae4788/mod_telnet_tlsinfo/0002755000175500017550000000000013164216767021504 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_telnet_tlsinfo/README.markdown0000644000175500017550000000344213163400720024165 0ustar debacledebacle--- summary: Telnet command for showing TLS info ... Introduction ============ This module adds two commands to the telnet console, `c2s:showtls()` and `s2s:showtls()`. These commands shows TLS parameters, such as ciphers and key agreement protocols, of all c2s or s2s connections. Configuration ============= Just add the module to the `modules_enabled` list. There is no other configuration. modules_enabled = { ... "telnet_tlsinfo"; } Usage ===== Simply type `c2s:showtls()` to show client connections or `s2s:showtls()` for server-to-server connections. These commands can also take a JID for limiting output to matching users or servers. s2s:showtls("prosody.im") | example.com -> prosody.im | protocol: TLSv1.1 | cipher: DHE-RSA-AES256-SHA | encryption: AES(256) | algbits: 256 | bits: 256 | authentication: RSA | key: DH | mac: SHA1 | export: false Field Description ---------------- ------------------------------------------------------------------------------------------------- protocol The protocol used. **Note**: With older LuaSec, this is the protocol that added the used cipher cipher The OpenSSL cipher string for the currently used cipher encryption Encryption algorithm used bits, algbits Secret bits involved in the cipher authentication The authentication algoritm used mac Message authentication algorithm used key Key exchange mechanism used. export Whethere an export cipher is used Compatibility ============= --------------------- ------- 0.9 with LuaSec 0.5 Works --------------------- ------- prosody-modules-c53cc1ae4788/mod_telnet_tlsinfo/mod_telnet_tlsinfo.lua0000644000175500017550000000230413163400720026053 0ustar debacledebacle-- mod_telnet_tlsinfo.lua module:set_global(); module:depends("admin_telnet"); local console_env = module:shared("/*/admin_telnet/env"); local c2s_sessions = module:shared("/*/c2s/sessions"); local s2s_sessions = module:shared("/*/s2s/sessions"); local function print_tlsinfo(print, session) if session.secure then local sock = session.conn:socket() for k,v in pairs(sock:info()) do print(("%20s: %s"):format(k, tostring(v))) end else print(("%20s: %s"):format("protocol", "TCP")) end end function console_env.c2s:showtls(pat) local print = self.session.print; for _, session in pairs(c2s_sessions) do if not pat or session.full_jid and session.full_jid:find(pat, nil, true) then print(session.full_jid or "unauthenticated") print_tlsinfo(print, session); print"" end end end function console_env.s2s:showtls(pat) local print = self.session.print; for _, session in pairs(s2s_sessions) do if not pat or session.from_host == pat or session.to_host == pat then if session.direction == "outgoing" then print(session.from_host, "->", session.to_host) else print(session.to_host, "<-", session.from_host) end print_tlsinfo(print, session); print"" end end end prosody-modules-c53cc1ae4788/mod_measure_message_length/0002755000175500017550000000000013164216767023161 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_measure_message_length/mod_measure_message_length.lua0000644000175500017550000000120213163400720031201 0ustar debacledebaclelocal bytes = module:measure("bytes", "sizes"); local lines = module:measure("lines", "count"); local words = module:measure("words", "count"); local function measure_length(event) local body = event.stanza:get_child_text("body"); if body then bytes(#body); lines(select(2, body:gsub("[^\n]+",""))); words(select(2, body:gsub("%S+",""))); end end module:hook("message/full", measure_length); module:hook("message/bare", measure_length); module:hook("message/host", measure_length); module:hook("pre-message/full", measure_length); module:hook("pre-message/bare", measure_length); module:hook("pre-message/host", measure_length); prosody-modules-c53cc1ae4788/mod_measure_message_length/README.markdown0000644000175500017550000000013613163400720025637 0ustar debacledebacleSimple module that collects statistics on message length in bytes, word count and line count. prosody-modules-c53cc1ae4788/mod_block_s2s_subscriptions/0002755000175500017550000000000013164216767023323 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_block_s2s_subscriptions/mod_block_s2s_subscriptions.lua0000644000175500017550000000126513163400720031516 0ustar debacledebacle local jid_split = require "util.jid".split; local jid_bare = require "util.jid".bare; local load_roster = require "core.rostermanager".load_roster; local blocked_servers = module:get_option_set("block_s2s_subscriptions")._items; function filter_presence(event) if blocked_servers[event.origin.from_host] and event.stanza.attr.type == "subscribe" then local stanza = event.stanza; local to_user, to_host = jid_split(stanza.attr.to); local roster = load_roster(to_user, to_host); if roster and roster[jid_bare(stanza.attr.from)] then return; -- In roster, pass through end return true; -- Drop end end module:hook("presence/bare", filter_presence, 200); -- Client receiving prosody-modules-c53cc1ae4788/mod_tls_policy/0002755000175500017550000000000013164216767020634 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_tls_policy/mod_tls_policy.lua0000755000175500017550000000240113163400720024334 0ustar debacledebacle assert(require"ssl.core".info, "Incompatible LuaSec version"); local function hook(event_name, typ, policy) if not policy then return end if policy == "FS" then policy = { cipher = "^E?C?DHE%-" }; elseif type(policy) == "string" then policy = { cipher = policy }; end module:hook(event_name, function (event) local origin = event.origin; if origin.encrypted then local info = origin.conn:socket():info(); for key, what in pairs(policy) do module:log("debug", "Does info[%q] = %s match %s ?", key, tostring(info[key]), tostring(what)); if (type(what) == "number" and what < info[key] ) or (type(what) == "string" and not info[key]:match(what)) then origin:close({ condition = "policy-violation", text = ("TLS %s '%s' not acceptable"):format(key, tostring(info[key])) }); return false; end module:log("debug", "Seems so"); end module:log("debug", "Policy matches"); end end, 1000); end local policy = module:get_option(module.name, {}); if type(policy) == "string" then policy = { c2s = policy, s2s = policy }; end hook("stream-features", "c2s", policy.c2s); hook("s2s-stream-features", "s2sin", policy.s2sin or policy.s2s); hook("stanza/http://etherx.jabber.org/streams:features", "s2sout", policy.s2sout or policy.s2s); prosody-modules-c53cc1ae4788/mod_tls_policy/README.markdown0000644000175500017550000000226013163400720023312 0ustar debacledebacle--- summary: Cipher policy enforcement with application level error reporting ... # Introduction This module arose from discussions at the XMPP Summit about enforcing better ciphers in TLS. It may seem attractive to disallow some insecure ciphers or require forward secrecy, but doing this at the TLS level would the user with an unhelpful "Encryption failed" message. This module does this enforcing at the application level, allowing better error messages. # Configuration First, download and add the module to `module_enabled`. Then you can decide on what policy you want to have. Requiring ciphers with forward secrecy is the most simple to set up. ``` lua tls_policy = "FS" -- allow only ciphers that enable forward secrecy ``` A more complicated example: ``` lua tls_policy = { c2s = { encryption = "AES"; -- Require AES (or AESGCM) encryption protocol = "TLSv1.2"; -- and TLSv1.2 bits = 128; -- and at least 128 bits (FIXME: remember what this meant) } s2s = { cipher = "AESGCM"; -- Require AESGCM ciphers protocol = "TLSv1.[12]"; -- and TLSv1.1 or 1.2 authentication = "RSA"; -- with RSA authentication }; } ``` # Compatibility Requires LuaSec 0.5 prosody-modules-c53cc1ae4788/mod_statistics/0002755000175500017550000000000013164216767020645 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_statistics/prosodytop.lua0000644000175500017550000000612713163400720023555 0ustar debacledebaclelocal curses = require "curses"; local server = require "net.server_select"; local timer = require "util.timer"; assert(curses.timeout, "Incorrect version of curses library. Try 'sudo luarocks install luaposix'"); local top = require "top"; function main() local stdscr = curses.stdscr() -- it's a userdatum --stdscr:clear(); local view = top.new({ stdscr = stdscr; prosody = { up_since = os.time() }; conn_list = {}; }); timer.add_task(0.01, function () local ch = stdscr:getch(); if ch then if stdscr:getch() == 410 then view:resized(); else curses.ungetch(ch); end end return 0.2; end); timer.add_task(0, function () view:draw(); return 1; end); --[[ posix.signal(28, function () table.insert(view.conn_list, { jid = "WINCH" }); --view:draw(); end); ]] -- Fake socket object around stdin local stdin = { getfd = function () return 0; end; dirty = function (self) return false; end; settimeout = function () end; send = function (_, d) return #d, 0; end; close = function () end; receive = function (_, patt) local ch = stdscr:getch(); if ch >= 0 and ch <=255 then return string.char(ch); elseif ch == 410 then view:resized(); else table.insert(view.conn_list, { jid = tostring(ch) }); --FIXME end return ""; end }; local function on_incoming(stdin, text) -- TODO: Handle keypresses if text:lower() == "q" then os.exit(); end end stdin = server.wrapclient(stdin, "stdin", 0, { onincoming = on_incoming, ondisconnect = function () end }, "*a"); local function handle_line(line) local e = { STAT = function (name) return function (value) view:update_stat(name, value); end end; SESS = function (id) return function (jid) return function (stats) view:update_session(id, jid, stats); end end end; }; local chunk = assert(loadstring(line)); setfenv(chunk, e); chunk(); end local stats_listener = {}; function stats_listener.onconnect(conn) --stdscr:mvaddstr(6, 0, "CONNECTED"); end local partial = ""; function stats_listener.onincoming(conn, data) --print("DATA", data) data = partial..data; local lastpos = 1; for line, pos in data:gmatch("([^\n]+)\n()") do lastpos = pos; handle_line(line); end partial = data:sub(lastpos); end function stats_listener.ondisconnect(conn, err) stdscr:mvaddstr(6, 0, "DISCONNECTED: "..(err or "unknown")); end local conn = require "socket".tcp(); assert(conn:connect("localhost", 5782)); handler = server.wrapclient(conn, "localhost", 5782, stats_listener, "*a"); end return { run = function () --os.setlocale("UTF-8", "all") curses.initscr() curses.cbreak() curses.echo(false) -- not noecho ! curses.nl(false) -- not nonl ! curses.timeout(0); local ok, err = pcall(main); --while true do stdscr:getch() end --stdscr:endwin() if ok then ok, err = xpcall(server.loop, debug.traceback); end curses.endwin(); --stdscr:refresh(); if not ok then print(err); end print"DONE" end; }; prosody-modules-c53cc1ae4788/mod_statistics/mod_statistics.lua0000644000175500017550000000722613163400720024365 0ustar debacledebaclemodule:set_global(); local stats = module:require("mod_statistics/stats"); local filters = require "util.filters"; local serialize = require "util.serialization".serialize; local cached_values = {}; local sessions = {}; local function push_stat(conn, name, value) local value_str = serialize(value); return conn:write((("STAT %q (%s)\n"):format(name, value_str):gsub("\\\n", "\\n"))); end local function push_stat_to_all(name, value) for conn in pairs(sessions) do push_stat(conn, name, value); end end local session_stats_tpl = ([[{ message_in = %d, message_out = %d; presence_in = %d, presence_out = %d; iq_in = %d, iq_out = %d; bytes_in = %d, bytes_out = %d; }]]):gsub("%s", ""); local jid_fields = { c2s = "full_jid"; s2sin = "from_host"; s2sout = "to_host"; component = "host"; }; local function push_session_to_all(session, stats) local id = tostring(session):match("[a-f0-9]+$"); -- FIXME: Better id? :/ local stanzas_in, stanzas_out = stats.stanzas_in, stats.stanzas_out; local s = (session_stats_tpl):format( stanzas_in.message, stanzas_out.message, stanzas_in.presence, stanzas_out.presence, stanzas_in.iq, stanzas_out.iq, stats.bytes_in, stats.bytes_out); local jid = session[jid_fields[session.type]] or ""; for conn in pairs(sessions) do conn:write(("SESS %q %q %s\n"):format(id, jid, s)); end end local available_stats = stats.stats; local active_sessions = stats.active_sessions; -- Handle statistics provided by other modules local function item_handlers(host) host = host and (host.."/") or ""; return function (event) -- Added local stats = event.item.statistics; local group = host..(stats.name and (stats.name.."::") or ""); for name, stat in pairs(stats) do available_stats[group..name] = stat; end end, function (event) -- Removed local stats = event.item.statistics; local group = host..(stats.name and (stats.name.."::") or ""); for name, stat in pairs(stats) do available_stats[group..name] = nil; end end; end module:handle_items("statistics-provider", item_handlers()); function module.add_host(module) module:handle_items("statistics-provider", item_handlers(module.host)); end -- Network listener local listener = {}; function listener.onconnect(conn) sessions[conn] = {}; push_stat(conn, "version", prosody.version); for name, value in pairs(cached_values) do push_stat(conn, name, value); end conn:write("\n"); -- Signal end of first batch (for non-streaming clients) end function listener.onincoming(conn, data) end function listener.ondisconnect(conn) sessions[conn] = nil; end function listener.onreadtimeout() return true; end function module.load() if not(prosody and prosody.arg) then return; end filters.add_filter_hook(stats.filter_hook); module:add_timer(1, function () for stat_name, stat in pairs(available_stats) do if stat.get then local cached = cached_values[stat_name]; local new_value = stat.get(); if new_value ~= cached then push_stat_to_all(stat_name, new_value); cached_values[stat_name] = new_value; end end end for session, session_stats in pairs(active_sessions) do active_sessions[session] = nil; push_session_to_all(session, session_stats); end return 1; end); end function module.unload() filters.remove_filter_hook(stats.filter_hook); end function module.command( args ) local command = args[1]; if command == "top" then local dir = module:get_directory(); package.path = dir.."/?.lua;"..dir.."/?.lib.lua;"..package.path; local prosodytop = require "prosodytop"; prosodytop.run(); end end if prosody and prosody.arg then module:provides("net", { default_port = 5782; listener = listener; private = true; }); end prosody-modules-c53cc1ae4788/mod_statistics/top.lua0000644000175500017550000001523213163400720022132 0ustar debacledebaclemodule("prosodytop", package.seeall); local array = require "util.array"; local it = require "util.iterators"; local curses = require "curses"; local stats = require "stats".stats; local time = require "socket".gettime; local sessions_idle_after = 60; local stanza_names = {"message", "presence", "iq"}; local top = {}; top.__index = top; local status_lines = { "Prosody $version - $time up $up_since, $total_users users, $cpu busy"; "Connections: $total_c2s c2s, $total_s2sout s2sout, $total_s2sin s2sin, $total_component component"; "Memory: $memory_lua lua, $memory_allocated process ($memory_used in use)"; "Stanzas in: $message_in_per_second message/s, $presence_in_per_second presence/s, $iq_in_per_second iq/s"; "Stanzas out: $message_out_per_second message/s, $presence_out_per_second presence/s, $iq_out_per_second iq/s"; }; function top:draw() self:draw_status(); self:draw_column_titles(); self:draw_conn_list(); self.statuswin:refresh(); self.listwin:refresh(); --self.infowin:refresh() self.stdscr:move(#status_lines,0) end -- Width specified as cols or % of unused space, defaults to -- title width if not specified local conn_list_columns = { { title = "ID", key = "id", width = "8" }; { title = "JID", key = "jid", width = "100%" }; { title = "STANZAS IN>", key = "total_stanzas_in", align = "right" }; { title = "MSG", key = "message_in", align = "right", width = "4" }; { title = "PRES", key = "presence_in", align = "right", width = "4" }; { title = "IQ", key = "iq_in", align = "right", width = "4" }; { title = "STANZAS OUT>", key = "total_stanzas_out", align = "right" }; { title = "MSG", key = "message_out", align = "right", width = "4" }; { title = "PRES", key = "presence_out", align = "right", width = "4" }; { title = "IQ", key = "iq_out", align = "right", width = "4" }; { title = "BYTES IN", key = "bytes_in", align = "right" }; { title = "BYTES OUT", key = "bytes_out", align = "right" }; }; function top:draw_status() for row, line in ipairs(status_lines) do self.statuswin:mvaddstr(row-1, 0, (line:gsub("%$([%w_]+)", self.data))); self.statuswin:clrtoeol(); end -- Clear stanza counts for _, stanza_type in ipairs(stanza_names) do self.prosody[stanza_type.."_in_per_second"] = 0; self.prosody[stanza_type.."_out_per_second"] = 0; end end local function padright(s, width) return s..string.rep(" ", width-#s); end local function padleft(s, width) return string.rep(" ", width-#s)..s; end function top:resized() self:recalc_column_widths(); --self.stdscr:clear(); self:draw(); end function top:recalc_column_widths() local widths = {}; self.column_widths = widths; local total_width = curses.cols()-4; local free_width = total_width; for i = 1, #conn_list_columns do local width = conn_list_columns[i].width or "0"; if not(type(width) == "string" and width:sub(-1) == "%") then width = math.max(tonumber(width), #conn_list_columns[i].title+1); widths[i] = width; free_width = free_width - width; end end for i = 1, #conn_list_columns do if not widths[i] then local pc_width = tonumber((conn_list_columns[i].width:gsub("%%$", ""))); widths[i] = math.floor(free_width*(pc_width/100)); end end return widths; end function top:draw_column_titles() local widths = self.column_widths; self.listwin:attron(curses.A_REVERSE); self.listwin:mvaddstr(0, 0, " "); for i, column in ipairs(conn_list_columns) do self.listwin:addstr(padright(column.title, widths[i])); end self.listwin:addstr(" "); self.listwin:attroff(curses.A_REVERSE); end local function session_compare(session1, session2) local stats1, stats2 = session1.stats, session2.stats; return (stats1.total_stanzas_in + stats1.total_stanzas_out) > (stats2.total_stanzas_in + stats2.total_stanzas_out); end function top:draw_conn_list() local rows = curses.lines()-(#status_lines+2)-5; local cutoff_time = time() - sessions_idle_after; local widths = self.column_widths; local top_sessions = array.collect(it.values(self.active_sessions)):sort(session_compare); for index = 1, rows do session = top_sessions[index]; if session then if session.last_update < cutoff_time then self.active_sessions[session.id] = nil; else local row = {}; for i, column in ipairs(conn_list_columns) do local width = widths[i]; local v = tostring(session[column.key] or ""):sub(1, width); if #v < width then if column.align == "right" then v = padleft(v, width-1).." "; else v = padright(v, width); end end table.insert(row, v); end if session.updated then self.listwin:attron(curses.A_BOLD); end self.listwin:mvaddstr(index, 0, " "..table.concat(row)); if session.updated then session.updated = false; self.listwin:attroff(curses.A_BOLD); end end else -- FIXME: How to clear a line? It's 5am and I don't feel like reading docs. self.listwin:move(index, 0); self.listwin:clrtoeol(); end end end function top:update_stat(name, value) self.prosody[name] = value; end function top:update_session(id, jid, stats) self.active_sessions[id] = stats; stats.id, stats.jid, stats.stats = id, jid, stats; stats.total_bytes = stats.bytes_in + stats.bytes_out; for _, stanza_type in ipairs(stanza_names) do self.prosody[stanza_type.."_in_per_second"] = (self.prosody[stanza_type.."_in_per_second"] or 0) + stats[stanza_type.."_in"]; self.prosody[stanza_type.."_out_per_second"] = (self.prosody[stanza_type.."_out_per_second"] or 0) + stats[stanza_type.."_out"]; end stats.total_stanzas_in = stats.message_in + stats.presence_in + stats.iq_in; stats.total_stanzas_out = stats.message_out + stats.presence_out + stats.iq_out; stats.last_update = time(); stats.updated = true; end function new(base) setmetatable(base, top); base.data = setmetatable({}, { __index = function (t, k) local stat = stats[k]; if stat and stat.tostring then if type(stat.tostring) == "function" then return stat.tostring(base.prosody[k]); elseif type(stat.tostring) == "string" then local v = base.prosody[k]; if v == nil then return "?"; end return (stat.tostring):format(v); end end return base.prosody[k]; end; }); base.active_sessions = {}; base.statuswin = curses.newwin(#status_lines, 0, 0, 0); base.promptwin = curses.newwin(1, 0, #status_lines, 0); base.promptwin:addstr(""); base.promptwin:refresh(); base.listwin = curses.newwin(curses.lines()-(#status_lines+2)-5, 0, #status_lines+1, 0); base.listwin:syncok(); base.infowin = curses.newwin(5, 0, curses.lines()-5, 0); base.infowin:mvaddstr(1, 1, "Hello world"); base.infowin:border(0,0,0,0); base.infowin:syncok(); base.infowin:refresh(); base:resized(); return base; end return _M; prosody-modules-c53cc1ae4788/mod_statistics/stats.lib.lua0000644000175500017550000001145113163400720023232 0ustar debacledebaclelocal it = require "util.iterators"; local log = require "util.logger".init("stats"); local has_pposix, pposix = pcall(require, "util.pposix"); local human; do local tostring = tostring; local s_format = string.format; local m_floor = math.floor; local m_max = math.max; local prefixes = "kMGTPEZY"; local multiplier = 1024; function human(num) num = tonumber(num) or 0; local m = 0; while num >= multiplier and m < #prefixes do num = num / multiplier; m = m + 1; end return s_format("%0."..m_max(0,3-#tostring(m_floor(num))).."f%sB", num, m > 0 and (prefixes:sub(m,m) .. "i") or ""); end end local last_cpu_wall, last_cpu_clock; local get_time = require "socket".gettime; local active_sessions, active_jids = {}, {}; local c2s_sessions, s2s_sessions; if prosody and prosody.arg then c2s_sessions, s2s_sessions = module:shared("/*/c2s/sessions", "/*/s2s/sessions"); end local stats = { total_users = { get = function () return it.count(it.keys(bare_sessions)); end }; total_c2s = { get = function () return it.count(it.keys(full_sessions)); end }; total_s2sin = { get = function () local i = 0; for conn,sess in next,s2s_sessions do if sess.direction == "incoming" then i = i + 1 end end return i end }; total_s2sout = { get = function () local i = 0; for conn,sess in next,s2s_sessions do if sess.direction == "outgoing" then i = i + 1 end end return i end }; total_s2s = { get = function () return it.count(it.keys(s2s_sessions)); end }; total_component = { get = function () local count = 0; for host, host_session in pairs(hosts) do if host_session.type == "component" then local c = host_session.modules.component; if c and c.connected then -- 0.9 only count = count + 1; end end end return count; end }; up_since = { get = function () return prosody.start_time; end; tostring = function (up_since) return tostring(os.time()-up_since).."s"; end; }; memory_lua = { get = function () return math.ceil(collectgarbage("count")*1024); end; tostring = human; }; time = { tostring = function () return os.date("%T"); end; }; cpu = { get = function () local new_wall, new_clock = get_time(), os.clock(); local pc = 0; if last_cpu_wall then pc = 100/((new_wall-last_cpu_wall)/(new_clock-last_cpu_clock)); end last_cpu_wall, last_cpu_clock = new_wall, new_clock; return math.ceil(pc); end; tostring = "%s%%"; }; }; local memory_update_interval = 60; if prosody and prosody.arg then memory_update_interval = module:get_option_number("statistics_meminfo_interval", 60); end if has_pposix and pposix.meminfo then local cached_meminfo, last_cache_update; local function meminfo() if not cached_meminfo or (os.time() - last_cache_update) > memory_update_interval then cached_meminfo = pposix.meminfo(); last_cache_update = os.time(); end return cached_meminfo; end stats.memory_allocated = { get = function () return math.ceil(meminfo().allocated); end; tostring = human; } stats.memory_used = { get = function () return math.ceil(meminfo().used); end; tostring = human; } stats.memory_unused = { get = function () return math.ceil(meminfo().unused); end; tostring = human; } stats.memory_returnable = { get = function () return math.ceil(meminfo().returnable); end; tostring = human; } end local add_statistics_filter; -- forward decl if prosody and prosody.arg then -- ensures we aren't in prosodyctl setmetatable(active_sessions, { __index = function ( t, k ) local v = { bytes_in = 0, bytes_out = 0; stanzas_in = { message = 0, presence = 0, iq = 0; }; stanzas_out = { message = 0, presence = 0, iq = 0; }; } rawset(t, k, v); return v; end }); local filters = require "util.filters"; local function handle_stanza_in(stanza, session) local s = active_sessions[session].stanzas_in; local n = s[stanza.name]; if n then s[stanza.name] = n + 1; end return stanza; end local function handle_stanza_out(stanza, session) local s = active_sessions[session].stanzas_out; local n = s[stanza.name]; if n then s[stanza.name] = n + 1; end return stanza; end local function handle_bytes_in(bytes, session) local s = active_sessions[session]; s.bytes_in = s.bytes_in + #bytes; return bytes; end local function handle_bytes_out(bytes, session) local s = active_sessions[session]; s.bytes_out = s.bytes_out + #bytes; return bytes; end function add_statistics_filter(session) filters.add_filter(session, "stanzas/in", handle_stanza_in); filters.add_filter(session, "stanzas/out", handle_stanza_out); filters.add_filter(session, "bytes/in", handle_bytes_in); filters.add_filter(session, "bytes/out", handle_bytes_out); end end return { stats = stats; active_sessions = active_sessions; filter_hook = add_statistics_filter; }; prosody-modules-c53cc1ae4788/mod_rawdebug/0002755000175500017550000000000013164216767020253 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_rawdebug/README.markdown0000644000175500017550000000044413163400720022733 0ustar debacledebacle--- summary: Extra verbose logging of sent and received --- Summary ======= Sometimes it is useful to get the raw XML logs from clients for debugging purposes, but some clients don't expose this. This module logs dumps everything sent and received into debug logs, for debugging purposes. prosody-modules-c53cc1ae4788/mod_rawdebug/mod_rawdebug.lua0000644000175500017550000000127213163400720023374 0ustar debacledebaclemodule:set_global(); local tostring = tostring; local filters = require "util.filters"; local function log_send(t, session) if t and t ~= "" and t ~= " " then session.log("debug", "SEND(%d): %s", #t, tostring(t)); end return t; end local function log_recv(t, session) if t and t ~= "" and t ~= " " then session.log("debug", "RECV(%d): %s", #t, tostring(t)); end return t; end local function init_raw_logging(session) filters.add_filter(session, "bytes/in", log_recv, -10000); filters.add_filter(session, "bytes/out", log_send, 10000); end filters.add_filter_hook(init_raw_logging); function module.unload() -- luacheck: ignore filters.remove_filter_hook(init_raw_logging); end prosody-modules-c53cc1ae4788/mod_http_dir_listing/0002755000175500017550000000000013164216767022021 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_http_dir_listing/README.markdown0000644000175500017550000000146013163400720024500 0ustar debacledebacle--- summary: HTTP directory listing ... Introduction ============ This module generates directory listings when invoked by `mod_http_files`. See [documentation on `mod_http_files`](http://prosody.im/doc/modules/mod_http_files). Configuration ============= The module itself doesn't have any configuration of its own, just enable the it along with `mod_http_files`. modules_enabled = { ... "http_files"; "http_dir_listing"; } http_dir_listing = true; The layout, CSS and icons in the `resources/` directory can be customized or replaced. All resources are cached in memory when the module is loaded and the images are inlined in the CSS. Compatibility ============= ------- -------------- trunk Works 0.9 Works 0.8 Doesn't work ------- -------------- prosody-modules-c53cc1ae4788/mod_http_dir_listing/http_dir_listing/0002755000175500017550000000000013164216767025367 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_http_dir_listing/http_dir_listing/mod_http_dir_listing.lua0000644000175500017550000000431013163400720032252 0ustar debacledebacle-- 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 mime = module:shared("/*/http_files/mime"); 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 local mime_map = mime.types; for file in lfs.dir(full_path) do if file:sub(1,1) ~= "." then local attr = stat(full_path..file) or {}; local path = { file }; local file_ext = file:match"%.([^.]+)$"; local type = attr.mode == "file" and file_ext and mime_map and mime_map[file_ext] or nil; local class = table.concat({ attr.mode or "unknown", file_ext, type and type:match"^[^/]+" }, " "); path.is_directory = attr.mode == "directory"; filelist:tag("li", { class = class }) :tag("a", { href = build_path(path), type = type }):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); prosody-modules-c53cc1ae4788/mod_http_dir_listing/http_dir_listing/resources/0002755000175500017550000000000013164216767027401 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_http_dir_listing/http_dir_listing/resources/style.css0000644000175500017550000000103313163400720031225 0ustar debacledebacle 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;} prosody-modules-c53cc1ae4788/mod_http_dir_listing/http_dir_listing/resources/text-x-generic.png0000644000175500017550000000041513163400720032727 0ustar debacledebaclePNG  IHDR(-SEPLTET- tRNSV5gbKGD|ѨeIDATA00 ?VJj6Go/泄*4kD0ohB h2!74(iИd/cpAHnƙo?uoH]IENDB`prosody-modules-c53cc1ae4788/mod_http_dir_listing/http_dir_listing/resources/folder.png0000644000175500017550000000110513163400720031334 0ustar debacledebaclePNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT81kQsW,)2v._?VW(Ĺ`Y)$Pi&%߽&6XZw9{yy_v7 w2 nY33 w~?t:ϗ9gBe8~T9gWU& $vxLӪ+bfkvܤjc$h4b<?\YPUS-~H̚??́={U9gO)1m!rx5)_;9x~J)N ˲|@+U%猈u#j9xs3DظiQ ATc;YuiIjAbR7 inPEar6_TRJ/sN ggD FNuk"ERR;'|:uxlQew"E‹G!c1IENDB`prosody-modules-c53cc1ae4788/mod_http_dir_listing/http_dir_listing/resources/user-home.png0000644000175500017550000000124713163400720031774 0ustar debacledebaclePNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<$IDAT8MkSA3skĪX0BE"{ɪ t/н?]7DN("V)MI6i;3w|Ƴ9xGjcU]LU4bz~3HUxhlT`=Ƙs9@N<c J Az^4M/{0t  s.۽ܼK I="5) oߤ~FhT{a +Y;5`Ao)_aAܹ1,3,g.p4^1@CI'laaeTe G6FXB9jipPƊB$=X#v#H1""d>RN|xAɣRN\Ue("[?ZI 팾i$Nwg}Wm/6 OIENDB`prosody-modules-c53cc1ae4788/mod_http_dir_listing/http_dir_listing/resources/template.html0000644000175500017550000000033113163400720032054 0ustar debacledebacle Index of {path}

Index of {path}

{filelist}
{footer}
prosody-modules-c53cc1ae4788/mod_pubsub_pivotaltracker/0002755000175500017550000000000013164216767023065 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_pubsub_pivotaltracker/mod_pubsub_pivotaltracker.lua0000644000175500017550000000437213163400720031024 0ustar debacledebaclemodule:depends("http"); local lom = require "lxp.lom"; local st = require "util.stanza"; local json = require "util.json"; local datetime = require "util.datetime".datetime; local pubsub_service = module:depends("pubsub").service; local node = module:get_option("pivotaltracker_node", "tracker"); local stanza_mt = require "util.stanza".stanza_mt; local function stanza_from_lom(lom) if lom.tag then local child_tags, attr = {}, {}; local stanza = setmetatable({ name = lom.tag, attr = attr, tags = child_tags }, stanza_mt); for i, attr_name in ipairs(lom.attr) do attr[attr_name] = lom.attr[attr_name] end for i, child in ipairs(lom) do if child.tag then child = stanza_from_lom(child); child_tags[#child_tags+1] = child; end stanza[i] = child; end return stanza; else return lom; end end function handle_POST(event) local data = lom.parse(event.request.body); if not data then return "Invalid XML. From you of all people..."; end data = stanza_from_lom(data); if data.name ~= "activity" then return "Unrecognised XML element: "..data.name; end local activity_id = data:get_child("id"):get_text(); local description = data:get_child("description"):get_text(); local author_name = data:get_child("author"):get_text(); local story = data:get_child("stories"):get_child("story"); local story_link = story:get_child("url"):get_text(); local ok, err = pubsub_service:publish(node, true, "activity", st.stanza("item", { id = "activity", xmlns = "http://jabber.org/protocol/pubsub" }) :tag("entry", { xmlns = "http://www.w3.org/2005/Atom" }) :tag("id"):text(activity_id):up() :tag("title"):text(description):up() :tag("link", { rel = "alternate", href = story_link }):up() :tag("published"):text(datetime()):up() :tag("author") :tag("name"):text(author_name):up() :up() ); module:log("debug", "Handled POST: \n%s\n", tostring(event.request.body)); return "Thank you Pivotal!"; end module:provides("http", { route = { POST = handle_POST; }; }); function module.load() if not pubsub_service.nodes[node] then local ok, err = pubsub_service:create(node, true); if not ok then module:log("error", "Error creating node: %s", err); else module:log("debug", "Node %q created", node); end end end prosody-modules-c53cc1ae4788/mod_onhold/0002755000175500017550000000000013164216767017736 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_onhold/mod_onhold.lua0000644000175500017550000000431513163400720022543 0ustar debacledebacle-- Prosody IM -- Copyright (C) 2008-2009 Matthew Wild -- Copyright (C) 2008-2009 Waqas Hussain -- Copyright (C) 2009 Jeff Mitchell -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local datamanager = require "util.datamanager"; local jid_bare = require "util.jid".bare; local jid_split = require "util.jid".split; local st = require "util.stanza"; local datetime = require "util.datetime"; local ipairs = ipairs; local onhold_jids = module:get_option("onhold_jids") or {}; for _, jid in ipairs(onhold_jids) do onhold_jids[jid] = true; end function process_message(event) local session, stanza = event.origin, event.stanza; local to = stanza.attr.to; local from = jid_bare(stanza.attr.from); local node, host; local onhold_node, onhold_host; if to then node, host = jid_split(to) else node, host = session.username, session.host; end if onhold_jids[from] then stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy(); local result = datamanager.list_append(node, host, "onhold", st.preserialize(stanza)); stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil; return true; end return nil; end module:hook("message/bare", process_message, 5); module:hook("message/full", process_message, 5); module:hook("presence/bare", function(event) if event.origin.presence then return nil; end local session = event.origin; local node, host = session.username, session.host; local from; local de_stanza; local data = datamanager.list_load(node, host, "onhold"); local newdata = {}; if not data then return nil; end for _, stanza in ipairs(data) do de_stanza = st.deserialize(stanza); from = jid_bare(de_stanza.attr.from); if not onhold_jids[from] then de_stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = de_stanza.attr.stamp}):up(); -- XEP-0203 de_stanza:tag("x", {xmlns = "jabber:x:delay", from = host, stamp = de_stanza.attr.stamp_legacy}):up(); -- XEP-0091 (deprecated) de_stanza.attr.stamp, de_stanza.attr.stamp_legacy = nil, nil; session.send(de_stanza); else table.insert(newdata, stanza); end end datamanager.list_store(node, host, "onhold", newdata); return nil; end, 5); prosody-modules-c53cc1ae4788/mod_onhold/README.markdown0000644000175500017550000000147013163400720022416 0ustar debacledebacle--- labels: summary: 'Module enabling "on-hold" functionality' ... Introduction ============ Enable mod\_onhold to allow temporarily placing messages from particular JIDs "on hold" -- i.e. store them, but do not deliver them until the hold status is taken away. Details ======= Right now, it is configured through adding JIDs to a list in prosody.cfg.lua. Eventually, more dynamically configurable support will be added (i.e. with ad-hoc commands or some such thing). Simply enable mod\_onhold in your list of modules, and then add a line: onhold\_jids = { "someone@address.com", "someoneelse@address2.com" } Until those JIDs are removed, messages from those JIDs will not be delivered. Once they are removed and prosody is restarted, they will be delivered the next time the user to which they are directed logs on. prosody-modules-c53cc1ae4788/mod_measure_storage/0002755000175500017550000000000013164216767021640 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_measure_storage/mod_measure_storage.lua0000644000175500017550000000335013163400720026345 0ustar debacledebaclemodule:set_global() local function return_args_after_calling(f, ...) f(); return ... end local function time_method(module, store_name, store_type, method_name, method_function) local opt_use_tags = module:get_option_boolean("measure_storage_tagged_metric", false); local metric_name, metric_tags; if opt_use_tags then metric_name, metric_tags = "storage_operation", ("store_name:%s,store_type:%s,store_operation:%s"):format(store_name, store_type, method_name); else metric_name = store_name.."_"..store_type.."_"..method_name; end local measure_operation_started = module:measure(metric_name, "times", metric_tags); return function (...) module:log("debug", "Measuring storage operation %s (%s)", metric_name, metric_tags or "no tags"); local measure_operation_complete = measure_operation_started(); return return_args_after_calling(measure_operation_complete, method_function(...)); end; end local function wrap_store(module, store_name, store_type, store) local new_store = setmetatable({}, { __index = function (t, method_name) local original_method = store[method_name]; if type(original_method) ~= "function" then if original_method then rawset(t, method_name, original_method); end return original_method; end local timed_method = time_method(module, store_name, store_type, method_name, original_method); rawset(t, method_name, timed_method); return timed_method; end; }); return new_store; end local function hook_event(module) module:hook("store-opened", function(event) event.store = wrap_store(module, event.store_name, event.store_type or "keyval", event.store); end); end function module.load() hook_event(module); end function module.add_host(module) hook_event(module); end prosody-modules-c53cc1ae4788/mod_measure_storage/README.markdown0000644000175500017550000000050513163400720024316 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: Measure storage API operations --- Introduction ============ This module collects statistics from storage operations. Configuration ============= Enable tagged metrics, whatever that is: ``` {.lua} measure_storage_tagged_metric = true ``` Compatibility ============= Requires 0.10 prosody-modules-c53cc1ae4788/mod_reload_modules/0002755000175500017550000000000013164216767021451 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_reload_modules/README.markdown0000644000175500017550000000154613163400720024135 0ustar debacledebacle--- labels: - 'Stage-Stable' summary: Automatically reload modules with the config ... Introduction ------------ By default Prosody does not reload modules at runtime unless instructed to via one of its admin interfaces. However sometimes you want to easily reload a module to apply new settings when the config changes. mod\_reload\_modules will reload a set list of modules every time Prosody reloads its config (e.g. on SIGHUP). Configuration ------------- Add "reload\_modules" to modules\_enabled. Then the list of modules to reload using the 'reload\_modules' option in your config like so: reload_modules = { "groups", "tls" } This would reload mod\_groups and mod\_tls whenever the config is reloaded. Note that on many systems this will be at least daily, due to logrotate. Compatibility ------------- ----- ------- 0.9 Works ----- ------- prosody-modules-c53cc1ae4788/mod_reload_modules/mod_reload_modules.lua0000644000175500017550000000270513163400720025772 0ustar debacledebaclelocal array, it, set = require "util.array", require "util.iterators", require "util.set"; local mm = require "core.modulemanager"; function reload_all() local modules = module:get_option_set("reload_modules", {}); if not modules then module:log("warn", "No modules listed in the config to reload - set reload_modules to a list"); return; end local configured_modules = module:get_option_inherited_set("modules_enabled", {}); local loaded_modules = set.new(array.collect(it.keys(prosody.hosts[module.host].modules))); local need_to_load = set.intersection(configured_modules - loaded_modules, modules); local need_to_unload = set.intersection(loaded_modules - configured_modules, modules); for module_name in need_to_load do module:log("debug", "Loading %s", module_name); mm.load(module.host, module_name); end for module_name in need_to_unload do module:log("debug", "Unloading %s", module_name); mm.unload(module.host, module_name); end modules:exclude(need_to_load+need_to_unload) for module_name in set.intersection(modules,configured_modules) do module:log("debug", "Reloading %s", module_name); mm.reload(module.host, module_name); end end if module.hook_global then module:hook_global("config-reloaded", reload_all); else -- COMPAT w/pre-0.9 function module.load() prosody.events.add_handler("config-reloaded", reload_all); end function module.unload() prosody.events.remove_handler("config-reloaded", reload_all); end end prosody-modules-c53cc1ae4788/mod_service_directories/0002755000175500017550000000000013164216767022507 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_service_directories/mod_service_directories.lua0000644000175500017550000001346713163400720030075 0ustar debacledebacle-- Prosody IM -- Copyright (C) 2011 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- An implementation of [XEP-0309: Service Directories] -- Imports and defines local st = require "util.stanza"; local jid_split = require "util.jid".split; 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; local subscription_from = {}; local subscription_to = {}; local contact_features = {}; local contact_vcards = {}; -- Advertise in disco module:add_identity("server", "directory", module:get_option_string("name", "Prosody")); module:add_feature("urn:xmpp:server-presence"); -- Handle subscriptions module:hook("presence/host", function(event) -- inbound presence to the host local origin, stanza = event.origin, event.stanza; local node, host, resource = jid_split(stanza.attr.from); if stanza.attr.from ~= host then return; end -- not from a host local t = stanza.attr.type; if t == "probe" then module:send(st.presence({ from = module.host, to = host, id = stanza.attr.id })); elseif t == "subscribe" then subscription_from[host] = true; module:send(st.presence({ from = module.host, to = host, id = stanza.attr.id, type = "subscribed" })); module:send(st.presence({ from = module.host, to = host, id = stanza.attr.id })); add_contact(host); elseif t == "subscribed" then subscription_to[host] = true; query_host(host); elseif t == "unsubscribe" then subscription_from[host] = nil; module:send(st.presence({ from = module.host, to = host, id = stanza.attr.id, type = "unsubscribed" })); remove_contact(host); elseif t == "unsubscribed" then subscription_to[host] = nil; remove_contact(host); end return true; end, 10); -- priority over mod_presence function remove_contact(host, id) contact_features[host] = nil; contact_vcards[host] = nil; if subscription_to[host] then subscription_to[host] = nil; module:send(st.presence({ from = module.host, to = host, id = id, type = "unsubscribe" })); end if subscription_from[host] then subscription_from[host] = nil; module:send(st.presence({ from = module.host, to = host, id = id, type = "unsubscribed" })); end end function add_contact(host, id) if not subscription_to[host] then module:send(st.presence({ from = module.host, to = host, id = id, type = "subscribe" })); end end -- Admin ad-hoc command to subscribe local function add_contact_handler(self, data, state) local layout = dataforms_new{ title = "Adding a Server Buddy"; instructions = "Fill out this form to add a \"server buddy\"."; { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "peerjid", type = "jid-single", required = true, label = "The server to add" }; }; if not state then return { status = "executing", form = layout }, "executing"; elseif data.action == "canceled" then return { status = "canceled" }; else 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" } }; end add_contact(peerjid); return { status = "completed" }; end end local add_contact_command = adhoc_new("Adding a Server Buddy", "http://jabber.org/protocol/admin#server-buddy", add_contact_handler, "admin"); module:add_item("adhoc", add_contact_command); -- Disco query remote host function query_host(host) local stanza = st.iq({ from = module.host, to = host, type = "get", id = "mod_service_directories:disco" }) :query("http://jabber.org/protocol/disco#info"); module:send(stanza); end -- Handle disco query result module:hook("iq-result/bare/mod_service_directories:disco", function(event) local origin, stanza = event.origin, event.stanza; if not subscription_to[stanza.attr.from] then return; end -- not from a contact local host = stanza.attr.from; local query = stanza:get_child("query", "http://jabber.org/protocol/disco#info") if not query then return; end -- extract disco features local features = {}; for _,tag in ipairs(query.tags) do if tag.name == "feature" and tag.attr.var then features[tag.attr.var] = true; end end contact_features[host] = features; if features["urn:ietf:params:xml:ns:vcard-4.0"] then local stanza = st.iq({ from = module.host, to = host, type = "get", id = "mod_service_directories:vcard" }) :tag("vcard", { xmlns = "urn:ietf:params:xml:ns:vcard-4.0" }); module:send(stanza); end return true; end); -- Handle vcard result module:hook("iq-result/bare/mod_service_directories:vcard", function(event) local origin, stanza = event.origin, event.stanza; if not subscription_to[stanza.attr.from] then return; end -- not from a contact local host = stanza.attr.from; local vcard = stanza:get_child("vcard", "urn:ietf:params:xml:ns:vcard-4.0"); if not vcard then return; end contact_vcards[host] = st.clone(vcard); return true; end); -- PubSub -- TODO the following should be replaced by mod_pubsub module:hook("iq-get/host/http://jabber.org/protocol/pubsub:pubsub", function(event) local origin, stanza = event.origin, event.stanza; local payload = stanza.tags[1]; local items = payload:get_child("items", "http://jabber.org/protocol/pubsub"); if items and items.attr.node == "urn:xmpp:contacts" then local reply = st.reply(stanza) :tag("pubsub", { xmlns = "http://jabber.org/protocol/pubsub" }) :tag("items", { node = "urn:xmpp:contacts" }); for host, vcard in pairs(contact_vcards) do reply:tag("item", { id = host }) :add_child(vcard) :up(); end origin.send(reply); return true; end end); prosody-modules-c53cc1ae4788/mod_http_logging/0002755000175500017550000000000013164216767021140 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_http_logging/mod_http_logging.lua0000644000175500017550000000266013163400720025150 0ustar debacledebacle-- mod_http_logging -- -- Copyright (C) 2015 Kim Alvefur -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- Produces HTTP logs in the style of Apache -- -- TODO -- * Configurable format? module:set_global(); local server = require "net.http.server"; local send_response = server.send_response; local function log_and_send_response(response, body) if not response.finished then body = body or response.body; local len = body and #body or "-"; local request = response.request; local ip = request.conn:ip(); local req = string.format("%s %s HTTP/%s", request.method, request.path, request.httpversion); local date = os.date("%d/%m/%Y:%H:%M:%S %z"); module:log("info", "%s - - [%s] \"%s\" %d %s", ip, date, req, response.status_code, tostring(len)); end return send_response(response, body); end if module.wrap_object_event then -- Use object event wrapping, allows clean unloading of the module module:wrap_object_event(server._events, false, function (handlers, event_name, event_data) if event_data.response then event_data.response.send = log_and_send_response; end return handlers(event_name, event_data); end); else -- Fall back to monkeypatching, unlikely to behave nicely in the -- presence of other modules also doing this server.send_response = log_and_send_response; function module.unload() server.send_response = send_response; end end prosody-modules-c53cc1ae4788/mod_auth_internal_yubikey/0002755000175500017550000000000013164216767023051 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_auth_internal_yubikey/mod_auth_internal_yubikey.lua0000644000175500017550000001717513163400720031001 0ustar debacledebacle-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local datamanager = require "util.datamanager"; local storagemanager = require "core.storagemanager"; local log = require "util.logger".init("auth_internal_yubikey"); local type = type; local error = error; local ipairs = ipairs; local hashes = require "util.hashes"; local jid = require "util.jid"; local jid_bare = require "util.jid".bare; local config = require "core.configmanager"; local usermanager = require "core.usermanager"; local new_sasl = require "util.sasl".new; local hosts = hosts; local prosody = _G.prosody; local yubikey = require "yubikey".new_authenticator({ prefix_length = module:get_option_number("yubikey_prefix_length", 0); check_credentials = function (ret, state, data) local account = data.account; local yubikey_hash = hashes.sha1(ret.public_id..ret.private_id..(ret.password or ""), true); if yubikey_hash == account.yubikey_hash then return true; end return false, "invalid-otp"; end; store_device_info = function (state, data) local new_account = {}; for k, v in pairs(data.account) do new_account[k] = v; end new_account.yubikey_state = state; datamanager.store(data.username, data.host, "accounts", new_account); end; }); local global_yubikey_key = module:get_option_string("yubikey_key"); local host = module.host; local provider = {}; log("debug", "initializing default authentication provider for host '%s'", host); function provider.test_password(username, password) log("debug", "test password '%s' for user %s at host %s", password, username, module.host); local account_info = datamanager.load(username, host, "accounts") or {}; local yubikey_key = account_info.yubikey_key or global_yubikey_key; if account_info.yubikey_key then log("debug", "Authenticating Yubikey OTP for %s", username); local authed, err = yubikey:authenticate(password, account_info.yubikey_key, account_info.yubikey_state or {}, { account = account_info, username = username, host = host }); if not authed then log("debug", "Failed to authenticate %s via OTP: %s", username, err); return authed, err; end return authed; elseif account_info.password and password == account_info.password then -- No yubikey configured for this user, treat as normal password log("debug", "No yubikey configured for %s, successful login using password auth", username); return true; else return nil, "Auth failed. Invalid username or password."; end end function provider.get_password(username) log("debug", "get_password for username '%s' at host '%s'", username, module.host); return (datamanager.load(username, host, "accounts") or {}).password; end function provider.set_password(username, password) local account = datamanager.load(username, host, "accounts"); if account then account.password = password; return datamanager.store(username, host, "accounts", account); end return nil, "Account not available."; end function provider.user_exists(username) local account = datamanager.load(username, host, "accounts"); if not account then log("debug", "account not found for username '%s' at host '%s'", username, module.host); return nil, "Auth failed. Invalid username"; end return true; end function provider.create_user(username, password) return datamanager.store(username, host, "accounts", {password = password}); end function provider.delete_user(username) return datamanager.store(username, host, "accounts", nil); end function provider.get_sasl_handler() local realm = module:get_option("sasl_realm") or module.host; local getpass_authentication_profile = { plain_test = function(sasl, username, password, realm) return usermanager.test_password(username, realm, password), true; end }; return new_sasl(realm, getpass_authentication_profile); end module:provides("auth", provider); function module.command(arg) local command = arg[1]; table.remove(arg, 1); if command == "associate" then local user_jid = arg[1]; if not user_jid or user_jid == "help" then prosodyctl.show_usage([[mod_auth_internal_yubikey associate JID]], [[Set the Yubikey details for a user]]); return 1; end local username, host = jid.prepped_split(user_jid); if not username or not host then print("Invalid JID: "..user_jid); return 1; end local password, public_id, private_id, key; for i=2,#arg do local k, v = arg[i]:match("^%-%-(%w+)=(.*)$"); if not k then k, v = arg[i]:match("^%-(%w)(.*)$"); end if k == "password" then password = v; elseif k == "fixed" then public_id = v; elseif k == "uid" then private_id = v; elseif k == "key" or k == "a" then key = v; end end if not password then print(":: Password ::"); print("This is an optional password that should be always"); print("entered during login *before* the yubikey password."); print("If the yubikey is lost/stolen, unless the attacker"); print("knows this prefix, they cannot access the account."); print(""); password = prosodyctl.read_password(); if not password then print("Cancelled."); return 1; end end if not public_id then print(":: Public Yubikey ID ::"); print("This is a fixed string of characters between 0 and 16"); print("bytes long that the Yubikey prefixes to every token."); print("The ID should be entered in modhex encoding, meaning "); print("a string up to 32 characters. This *must* match"); print("exactly the fixed string programmed into the yubikey."); print(""); io.write("Enter fixed id (modhex): "); while true do public_id = io.read("*l"); if #public_id > 32 then print("The fixed id must be 32 characters or less. Please try again."); elseif public_id:match("[^cbdefghijklnrtuv]") then print("The fixed id contains invalid characters. It must be entered in modhex encoding. Please try again."); else break; end end end if not private_id then print(":: Private Yubikey ID ::"); print("This is a fixed secret UID programmed into the yubikey"); print("during configuration. It must be entered in hex (not modhex)"); print("encoding. It is always 6 bytes long, which is 12 characters"); print("in hex encoding."); print(""); while true do io.write("Enter private UID (hex): "); private_id = io.read("*l"); if #private_id ~= 12 then print("The id length must be 12 characters in hex encoding. Please try again."); elseif private_id:match("%X") then print("The key contains invalid characters - it must be in hex encoding (not modhex). Please try again."); else break; end end end if not key then print(":: AES Encryption Key ::"); print("This is the secret key that the Yubikey uses to encrypt the"); print("generated tokens. It is 32 characters in hex encoding."); print(""); while true do io.write("Enter AES key (hex): "); key = io.read("*l"); if #key ~= 32 then print("The key length must be 32 characters in hex encoding. Please try again."); elseif key:match("%X") then print("The key contains invalid characters - it must be in hex encoding (not modhex). Please try again."); else break; end end end local hash = hashes.sha1(public_id..private_id..password, true); local account = { yubikey_hash = hash; yubikey_key = key; }; storagemanager.initialize_host(host); local ok, err = datamanager.store(username, host, "accounts", account); if not ok then print("Error saving configuration:"); print("", err); return 1; end print("Saved."); return 0; end end prosody-modules-c53cc1ae4788/mod_auth_internal_yubikey/README.markdown0000644000175500017550000001003313163400720025524 0ustar debacledebacle--- labels: - 'Stage-Beta' - 'Type-Auth' summary: 'Two-factor authentication using Yubikeys' ... Introduction ============ A [YubiKey](http://www.yubico.com/yubikey) is a small USB one-time-password (OTP) generator. The idea behind one-time-passwords is that they can, well, only be used once. After authenticating with an OTP the only way to log in again is to calculate another one and use that. The only (practical) way to generate this is by inserting the (correct) Yubikey and pressing its button. Acting as a USB keyboard it then "types" the OTP into the password prompt of your XMPP client. Details ======= This self-contained module handles all the authentication of Yubikeys, it does not for example depend on the Yubico authentication service, or on any external system service such as PAM. When this module is enabled, only PLAIN authentication is enabled on the server (because Prosody needs to receive the full password from the client to decode it, not a hash), so connection encryption will automatically be enforced by Prosody. Even if the password is intercepted it is of little use to the attacker as it expires as soon as it is used. Additionally the data stored in Prosody's DB is not enough to authenticate as the user if stolen by the attacker. When this module is in use each user can either use normal password authentication, or instead have their account associated with a Yubikey - at which point only the key will work. Installation ============ Requires bitlib for Lua, and yubikey-lua from http://code.matthewwild.co.uk/yubikey-lua . When properly installed, the command `lua -lbit -lyubikey` should give you a Lua prompt with no errors. Configuration ============= Associating keys ---------------- Each Yubikey is configured with several pieces of information that Prosody needs to know. This information is shown in the Yubikey personalization tool (the *yubikey-personalization* package in Debian/Ubuntu). To associate a Yubikey with a user, run the following prosodyctl command: prosodyctl mod_auth_internal_yubikey associate user@example.com This will run you through a series of questions about the information Prosody requires about the key configuration. **NOTE:** All keys used with the server (rather, with a given host) must all have a "public ID" (uid) of the same length. This length must be set in the Prosody config with the 'yubikey\_prefix\_length' option. Instead of entering the information interactively it is also possible to specify each option on the command-line (useful for automation) via --option="value". The valid options are: password The user's password (may be blank) ---------- -------------------------------------------------------------------------------------------- fixed The public ID that the Yubikey prefixes to the OTP uid The private ID that the Yubikey encrypts in the OTP key The AES key that the Yubikey uses (may be blank if a global shared key is used, see below) If a password is configured for the user (recommended) they must enter this into the password box immediately before the OTP. This password doesn't have to be incredibly long or secure, but it prevents the Yubikey being used for authentication if it is stolen and the password isn't known. Configuring Prosody ------------------- To use this module for authentication, set in the config: authentication = "internal_yubikey" Module-specific options: yubikey\_prefix\_length (**REQUIRED**) The length of the public ID prefixed to the OTPs ------------------------- ------------------------------------------------------------------------------------------------------------------- yubikey\_global\_key If all Yubikeys use the same AES key, you can specify it here. Pass --key="" to prosodyctl when associating keys. If switching from a plaintext storage auth module then users without Yubikeys associated with their account can continue to use their existing passwords as normal, otherwise password resets are required. Compatibility ============= ----- ------- 0.8 Works ----- ------- prosody-modules-c53cc1ae4788/mod_block_outgoing/0002755000175500017550000000000013164216767021460 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_block_outgoing/mod_block_outgoing.lua0000644000175500017550000000176713163400720026017 0ustar debacledebacle-- Module to block all outgoing stanzas from a list of users local jid_bare = require "util.jid".bare; local is_admin = require "core.usermanager".is_admin; local set = require "util.set"; local block_users = module:get_option_set("block_outgoing_users", {}); local block_all = block_users:empty(); local stanza_types = module:get_option_set("block_outgoing_stanzas", { "message" }); local jid_types = set.new{ "host", "bare", "full" }; local function block_stanza(event) local stanza = event.stanza; local from_jid = jid_bare(stanza.attr.from); if stanza.attr.to == nil or stanza.attr.to == module.host or is_admin(from_jid, module.host) then return; end if block_all or block_users:contains(from_jid) then module:log("debug", "Blocked outgoing %s stanza from %s", stanza.name, stanza.attr.from); return true; end end function module.load() for stanza_type in stanza_types do for jid_type in jid_types do module:hook("pre-"..stanza_type.."/"..jid_type, block_stanza, 10000); end end end prosody-modules-c53cc1ae4788/mod_block_outgoing/README.markdown0000644000175500017550000000114213163400720024134 0ustar debacledebacle--- summary: 'Block outgoing stanzas from users' ... Introduction ============ This module blocks all outgoing stanzas from a list of users. Using ===== Add mod_block_outgoing to the enabled modules in your config file: ``` {.lua} modules_enabled = { -- ... "block_outgoing", -- ... } ``` Either in a section for a certain host or the global section define which users and what stanzas to block: ``` {.lua} block_outgoing_users = { "romeo@example.com", "juliet@example.com" } block_outgoing_stanzas = { "message", "iq", "presence" } ``` block_outgoing_stanzas defaults to "message" if not specified. prosody-modules-c53cc1ae4788/mod_log_mark/0002755000175500017550000000000013164216767020246 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_log_mark/mod_log_mark.lua0000644000175500017550000000023713163400720023362 0ustar debacledebaclemodule:set_global(); local log = _G.log; module:add_timer(60-os.date("%S"), function (now) log("info", "-- MARK --"); return 90 - ((now + 30) % 60); end); prosody-modules-c53cc1ae4788/mod_log_mark/README.markdown0000644000175500017550000000033213163400720022722 0ustar debacledebacle--- summary: Log a message once per minute ... This module sends `-- MARK --` to the log once per minute. This may be useful to give a sense of how busy the server is or see that logging and timers are still working. prosody-modules-c53cc1ae4788/.hgtags0000644000175500017550000000016613163400720017052 0ustar debacledebacle2c07bcf56a36d6e74dc0f5422e89bd61f4d31239 0.8-diverge 1656d4fd71d07aa3a52da89d4daf7723a555e7dd last-google-code-commit prosody-modules-c53cc1ae4788/mod_http_altconnect/0002755000175500017550000000000013164216767021644 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_http_altconnect/mod_http_altconnect.lua0000644000175500017550000000344513163400720026362 0ustar debacledebacle-- mod_http_altconnect -- XEP-0156: Discovering Alternative XMPP Connection Methods module:depends"http"; local json = require"util.json"; local st = require"util.stanza"; local array = require"util.array"; local host_modules = hosts[module.host].modules; local function get_supported() local uris = array(); if host_modules["bosh"] then uris:push({ rel = "urn:xmpp:alt-connections:xbosh", href = module:http_url("bosh", "/http-bind") }); end if host_modules["websocket"] then uris:push({ rel = "urn:xmpp:alt-connections:websocket", href = module:http_url("websocket", "xmpp-websocket"):gsub("^http", "ws") }); end return uris; end local function GET_xml(event) local request, response = event.request, event.response; local xrd = st.stanza("XRD", { xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0' }); local uris = get_supported(); for i, method in ipairs(uris) do xrd:tag("Link", method):up(); end response.headers.content_type = "application/xrd+xml" response.headers.access_control_allow_origin = "*"; return '' .. tostring(xrd); end local function GET_json(event) local request, response = event.request, event.response; local jrd = { links = get_supported() }; response.headers.content_type = "application/json" response.headers.access_control_allow_origin = "*"; return json.encode(jrd); end; local function GET_either(event) local accept_type = event.request.headers.accept or ""; if ( accept_type:find("xml") or #accept_type ) < ( accept_type:find("json") or #accept_type+1 ) then return GET_xml(event); else return GET_json(event); end end; module:provides("http", { default_path = "/.well-known"; route = { ["GET /host-meta"] = GET_either; -- ["GET /host-meta.xml"] = GET_xml; -- Hmmm ["GET /host-meta.json"] = GET_json; }; }); prosody-modules-c53cc1ae4788/mod_private_adhoc/0002755000175500017550000000000013164216767021263 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_private_adhoc/mod_private_adhoc.lua0000755000175500017550000000275413163400720025425 0ustar debacledebacle-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- Module by Thomas Raschbacher 2014 -- lordvan@lordvan.com module:depends"adhoc"; local dataforms_new = require "util.dataforms".new; local st = require "util.stanza"; local jid_split = require "util.jid".split; local private_storage = module:open_store("private"); local private_adhoc_result_layout = dataforms_new{ { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; { name = "privatexmldata", type = "text-multi", label = "Private XML data" }; }; function private_adhoc_command_handler (self, data, state) local username, hostname = jid_split(data.from); local data, err = private_storage:get(username); local dataString = ""; if not data then dataString = "No data found."; if err then dataString = dataString..err end; else for key,value in pairs(data) do dataString = dataString..tostring(st.deserialize(value)):gsub("><",">\n<") dataString = dataString.."\n\n"; end end return { status = "completed", result= { layout = private_adhoc_result_layout, values = {privatexmldata=dataString.."\n"}} }; end local adhoc_new = module:require "adhoc".new; local descriptor = adhoc_new("Query private data", "private_adhoc", private_adhoc_command_handler, "local_user"); module:add_item ("adhoc", descriptor); prosody-modules-c53cc1ae4788/mod_private_adhoc/README.markdown0000644000175500017550000000053413163400720023743 0ustar debacledebacle--- summary: Retrieve private XML data via adhoc command ... Introduction ------------ This is a very simple module which implements an adhoc commant toretrieves the users private XML data. Details ------- Simple module which adds the adhoc command "Query private data" and will return the users private XML data (typically muc bookmarks,.. ). prosody-modules-c53cc1ae4788/mod_streamstats/0002755000175500017550000000000013164216767021025 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_streamstats/mod_streamstats.lua0000644000175500017550000000445513163400720024726 0ustar debacledebaclemodule:set_global(); local stats = module:shared"stats"; local iter = require "util.iterators"; local count, keys = iter.count, iter.keys; stats.stats = stats.stats or {}; stats.conns = stats.conns or {}; setmetatable(stats, { __index = { broadcast = function (self, stat) local value = self.stats[stat]; for conn in pairs(self.conns) do conn:write(stat..":"..value.."\n"); end end; adjust = function (self, stat, delta) if delta == 0 then return; end self.stats[stat] = (self.stats[stat] or 0) + delta; self:broadcast(stat); end; set = function (self, stat, value) if value == self.stats[stat] then return; end self.stats[stat] = value; self:broadcast(stat); end; add_conn = function (self, conn) self.conns[conn] = true; for stat, value in pairs(self.stats) do conn:write(stat..":"..value.."\n"); end end; remove_conn = function (self, conn) self.conns[conn] = nil; end; }; }); local network = {}; function network.onconnect(conn) stats:add_conn(conn); end function network.onincoming(conn, data) end function network.ondisconnect(conn, reason) stats:remove_conn(conn); end module:add_timer(1, function () stats:set("s2s-in", count(keys(prosody.incoming_s2s))); return math.random(10, 20); end); module:add_timer(3, function () local s2sout_count = 0; for _, host in pairs(prosody.hosts) do s2sout_count = s2sout_count + count(keys(host.s2sout)); end stats:set("s2s-out", s2sout_count); return math.random(10, 20); end); function module.add_host(module) module:hook("resource-bind", function () stats:adjust("c2s", 1); end); module:hook("resource-unbind", function () stats:adjust("c2s", -1); end); local c2s_count = 0; for username, user in pairs(hosts[module.host].sessions or {}) do for resource, session in pairs(user.sessions or {}) do c2s_count = c2s_count + 1; end end stats:set("c2s", c2s_count); module:hook("s2sin-established", function (event) stats:adjust("s2s-in", 1); end); module:hook("s2sin-destroyed", function (event) stats:adjust("s2s-in", -1); end); module:hook("s2sout-established", function (event) stats:adjust("s2s-out", 1); end); module:hook("s2sout-destroyed", function (event) stats:adjust("s2s-out", -1); end); end module:provides("net", { default_port = 5444; listener = network; }); prosody-modules-c53cc1ae4788/mod_lib_ldap/0002755000175500017550000000000013164216767020221 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_lib_ldap/README.md0000644000175500017550000001511513163400720021460 0ustar debacledebacle# LDAP plugin suite for Prosody The LDAP plugin suite includes an authentication plugin (mod\_auth\_ldap2) and storage plugin (mod\_storage\_ldap) to query against an LDAP server. It also provides a plugin library (mod\_lib\_ldap) for accessing an LDAP server to make writing other LDAP-based plugins easier in the future. # LDAP Authentication **NOTE**: LDAP authentication currently only works with plaintext auth (as opposed to DIGEST-MD5 or SCRAM) If this isn't ok with you, don't use it! (Or better yet, fix it =) ) With that note in mind, if you need to allow (XMPP) clients to connect to your server without TLS and want to use this module, you need to set 'allow\_unencrypted\_plain\_auth' to true in your configuration. You probably don't actually want to do this, though. To enable LDAP authentication, set 'authentication' to 'ldap2' in your configuration file. See also http://prosody.im/doc/authentication. # LDAP Storage LDAP storage is currently read-only, and it only supports rosters and vCards. To enable LDAP storage, set 'storage' to 'ldap' in your configuration file. See also http://prosody.im/doc/storage. # LDAP Configuration All of the LDAP-specific configuration for the plugin set goes into an 'ldap' section in the configuration. You must set the 'hostname' field in the 'ldap' section to your LDAP server's location (a custom port is also accepted, so I guess it's not strictly a hostname). The 'bind\_dn' and 'bind\_password' are optional if you want to bind as a specific DN. There should be an example configuration included with this README, so feel free to consult that. ## The user section The user section must contain the following keys: * basedn - The base DN against which to base your LDAP queries for users. * filter - An LDAP filter expression that matches users. * usernamefield - The name of the attribute in an LDAP entry that contains the username. * namefield - The name of the attribute in an LDAP entry that contains the user's real name. ## The groups section The LDAP plugin suite has support for grouping (ala mod\_groups), which can be enabled via the groups section in the ldap section of the configuration file. Currently, you must have at least one group. The groups section must contain the following keys: * basedn - The base DN against which to base your LDAP queries for groups. * memberfield - The name of the attribute in an LDAP entry that contains a list of a group's members. The contents of this field must match usernamefield in the user section. * namefield - The name of the attribute in an LDAP entry that contains the group's name. The groups section must contain at least one entry in its array section. Each entry must be a table, with the following keys: * name - The name of the group that will be presented in the roster. * $namefield (whatever namefield is set to is the name) - An attribute pair to match this group against. * admin (optional) - whether or not this group's members are admins. ## The vcard\_format section The vcard\_format section is used to generate a vCard given an LDAP entry. See http://xmpp.org/extensions/xep-0054.html for more information. The JABBERID field is automatically populated. The key/value pairs in this table fall into three categories: ### Simple pairs Some values in the vcard\_format table are simple key-value pairs, where the key corresponds to a vCard entry, and the value corresponds to the attribute name in the LDAP entry for the user. The fields that be configured this way are: * displayname - corresponds to FN * nickname - corresponds to NICKNAME * birthday - corresponds to BDAY * mailer - corresponds to MAILER * timezone - corresponds to TZ * title - corresponds to TITLE * role - corresponds to ROLE * note - corresponds to NOTE * rev - corresponds to REV * sortstring - corresponds to SORT-STRING * uid - corresponds to UID * url - corresponds to URL * description - corresponds to DESC ### Single-level fields These pairs have a table as their values, and the table itself has a series of key value pairs that are translated similarly to simple pairs. The fields that are configured this way are: * name - corresponds to N * family - corresponds to FAMILY * given - corresponds toGIVEN * middle - corresponds toMIDDLE * prefix - corresponds toPREFIX * suffix - corresponds toSUFFIX * photo - corresponds to PHOTO * type - corresponds to TYPE * binval - corresponds to BINVAL * extval - corresponds to EXTVAL * geo - corresponds to GEO * lat - corresponds to LAT * lon - corresponds to LON * logo - corresponds to LOGO * type - corresponds to TYPE * binval - corresponds to BINVAL * extval - corresponds to EXTVAL * org - corresponds to ORG * orgname - corresponds to ORGNAME * orgunit - corresponds to ORGUNIT * sound - corresponds to SOUND * phonetic - corresponds to PHONETIC * binval - corresponds to BINVAL * extval - corresponds to EXTVAL * key - corresponds to KEY * type - corresponds to TYPE * cred - corresponds to CRED ### Multi-level fields These pairs have a table as their values, and each table itself has tables as its values. The nested tables have the same key-value pairs you're used to, the only difference being that values may have a boolean as their type, which converts them into an empty XML tag. I recommend looking at the example configuration for clarification. * address - ADR * telephone - TEL * email - EMAIL For example, to get something like this in your vCard: 555-555-5555 Your configuration for `telephone` will probably look something like this: telephone = { work = { voice = true, number = 'telephoneNumber', }, } ### Unsupported vCard fields * LABEL * AGENT * CATEGORIES * PRODID * CLASS ### Example Configuration You can find an example configuration in the dev directory underneath the directory that this file is located in. # Missing Features This set of plugins is missing a few features, some of which are really just ideas: * Implement non-plaintext authentication. * Use proper LDAP binding (LuaLDAP must be patched with http://prosody.im/patches/lualdap.patch, though) * Non-hardcoded LDAP groups (derive groups from LDAP queries) * LDAP-based MUCs (like a private MUC per group, or something) * This suite of plugins was developed with a POSIX-style setup in mind; YMMV. Patches to work with other setups are welcome! * Add ability for users to change their vCard/passwords/etc from within Prosody prosody-modules-c53cc1ae4788/mod_lib_ldap/README.markdown0000644000175500017550000000420713163400720022702 0ustar debacledebacle--- labels: summary: Library module for LDAP ... Introduction ============ This module is used by other modules to access an LDAP server. It's pretty useless on its own; you should use it if you want to write your own LDAP-related module, or if you want to use one of mine ([mod\_auth\_ldap2](mod_auth_ldap2.html), [mod\_storage\_ldap](mod_storage_ldap.html)). Installation ============ Simply copy ldap.lib.lua into your Prosody installation's plugins directory. Configuration ============= Configuration for this module (and all modules that use it) goes into the *ldap* section of your prosody.cfg.lua file. Each plugin that uses it may add their own sections; this plugin relies on the following keys: - hostname - Where your LDAP server is located - bind\_dn - The DN to perform queries as - bind\_password - The password to use for queries - use\_tls - Whether or not TLS should be used to connect to the LDAP server - user.usernamefield - The LDAP field that contains a user's username - user.basedn - The base DN for user records API === ldap.getconnection() -------------------- Returns an LDAP connection object corresponding to the configuration in prosody.cfg.lua. The connection object is a [LuaLDAP](http://www.keplerproject.org/lualdap/) connection. ldap.getparams() ---------------- Returns the LDAP configuration provided in prosody.cfg.lua. Use this if you want to stick some configuration information for your module into the LDAP section in the configuration file. ldap.bind(username, password) ----------------------------- Verifies that *username* and *password* bind ok. **NOTE**: This does not bind the current LDAP connection to the given username! ldap.singlematch(query) ----------------------- Used to fetch a single LDAP record given an LDAP query. A convenience function. ldap.filter.combine\_and(...) ----------------------------- Takes a list of LDAP filter expressions and returns a filter expression that results in the intersection of each given expression (it ANDs them together). More Information ================ For more information, please consult the README.html file under prosody-modules/mod\_lib\_ldap. prosody-modules-c53cc1ae4788/mod_lib_ldap/ldap.lib.lua0000644000175500017550000001604013163400720022367 0ustar debacledebacle-- 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. -- local ldap; local connection; local params = module:get_option("ldap"); local format = string.format; local tconcat = table.concat; local _M = {}; local config_params = { hostname = 'string', user = { basedn = 'string', namefield = 'string', filter = 'string', usernamefield = 'string', }, groups = { basedn = 'string', namefield = 'string', memberfield = 'string', _member = { name = 'string', admin = 'boolean?', }, }, admin = { _optional = true, basedn = 'string', namefield = 'string', filter = 'string', } } local function run_validation(params, config, prefix) prefix = prefix or ''; -- verify that every required member of config is present in params for k, v in pairs(config) do if type(k) == 'string' and k:sub(1, 1) ~= '_' then local is_optional; if type(v) == 'table' then is_optional = v._optional; else is_optional = v:sub(-1) == '?'; end if not is_optional and params[k] == nil then return nil, prefix .. k .. ' is required'; end end end for k, v in pairs(params) do local expected_type = config[k]; local ok, err = true; if type(k) == 'string' then -- verify that this key is present in config if k:sub(1, 1) == '_' or expected_type == nil then return nil, 'invalid parameter ' .. prefix .. k; end -- type validation if type(expected_type) == 'string' then if expected_type:sub(-1) == '?' then expected_type = expected_type:sub(1, -2); end if type(v) ~= expected_type then return nil, 'invalid type for parameter ' .. prefix .. k; end else -- it's a table (or had better be) if type(v) ~= 'table' then return nil, 'invalid type for parameter ' .. prefix .. k; end -- recurse into child ok, err = run_validation(v, expected_type, prefix .. k .. '.'); end else -- it's an integer (or had better be) if not config._member then return nil, 'invalid parameter ' .. prefix .. tostring(k); end ok, err = run_validation(v, config._member, prefix .. tostring(k) .. '.'); end if not ok then return ok, err; end end return true; end local function validate_config() if true then return true; -- XXX for now end -- this is almost too clever (I mean that in a bad -- maintainability sort of way) -- -- basically this allows a free pass for a key in group members -- equal to params.groups.namefield setmetatable(config_params.groups._member, { __index = function(_, k) if k == params.groups.namefield then return 'string'; end end }); local ok, err = run_validation(params, config_params); setmetatable(config_params.groups._member, nil); if ok then -- a little extra validation that doesn't fit into -- my recursive checker local group_namefield = params.groups.namefield; for i, group in ipairs(params.groups) do if not group[group_namefield] then return nil, format('groups.%d.%s is required', i, group_namefield); end end -- fill in params.admin if you can if not params.admin and params.groups then local admingroup; for _, groupconfig in ipairs(params.groups) do if groupconfig.admin then admingroup = groupconfig; break; end end if admingroup then params.admin = { basedn = params.groups.basedn, namefield = params.groups.memberfield, filter = group_namefield .. '=' .. admingroup[group_namefield], }; end end end return ok, err; end -- what to do if connection isn't available? local function connect() return ldap.open_simple(params.hostname, params.bind_dn, params.bind_password, params.use_tls); end -- this is abstracted so we can maintain persistent connections at a later time function _M.getconnection() return connect(); end function _M.getparams() return params; end -- XXX consider renaming this...it doesn't bind the current connection function _M.bind(username, password) 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 conn:close(); return true; end return conn, err; end function _M.singlematch(query) local ld = _M.getconnection(); query.sizelimit = 1; query.scope = 'subtree'; for dn, attribs in ld:search(query) do attribs.dn = dn; return attribs; end end _M.filter = {}; function _M.filter.combine_and(...) local parts = { '(&' }; local arg = { ... }; for _, filter in ipairs(arg) do if filter:sub(1, 1) ~= '(' and filter:sub(-1) ~= ')' then filter = '(' .. filter .. ')' end parts[#parts + 1] = filter; end parts[#parts + 1] = ')'; return tconcat(parts, ''); end do local ok, err; prosody.unlock_globals(); ok, ldap = pcall(require, 'lualdap'); prosody.lock_globals(); if not ok then module:log("error", "Failed to load the LuaLDAP library for accessing LDAP: %s", ldap); module:log("error", "More information on install LuaLDAP can be found at http://www.keplerproject.org/lualdap"); return; end if not params then module:log("error", "LDAP configuration required to use the LDAP storage module"); return; end ok, err = validate_config(); if not ok then module:log("error", "LDAP configuration is invalid: %s", tostring(err)); return; end end return _M; prosody-modules-c53cc1ae4788/mod_lib_ldap/dev/0002755000175500017550000000000013164216767020777 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_lib_ldap/dev/README.md0000644000175500017550000000106213163400720022232 0ustar debacledebacleDeveloper Utilities/Tests ========================= This directory exists for reasons of sanity checking. If you wish to run the tests, set up Prosody as you normally would, and install the LDAP modules as normal as well. Set up OpenLDAP using the configuration directory found in this directory (slapd.conf), and run the following command to import the test definitions into the LDAP server: ldapadd -x -w prosody -D 'cn=Manager,dc=example,dc=com' -f posix-users.ldif Then just run prove (you will need perl and AnyEvent::XMPP installed): prove t prosody-modules-c53cc1ae4788/mod_lib_ldap/dev/prosody-posix-ldap.cfg.lua0000644000175500017550000000465413163400720026003 0ustar debacledebacle-- Use Include 'prosody-posix-ldap.cfg.lua' from prosody.cfg.lua to include this file 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 bind_dn = 'cn=Manager,dc=example,dc=com', -- Bind DN for LDAP authentication (optional if anonymous bind is supported) 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)(!(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 = { basedn = 'ou=Groups,dc=example,dc=com', -- The base DN where group records can be found memberfield = 'memberUid', -- The field that contains user ID records for this group (each member must have a corresponding entry under the user basedn with the same value in usernamefield) namefield = 'cn', -- The field that contains the group's name (used for matching groups in LDAP to group definitions below) { name = 'everyone', -- The group name that will be seen in users' rosters cn = 'Everyone', -- This field's key *must* match ldap.groups.namefield! It's the name of the LDAP group this definition represents admin = false, -- (Optional) A boolean flag that indicates whether members of this group should be considered administrators. }, { name = 'admin', cn = 'Admin', admin = true, }, }, vcard_format = { displayname = 'cn', -- Consult the vCard configuration section in the README nickname = 'uid', photo = { type = 'image/jpeg', binval = 'jpegPhoto', }, telephone = { work = { voice = true, number = 'telephoneNumber', }, }, }, } prosody-modules-c53cc1ae4788/mod_lib_ldap/dev/posix-users.ldif0000644000175500017550000003130013163400720024112 0ustar debacledebacle# This is an LDIF file containing simple user definitions for a POSIX-style LDAP # setup. dn: dc=example,dc=com objectclass: dcObject objectclass: organization o: Example dc: example dn: cn=Manager,dc=example,dc=com objectclass: organizationalRole cn: Manager dn: ou=Groups,dc=example,dc=com ou: Groups objectclass: organizationalUnit dn: ou=Users,dc=example,dc=com 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 uid: one uidNumber: 1000 gidNumber: 1000 sn: Testerson cn: John Testerson userPassword: 12345 homeDirectory: /home/one telephoneNumber: 555-555-5555 dn: uid=two,ou=Users,dc=example,dc=com objectclass: posixAccount objectclass: person uid: two uidNumber: 1001 gidNumber: 1001 sn: Testerson cn: Jane Testerson userPassword: 23451 homeDirectory: /home/two dn: uid=three,ou=Users,dc=example,dc=com objectclass: posixAccount objectclass: person uid: three uidNumber: 1002 gidNumber: 1002 sn: Testerson cn: Jerry Testerson userPassword: 34512 homeDirectory: /home/three dn: uid=four,ou=Users,dc=example,dc=com objectclass: posixAccount objectclass: person uid: four uidNumber: 1003 gidNumber: 1003 sn: Testerson cn: Jack Testerson userPassword: 45123 homeDirectory: /home/four dn: uid=five,ou=Users,dc=example,dc=com objectclass: posixAccount objectclass: person objectclass: inetOrgPerson uid: five uidNumber: 1004 gidNumber: 1004 sn: Testerson cn: Jimmy Testerson userPassword: 51234 homeDirectory: /home/five jpegPhoto:: /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg SlBFRyB2NjIpLCBkZWZhdWx0IHF1YWxpdHkK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMP FB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEc ITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgA yADIAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMC BAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYn KCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeY mZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5 +v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwAB AgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpD REVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ip qrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMR AD8A9/opM0ZFADVGGan00feNLkUALSGkJqjfX5t4jsjkZyDtwuRmhK4m0ldl4sBnNZk+sRCJnt0a cqdoC/xH0FYM+t363cPlxyPCv+sDYBfPXgdMdqoXGtxRXxuZftMSxMypAkXBXuST3JreNB9TnliI 9GaNz4jvYpQk6wwCQ7UAcMwPoazrjUJpmO+Rm+prjtZ1Zb7UZLiOLyQcYA68dz71qWV99qtEkJ+b 7rc9xXq4ehFK9tTzsVOT1voaDszdTgVUlnVBhT/9eoZrgngVXLHOTXao2OBslLljk0hmx3qBn5zm omfBzV2EWPPOaPtJHeqZkqNpKl2GrmvDfsjA7sEd810+k+J5Y8JMfMT9RXn/AJ1WYropgZ/KuerS hOPvI1p1JQleLPRNY+w3Vut8kLEnKyFV5IIOO4zzXKzNaPfWipFIES1cbGTB3bsDjcf50/Sbu4Lh BcZjbho5BuUj3Bq5q1tBY6pBcvHDHC0GQFYIud3bj3r5jMsK4ax2Z9Ll+JU9JbopAafFDdS3GnzX HyNnaE4QlR/ER6VXt7vRv7RsSvh+4VjDIIpT5Gdvz5xyW9en+NaktxaxXl7GY4GWOE8NKmGAcZz+ 7JGPqfp3q1aotybCaOzQ25gf5kkyB970iH81rClBxhZmtWfNNs5CO+0C50qWaDwrax2zSoSjXVqv zYbB56d+vPNTeF7vR3+LDQQaI1rqAtMmYXQeML5a4AVRjpjoa2ltWOli1tbDaTLGEQTEcc458k4H b+vrDothJa/FqeWXSzGXgwtznIOEAPOB/kVqtzP0PRmTGMDtRVgrzmihQKUynHNd5Icxj3HNPMs+ PvJn6GubGsKTn5vqoFPXVmkU+XHKw/vZrosZXNp574A4MPtyf8KwrubXpXbbIIEB6hgc0v2qYtlZ GT6jNI1wmTueR26YC0AY19P4kTIFyZl9uKp2viPXLaXZPHcY9XjJB/H+tdJvXvGVA7soxUT3UfIw GTuRzincLFceILkENLaK/fONx/xq1Bd6XrDgyK8UvcDoaqta2dwN5X3yvH8qhezWIb4mYkddxzVR m1syJU4y3Q7U/DHmFpLaOOeMknHcVzot1tN8aRmPDYYe9dXY3rQOpEjbM4z2H19Ks6hpcGoxG4Ql ZeSdo4Jrvw+Ns7TOCvgrq8DiMjnims9Wb6zktC277uePes5n7Zr14zUldHkyg4uzHM1RO/FNZ6hd 6LgkPZuOKZuz1qIv6GmPJtGai5SROzBF5xUQmJbcTVQksxJp6mspSbNoxsblhdFHBzXpPhy7iuo1 jlCs6D5GPUeoryW3cqw5rsPDmoeTNGWbGDXJiaanCx0Yeo4TuenCJfSl2ACkilWWNXQgqRkEU814 57KGbRQU3KQe/pTgKXpTsBSe1PaWb/vs0VbYZHBxRWbhqVzHmAkQP80RUe3/ANc1ZE0bcAdPTrXJ y3csMO7du47Meas2V5NOuHLAf3UHT8a6HuZHR/aVj6I4Huxp8dzIWG1Ac92Un+lYgLZzu2+maR5f KUtvP1BJ/SpGa017Omf3iKPYVWlvJGXcjAv25xn8ax5b2Rk4dyPfiqiXJl3KSzc9xQBrpdXImXfG VJ7rkn9Kvm5u1HmbUOPUBTWFbyz7yUjI9z/nFXl81hmWUL79adguaUFzJOfMeONW+7kHitW0ultx jzVIzXM/aI48Rx5PqTxmrMVzICAISTjjIqWNG5qtsNQRDGAVBya4zULJ7WYpg4C7ua7XT5y4bd8u BkjFOurSG7wsiAkgjPtXXQxbp6PY5a+FjV1W55m8gx15qIyAitfVtBmt7l/KwyHOCTgmsBvMibaQ B9a9WFaM1oeVOhKm9USls85wfWoWcs3J/CkaYn5Tt/CmkjGc0OQlEeG6U4VEGz1pwepuVYsxnBFb ukzbZVrnkYE1qWMm2RaiexUdz2fR5UfTo9oAx2rQByK4vSdeg0rSJrq73m3iUM5RdxHOM4/Gkb4o aAIRJGl7IM4wsGD+pFeJWapzabPaoXnBNI7aiuFg+KGl3cgS207U5WJxhYV6/wDfVMuPijYWyuW0 jVTsJD/uV+X/AMerL2sO5ryS7HeHpRXnsPxa06e4WEaTqSlhwSqflw1FL20O4/Zy7HD3kkW3IDFu xqxpkflL5ag5bl3/AKCoY4lKEsm5ywwPStqyjjihO1gW6FsV1GBVnb7ODkbVPdjzUX2sCISFNyno SCc03Uf3mQd31JqH5EiQuC3y4VQM4AqRkU91GyPN0CAsfwrkNI8X+bemO9SONWOY3A+79f8AGuvn SNkK4Kq4wwxg815TrWlzaVqEkLfczujP95exFS9CkexWbh18wyB1PQ84NNuZ2Y8sUX6YOP6V5loH i670r9w586D/AJ5ueR9DXWR67aaiFaOYq7fwt29uKrmFY02vkib5eo7k1uabdSzWu/qn+ya5by4n ZSjl1IzxjI/AnrW1pep6daEwT+ZAkhw3moRzS0A6myuduGMmcetan2pPLyp5HWsfz9Kij3+ehUc/ J0rPl1qGRyLcEAcZ9aWgzUvb2zndYp1zk4HbBrD1XTbC8yY5EE+0YboSR1/rVfWZ3MKyWxLOBkqP vevSuIfUtUvZBJErRqpwWIOPSqhOcHoTKEZq0jfm0W4gTdsYr+eaqSW8sTYkRlPoRitK3N69hsiu HMmchQ3y/nmrQnuIrQw3WmPdSsBiRvlUe+Qa64Yt/aRyzwi+yzAKEDOOKYWPeuwvtIWbT4zHIm5V GUVcc1y1xavExBHIrphVVRXRy1KTpvUjSTkDNaFtKUYNjIrIKspq5a3LoRlQwFXzEJHoOgXVncp9 nlwUcbXikHDA12kfh3RzH8thAQw7rnIrzLSb20aRd0bK2a9R0W7We0VN2WUd/SvOxdKMveZ34SpJ e6i1aada2MPlW8CRxj+FRU0lvFNE0UsaPGwwVYZBqUUtciirWOu7KQ0uxAwLOADjjyx26UVcxRRy R7DuzwtnaGeNccAbj9auRzv5RzgZJ9gBk4/Tk1mPKXuBtzhWABHU4H8qbJcOURUPL/KMHoB71o3q TYsPMszEANs3cc4Le/0om87gYRF7AUQw+RgHc8h65Ocf4VXuDIXIjwQPvuc8n0FILELjzJSFQbMn 5jUep6JFqunPFIdkoGYZf7reh9qe27fkn7o9eBRFGbh8Gfao6DpQB5Re2N1pt41veQ7XU/xd/cUs d39nIWWHeo6EPjFet3+mW9/bGK7gWRAMKzfeH0NcpN4LsGfCSSIWOB3ApDKFjqAlgBgnaQrz8zfM n+NTW+tzRziGdg6d/MGaaPA93GwksbpN3YZIJqYaHqTlUv7JgFP+uQZx9aXNYLFu6vLm4AjtpvLA XkD0PSum0OWzttOiydz7AXdjncT1P51yt1BbwXfnW8pkCKAwPHT19ulV9N1EiQwO/J+6f6VomJnX avcxXK5QmIjkMpwa5kRzSOfnaZmPBc5P1q+XtyoDzDjt6fWqNzOiS7EQDA79896HYSJYZ7q1c7CN 44JAxg10OiiSZxNeyl1B4QM2M+/NYVnNHI2yVeQcIf61PdaiIMBFA7EhuKEJnSavrkVnbloSFKsA DniqNjqlnrxTYBBIRg56Zrib7UWv7wW287IzvdifSrNoklr/AKTbiQwxrlmA4+vvVRk4O8RSgpq0 jtr3Rfs6K8k0GG6YkGf1rPWxkjfKMMfnWI/iO3JYPKz44BLZ/Q1Ol+gAkRFA7OxGP0PWuiOLl1Rz ywkejOnsm8ra8hwckZI5rsNI1RoXVw3ToK8sbxRbxBWd0lK9doY/hnFL/wALCt7UZWDdjsHx/OuX EVJVHpsdeFp06S11Z9HWd5HeQCSM/UelEt/awTxwS3EUcshwiM4BY+w718yXfxd17yHi03ZZK4wX X5nx+PA/KqfgZNV8T+PtNMs81xKk6zySSMWwqkEk/l+tYpvqaSSvofWAOaKaDgc0VZB8+kkptQhS xwW79amswj3AAGI4F6+pqlK6IV9QuVH6ZNTaYCBMRzu5G7oPSgDQRi5LHOP4VHf/ABqOcs+2CMqF U5kY9B7UxpjHwGLO3ANNAVVK/wAAOWb+81AEMoEhIx8g/Wpbcr5rKseSo5Y9qgeZmJK+vFW7T5UY kdOhPOaAJJJPNiKkkAc8nFUlCBwOSQCfxp0gbfxlsnmoiqxSF87nzgegoEDMwBwcMeM57V1mi2uN OYzDeXX+LniuUWMzSqM/gK7K0lCWqIvQL1ob0BbnmWqTWwtWWW9El15oRYDCEKDPqOo6fpXPqjrq apxn0ro9Y0z7TqDlxlWZgG9MZxULaS8lxDcgFcKAxPHI60o7lPYDZFYWDHlgCD1/WqVwP3RZsgqN pz710Zt0MQUMpc8ZPWqNzZM5YAfORtxjn61skjJtoylkZZJVQHO/aW9h2/PNR3QxGH25P97OKvSR /Zn7Esx2pmq84ebAZVPHQcj8jSsNM59CVD/7TYwOprZ0yWc200EUzSOI2V4EQllzxn3GTWRqMIt3 RIyVcfM2O3cV33w8hvdRea6kmZhFGURgABuPTp7ZrOTsaRV2edy2s9nJme0uQc5BaMjNQyalNsKL Eyg9z1r2OfX7hEFldRRFo2I3OoLfnXO61bpcRtcxRQlVGWUxrx+lZ85v7B7nmi3d190yHB4IpojJ bnJ78GulD2Uhw1vbn8Mfyrb0t7RWAt9Otg/GCkW9vzOafOR7JnPaP4SvtV2OymC3P/LSQdR/sjvX vPw/0XTvC1qRbRBp5QPMuHHzN7ew9q5rTNP1C+kWWQrCo/vfMcV2axGygDuy+UoyWzwPrWNSU1qb Rpxsdsl3E4HPWivM73x1awSrbWkgllPBdeVj9z6n2opqqzN0UmefS/NMwJJMjgZPpWjEx+YKQoOB 9Ky3DNNBubG35mA+tW7WX5Xk7nGM9q3MC7jEuAD05J601pDMxSMDAGMjoKaSSCP4mPftU9qqojdB jJ69aAKwi2y/McenPSrDzZTanTpzVfmW5ZuNnNPzzgjIoAkciOIE8Hpk1RWTe0nykYHAqyXLtjby B09KpSkJcYB4xj6+poA0rUhpSw+g/CuogcLCqg8YrkdMlCuB69M9uK6KCTC8mkBkXUZFxMB8zBiR xUBjJQDHz9K0J1zdMxAwOee9NaKTYWXGW6KPSkMzrYEyncu714Iz+lVZJWW6YkYTGAevHrWlIhTM SHDYJJHaslweRwcZz/n8q1g7ohlHUSrbSvyqRkYHJqnA4jcg7WB59xVsQCRiwC9M4PU/Tmq3lFd5 YZB4GaUmNIyprb+0dWfaOGYEivYPDdgul+HlEMfBbc3Y81wOlaZvv0AyrdyB2rttUvvslhDbxOFP RgOcispa6GkEZVzpdqmoTT3E5mBclY1+UAe57/pVPUGW5i8hFCRf3VGBS7mkPOc1Yig3EZFXGma8 7Mq10KItnYPyrobHT4rfBCgfSpYIwo6VY3ha0UUibtmlBMIwADWvaXEc8bQyqGRhtZW6Ee9cqZ8d 6sWt6VkGDUT1Licj4g0t9F1mawGfs8vzxNj7yk8A+uDx+FFdrr2nrr2lYUf6XAC8LY6+q/j/ADxR XFJNOx2wlFx1POnkLFuxPJHoB/kVNbbsr37hfU1XRQ0cz+ox+tWYAY5h32qAPrXYeUXJGKkKpyw5 JPrUjSYQhM8jBNNjVjkngVKyfIiqOTzk9qAIkzHGTn+HOKhs5jcSFRnCHHXrVi5ysJUDkjFQ2irC hXHucUCLTrhCV496zpkwOOp+Zj6Cr7PlSWPbAAqpcoVxGpHzcsfYdqACxkVZox3GOK6eA8E/hXJQ kLdIe2a6SC4BizwKVwsWJHSOUO3A6Z9KbOrmPemefQZyKq3EoeJ1Hpx9antrhm0vzJHwzctyflAp bjKV0yhgG4OOhOCfrisu6mjUAZHTn2NVJ9Xgu7l5I33KRgE+nNZt1qUA3fMQfU962jZIhp3L1u/n B8FWQfoaEjCuVcHYxwCexpukMstxHJEcxkkYH61Jqbm3vZIj9wEEY9CKzkUjftzHpuny30mNiKW4 74HA/E1xlz4jup9QFycMufmU/wAQ/wAas67fTNbwWRmOxEDOB13GsBUwcEYPepgupTZ6HYPFdW6T xHcjjIP9K040AFcT4bvjbXf2R2/dTHK+zf8A167dG4re+g4kgOOlNeTHFIz45qpNNxxUORokLJIB 34p9vN845rMluAO4pkN4DJgGpuUdzp8hO1geRRVLQ590gB70Vm9y1scCNqJIh6AYqxAxkmPYAZqn McxrnrgA/hWjp8eQWxy1Vc5LF17hIYAChLnsO1IsmV3OcE9BTZQd+09yBRGDyzUwGtICcc/jURdR ggj5u9SPg9elV5YSlqX/ALg4NAExfAB9Dn9KZuJcufU4+mKYW8zbjpimTsVC46YOaAIXYrIg6Ekj NakUxCKB0AyaxFZpl3AHA4B9eK1YOAo7/wD16nYZeLZAIqysYn05om4DKcfyP+feoLW3dwDgn5jT 7xmsITK3CDr7UReoNHk8k7WsskAYgoSAPbNVXu2aQIMsx7CtPX0QXks0Y++SfzqnaQRwOJ35Oela EnS6HGbBEjdwCPmPPOTW3qlzABBJd7fO27goGCR1ANY+kTQ3d+Xm4iRcuT6VlXl617dSzsx/1jAZ 7DqP0oeoLQklmaeZpH5JNQt0z0K/y/8ArH+dOTmlcYwe2dp+h4NMYiswKuhw6ncp9CK9Dsrv7TZw zDo6Bq86jPPP8IJ/z+Ndf4fkP9kRqf4WYfqaVyo7m7JLhetZV3c7aluLgKpJPSuZ1PUyDsjO6Rug 9PeszdElzfM8vkw/NI36VesoDHyzZP8AWszTYBGvmOdztySa2IpBgU0gex0+jSbXWiqWmzYlAzRS Y1sUH0efaJfLEhZh8u7GPerkVoYieMDtWmvyqBknHGTTriIiPOOcdq3q01DY8+jVczIkwZAByQO1 RiTAxg7asECMhWHfk1UlYtcFVxs9ayNiKVz0xyDyakkcNYurdcYqCQsrSMeNnBp+BLEGB5PagCvC SI8HtwPenPtlXH4VHLIkalicbe3rUcV3A8mc9av2cmroj2kU7XLUECmJQvGMGtO1gXIz2NQR7Nw2 96sSTLAoJPNZqMm7ItySV2b8CwwW24kZxk5rB1G/iubaUOAyl2XHqOR/KqN3qruvlgkBjjHsOtYv 2phpwcjDEFsehr0cPgrK9Q4a+M6QMa70uSZnNlIsiKcFG4KVWTRNSncK+1EJABBHeuq8FlLj7eDy WAHPqM/41PJbA6nAxyFSQFsf59QK8+o7SaR2wTcU2Yktr/ZKHT1+8drSt3OegrEf5Lu4UdCxP4hi P5Guj1Ex3OqyneOnP4D/AOtXLK5klD/89JZP1ppjZdhOVx7fy/8ArVLL/qXHt/8AXqCBgGjPY5z+ opS7MuBwAvJ/ChsaAMFeQHrnb+ZzXUaBMW0rcezt/OuYgiw7MefmzzXoXhrwrdah4enuzmK1t4Wk Z8cu/wB7aPz5NS2XHTc5bW9VKKUi+Zj+lYlmjPKZHOWJ5Jq9qtuI5yMcZqG1XHFQnc3aNWA4XAq0 jVTiPFWI+vWtBG3pzjzV+tFQ2BIlXHrRUsDXkuNsakdS1X4JRcxEADIrEkb5VHoTU9jcbbhYyQFb kmvRrx5oniYefLJC3KMshXGW6+wFU8COQyMQdxxx0ArWv4pF+Zf4xxWJcRMbUpuxnnj1rzz0mJKu RMHGVkHFPWJUsWBzwuQRUow0cAdef4qbqCtDYvLDyY+x6EVUfiJexy9wGe4YtITk5wKVRDCPMLYx z1qq8mWYnAyc02OEzyB5P9WDwvqfWvWgtLWPKk7u9zZtdRbzAdp2YAGepqd7t5WLyEBfSs6P2Gas qFRcvgnHT0rWFOKfNYznVk1y3JA+9g5GC3Cf7vc1BJFui8ukidizTN1J4HoO1OMqpG8rHAAyfoK1 5lbUx5XfQk8Gj7NqN+hJ25H+H9f0redAs8kpGQsZf8QMj+Vcf4Yv2F3cseDMy4/PNdhqn7rTZyD1 ifP0xj+tfPVWnNtH0FPSKTPPZ59lxfzZznftP8qoKfLa124bL/4CllLyADp5h2nHtT0AMkBHTOfy JNCAmiiJcbu2SBVlwBHtAxu44pqcAH2p33mAAzk7QB3J/wA4pXKSOm8DeGH8Ua5HbNuW2X95O4/h XPT6npX0Nc6dBb+HbixtoljhFuyIi9BwayPAPhmPw34chQqPtdwBJOw9SOF+gH9a6mQbo2Ujggin bQm+p8t65DtmPHNZUQwea6XxRAYr6VMY2sRXOKO1ZROxstoatRdqqRjNW4RhhWpNzZ08fvV+tFSa YuZVxRUPcENL/MwPY1Ax3SgbsZ4pS2WzVeXhjXr7nz2x114A2nwNGd3y4BHSsCQt5YB+9nk+lauk XS3OjmJvvRHAPt1rn77VLKzEiyS5O7IwCc+1eZNcsmj1oO8UywAXjZI8qE7k8kZ4pmqXH2PSlR/m eXgiubn8SyecwgAVeDk9/wAKl1LUPtyWxzlgnzH3zV0Yc01cirPlg7FZdmeFqQHOD1HpUK9KkD4G K9aJ5Uiyr7RSB8k7jx6VXMhJxRv7Dqaq5NiyHJOM4z19hVDV7vZbi3U4aTr7LUktwlvC0jNhR19z 6CsCW4e4maV/vMenoOwrnxNbljyrc6MNR5pcz2Rp6DP5OrWyn7rSLnNdp4im8vTbkKQGEQ/Xr/Kv PbOXyryKTurAj61v6lqizl4c/wCst1H4gZ/lmvKkeojnVOS7/wBwbR9anRcSAdlXH9P8ahRduF67 f1NaEVsypvkUgHkA96G7DUW3oMeQRpk8+gHU1v8AgjS5NS8QQXM8eIIXDKCOrdvy61mQf6wE9a9F 8Dxh9Sts/wB9f51nzXN/ZW1Z7jEuyJUH8IAp5GaB0pa3OU8H+Iunm11y5AXCs29foea8/UfOR0Fe 3fFXTg8NveKOoMbH6cj+teKSLtkI7g1jazsdUXeJNHnAq1GelUkbJxVqM1oI6HRvmuUHqaKXw4C+ owr1ywFFQxnH2/iPY2y5jbb/AH1Ga1luorlPMikDD2NFFd9GpJvU8mvTildGrZXS2drCp+7NIwY+ 3ArB1fS9kUgY8KSR70UVhJ++zopr3EcnFETc7s52kda0gSg+bG7JJ9jmiitMP8TM8R8KQ4S8cmnC TPWiiu9HAxwJxk8CmTXMcEeWOB6dzRRRVk4xbRVKKlNJmPc3T3LgnhR0XsKhyBRRXlSk5O7PUikl ZB5m3mni5KzLNIRleAPUUUVJQ5L6ZLhZYAEZSCGZQ3I9jxVx9S1C5cvLNvJ6kqP6Ciimop7hzNbE sM9yCDtU/UV13hzxZPo91FMbNJQjAkbyOlFFP2cR+0l3PS7b4w2rqBNpMyt/sSg/0Fa0PxT0OTG+ 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 gidNumber: 2000 memberUid: one memberUid: two memberUid: three memberUid: four memberUid: five dn: cn=Admin,ou=Groups,dc=example,dc=com objectclass: posixGroup cn: Admin gidNumber: 2001 memberUid: one memberUid: two prosody-modules-c53cc1ae4788/mod_lib_ldap/dev/TODO.md0000644000175500017550000000012413163400720022040 0ustar debacledebacle * Make groups optional * Make groups work with both posixGroup and groupOfNames prosody-modules-c53cc1ae4788/mod_lib_ldap/dev/t/0002755000175500017550000000000013164216767021242 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_lib_ldap/dev/t/00-login.t0000644000175500017550000000205413163400720022732 0ustar debacledebacleuse strict; use warnings; use lib 't'; use TestConnection; use Test::More; my @users = ( 'one', 'two', 'three', 'four', 'five', 'six', ); plan tests => scalar(@users) + 3; foreach my $username (@users) { my $conn = TestConnection->new($username); $conn->reg_cb(session_ready => sub { $conn->cond->send; }); my $error = $conn->cond->recv; ok(! $error) or diag("$username login failed: $error"); } do { my $conn = TestConnection->new('one', password => '23451'); $conn->reg_cb(session_ready => sub { $conn->cond->send; }); my $error = $conn->cond->recv; ok($error); }; do { my $conn = TestConnection->new('invalid', password => '12345'); $conn->reg_cb(session_ready => sub { $conn->cond->send; }); 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); }; prosody-modules-c53cc1ae4788/mod_lib_ldap/dev/t/TestConnection.pm0000644000175500017550000000213113163400720024511 0ustar debacledebaclepackage TestConnection; use strict; use warnings; use parent 'AnyEvent::XMPP::IM::Connection'; use 5.010; our $HOST = 'localhost'; our $TIMEOUT = 5; our %PASSWORD_FOR = ( one => '12345', two => '23451', three => '34512', four => '45123', five => '51234', six => '123456', seven => '1234567', ); sub new { my ( $class, $username, %options ) = @_; my $cond = AnyEvent->condvar; my $timer = AnyEvent->timer( after => $TIMEOUT, cb => sub { $cond->send('timeout'); }, ); my $self = $class->SUPER::new( username => $username, domain => $HOST, password => $options{'password'} // $PASSWORD_FOR{$username}, resource => 'test bot', ); $self->reg_cb(error => sub { my ( undef, $error ) = @_; $cond->send($error->string); }); bless $self, $class; $self->{'condvar'} = $cond; $self->{'timeout_timer'} = $timer; $self->connect; return $self; } sub cond { my ( $self ) = @_; return $self->{'condvar'}; } 1; prosody-modules-c53cc1ae4788/mod_lib_ldap/dev/t/01-rosters.t0000644000175500017550000000617413163400720023333 0ustar debacledebacleuse strict; use warnings; use lib 't'; use AnyEvent::XMPP::Util qw(split_jid); use TestConnection; use Test::More; sub test_roster { my ( $username, $expected_contacts ) = @_; local $Test::Builder::Level = $Test::Builder::Level + 1; my @contacts; my $conn = TestConnection->new($username); $conn->reg_cb(roster_update => sub { my ( undef, $roster ) = @_; @contacts = sort { $a->{'username'} cmp $b->{'username'} } map { +{ username => (split_jid($_->jid))[0], name => $_->name, groups => [ sort $_->groups ], subscription => $_->subscription, } } $roster->get_contacts; $conn->cond->send; }); my $error = $conn->cond->recv; if($error) { fail($error); return; } @$expected_contacts = sort { $a->{'username'} cmp $b->{'username'} } @$expected_contacts; foreach my $contact (@$expected_contacts) { $contact->{'subscription'} = 'both'; @{ $contact->{'groups'} } = sort @{ $contact->{'groups'} }; } is_deeply(\@contacts, $expected_contacts); } plan tests => 5; test_roster(one => [{ username => 'two', name => 'Jane Testerson', groups => ['everyone', 'admin'], }, { username => 'three', name => 'Jerry Testerson', groups => ['everyone'], }, { username => 'four', name => 'Jack Testerson', groups => ['everyone'], }, { username => 'five', name => 'Jimmy Testerson', groups => ['everyone'], }]); test_roster(two => [{ username => 'one', name => 'John Testerson', groups => ['everyone', 'admin'], }, { username => 'three', name => 'Jerry Testerson', groups => ['everyone'], }, { username => 'four', name => 'Jack Testerson', groups => ['everyone'], }, { username => 'five', name => 'Jimmy Testerson', groups => ['everyone'], }]); test_roster(three => [{ username => 'one', name => 'John Testerson', groups => ['everyone'], }, { username => 'two', name => 'Jane Testerson', groups => ['everyone'], }, { username => 'four', name => 'Jack Testerson', groups => ['everyone'], }, { username => 'five', name => 'Jimmy Testerson', groups => ['everyone'], }]); test_roster(four => [{ username => 'one', name => 'John Testerson', groups => ['everyone'], }, { username => 'two', name => 'Jane Testerson', groups => ['everyone'], }, { username => 'three', name => 'Jerry Testerson', groups => ['everyone'], }, { username => 'five', name => 'Jimmy Testerson', groups => ['everyone'], }]); test_roster(five => [{ username => 'one', name => 'John Testerson', groups => ['everyone'], }, { username => 'two', name => 'Jane Testerson', groups => ['everyone'], }, { username => 'three', name => 'Jerry Testerson', groups => ['everyone'], }, { username => 'four', name => 'Jack Testerson', groups => ['everyone'], }]); prosody-modules-c53cc1ae4788/mod_lib_ldap/dev/t/02-vcard.t0000644000175500017550000003201613163400720022724 0ustar debacledebacleuse strict; use warnings; use lib 't'; use TestConnection; use AnyEvent::XMPP::Ext::VCard; use MIME::Base64 qw(decode_base64); use Test::More; sub test_vcard { my ( $username, $expected_fields ) = @_; $expected_fields->{'JABBERID'} = $username . '@' . $TestConnection::HOST; $expected_fields->{'VERSION'} = '2.0'; my $conn = TestConnection->new($username); my $vcard = AnyEvent::XMPP::Ext::VCard->new; local $Test::Builder::Level = $Test::Builder::Level + 1; $conn->reg_cb(stream_ready => sub { $vcard->hook_on($conn); }); $conn->reg_cb(session_ready => sub { $vcard->retrieve($conn, undef, sub { my ( $jid, $vcard, $error ) = @_; if(eval { $vcard->isa('AnyEvent::XMPP::Error') }) { $error = $vcard; } if($error) { $conn->cond->send($error->string); return; } delete $vcard->{'_avatar_hash'}; # we don't check this delete $vcard->{'PHOTO'}; # PHOTO data is treated specially # by the vCard extension foreach my $key (keys %$vcard) { my $value = $vcard->{$key}; $value = $value->[0] if ref($value) eq 'ARRAY'; if($value eq '') { delete $vcard->{$key}; } else { $vcard->{$key} = $value; } } is_deeply $vcard, $expected_fields or diag(explain($vcard)); $conn->cond->send; }); }); my $error = $conn->cond->recv; if($error) { fail($error); return; } } plan tests => 5; my $photo_data = do { local $/; my $data = ; chomp $data; decode_base64($data) }; test_vcard(one => { FN => 'John Testerson', NICKNAME => 'one', TEL => { NUMBER => '555-555-5555', WORK => '', VOICE => '', }, }); test_vcard(two => { FN => 'Jane Testerson', NICKNAME => 'two', TEL => { NUMBER => '', WORK => '', VOICE => '', }, }); test_vcard(three => { FN => 'Jerry Testerson', NICKNAME => 'three', TEL => { NUMBER => '', WORK => '', VOICE => '', }, }); test_vcard(four => { FN => 'Jack Testerson', NICKNAME => 'four', TEL => { NUMBER => '', WORK => '', VOICE => '', }, }); test_vcard(five => { FN => 'Jimmy Testerson', NICKNAME => 'five', TEL => { NUMBER => '', WORK => '', VOICE => '', }, _avatar => $photo_data, _avatar_type => 'image/jpeg', }); __DATA__ /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg SlBFRyB2NjIpLCBkZWZhdWx0IHF1YWxpdHkK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMP FB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEc ITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgA yADIAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMC BAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYn KCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeY mZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5 +v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwAB AgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpD REVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ip qrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMR AD8A9/opM0ZFADVGGan00feNLkUALSGkJqjfX5t4jsjkZyDtwuRmhK4m0ldl4sBnNZk+sRCJnt0a cqdoC/xH0FYM+t363cPlxyPCv+sDYBfPXgdMdqoXGtxRXxuZftMSxMypAkXBXuST3JreNB9TnliI 9GaNz4jvYpQk6wwCQ7UAcMwPoazrjUJpmO+Rm+prjtZ1Zb7UZLiOLyQcYA68dz71qWV99qtEkJ+b 7rc9xXq4ehFK9tTzsVOT1voaDszdTgVUlnVBhT/9eoZrgngVXLHOTXao2OBslLljk0hmx3qBn5zm omfBzV2EWPPOaPtJHeqZkqNpKl2GrmvDfsjA7sEd810+k+J5Y8JMfMT9RXn/AJ1WYropgZ/KuerS hOPvI1p1JQleLPRNY+w3Vut8kLEnKyFV5IIOO4zzXKzNaPfWipFIES1cbGTB3bsDjcf50/Sbu4Lh BcZjbho5BuUj3Bq5q1tBY6pBcvHDHC0GQFYIud3bj3r5jMsK4ax2Z9Ll+JU9JbopAafFDdS3GnzX HyNnaE4QlR/ER6VXt7vRv7RsSvh+4VjDIIpT5Gdvz5xyW9en+NaktxaxXl7GY4GWOE8NKmGAcZz+ 7JGPqfp3q1aotybCaOzQ25gf5kkyB970iH81rClBxhZmtWfNNs5CO+0C50qWaDwrax2zSoSjXVqv zYbB56d+vPNTeF7vR3+LDQQaI1rqAtMmYXQeML5a4AVRjpjoa2ltWOli1tbDaTLGEQTEcc458k4H b+vrDothJa/FqeWXSzGXgwtznIOEAPOB/kVqtzP0PRmTGMDtRVgrzmihQKUynHNd5Icxj3HNPMs+ PvJn6GubGsKTn5vqoFPXVmkU+XHKw/vZrosZXNp574A4MPtyf8KwrubXpXbbIIEB6hgc0v2qYtlZ GT6jNI1wmTueR26YC0AY19P4kTIFyZl9uKp2viPXLaXZPHcY9XjJB/H+tdJvXvGVA7soxUT3UfIw GTuRzincLFceILkENLaK/fONx/xq1Bd6XrDgyK8UvcDoaqta2dwN5X3yvH8qhezWIb4mYkddxzVR m1syJU4y3Q7U/DHmFpLaOOeMknHcVzot1tN8aRmPDYYe9dXY3rQOpEjbM4z2H19Ks6hpcGoxG4Ql ZeSdo4Jrvw+Ns7TOCvgrq8DiMjnims9Wb6zktC277uePes5n7Zr14zUldHkyg4uzHM1RO/FNZ6hd 6LgkPZuOKZuz1qIv6GmPJtGai5SROzBF5xUQmJbcTVQksxJp6mspSbNoxsblhdFHBzXpPhy7iuo1 jlCs6D5GPUeoryW3cqw5rsPDmoeTNGWbGDXJiaanCx0Yeo4TuenCJfSl2ACkilWWNXQgqRkEU814 57KGbRQU3KQe/pTgKXpTsBSe1PaWb/vs0VbYZHBxRWbhqVzHmAkQP80RUe3/ANc1ZE0bcAdPTrXJ y3csMO7du47Meas2V5NOuHLAf3UHT8a6HuZHR/aVj6I4Huxp8dzIWG1Ac92Un+lYgLZzu2+maR5f KUtvP1BJ/SpGa017Omf3iKPYVWlvJGXcjAv25xn8ax5b2Rk4dyPfiqiXJl3KSzc9xQBrpdXImXfG VJ7rkn9Kvm5u1HmbUOPUBTWFbyz7yUjI9z/nFXl81hmWUL79adguaUFzJOfMeONW+7kHitW0ultx jzVIzXM/aI48Rx5PqTxmrMVzICAISTjjIqWNG5qtsNQRDGAVBya4zULJ7WYpg4C7ua7XT5y4bd8u BkjFOurSG7wsiAkgjPtXXQxbp6PY5a+FjV1W55m8gx15qIyAitfVtBmt7l/KwyHOCTgmsBvMibaQ B9a9WFaM1oeVOhKm9USls85wfWoWcs3J/CkaYn5Tt/CmkjGc0OQlEeG6U4VEGz1pwepuVYsxnBFb ukzbZVrnkYE1qWMm2RaiexUdz2fR5UfTo9oAx2rQByK4vSdeg0rSJrq73m3iUM5RdxHOM4/Gkb4o aAIRJGl7IM4wsGD+pFeJWapzabPaoXnBNI7aiuFg+KGl3cgS207U5WJxhYV6/wDfVMuPijYWyuW0 jVTsJD/uV+X/AMerL2sO5ryS7HeHpRXnsPxa06e4WEaTqSlhwSqflw1FL20O4/Zy7HD3kkW3IDFu xqxpkflL5ag5bl3/AKCoY4lKEsm5ywwPStqyjjihO1gW6FsV1GBVnb7ODkbVPdjzUX2sCISFNyno SCc03Uf3mQd31JqH5EiQuC3y4VQM4AqRkU91GyPN0CAsfwrkNI8X+bemO9SONWOY3A+79f8AGuvn SNkK4Kq4wwxg815TrWlzaVqEkLfczujP95exFS9CkexWbh18wyB1PQ84NNuZ2Y8sUX6YOP6V5loH i670r9w586D/AJ5ueR9DXWR67aaiFaOYq7fwt29uKrmFY02vkib5eo7k1uabdSzWu/qn+ya5by4n ZSjl1IzxjI/AnrW1pep6daEwT+ZAkhw3moRzS0A6myuduGMmcetan2pPLyp5HWsfz9Kij3+ehUc/ J0rPl1qGRyLcEAcZ9aWgzUvb2zndYp1zk4HbBrD1XTbC8yY5EE+0YboSR1/rVfWZ3MKyWxLOBkqP vevSuIfUtUvZBJErRqpwWIOPSqhOcHoTKEZq0jfm0W4gTdsYr+eaqSW8sTYkRlPoRitK3N69hsiu HMmchQ3y/nmrQnuIrQw3WmPdSsBiRvlUe+Qa64Yt/aRyzwi+yzAKEDOOKYWPeuwvtIWbT4zHIm5V GUVcc1y1xavExBHIrphVVRXRy1KTpvUjSTkDNaFtKUYNjIrIKspq5a3LoRlQwFXzEJHoOgXVncp9 nlwUcbXikHDA12kfh3RzH8thAQw7rnIrzLSb20aRd0bK2a9R0W7We0VN2WUd/SvOxdKMveZ34SpJ e6i1aada2MPlW8CRxj+FRU0lvFNE0UsaPGwwVYZBqUUtciirWOu7KQ0uxAwLOADjjyx26UVcxRRy R7DuzwtnaGeNccAbj9auRzv5RzgZJ9gBk4/Tk1mPKXuBtzhWABHU4H8qbJcOURUPL/KMHoB71o3q TYsPMszEANs3cc4Le/0om87gYRF7AUQw+RgHc8h65Ocf4VXuDIXIjwQPvuc8n0FILELjzJSFQbMn 5jUep6JFqunPFIdkoGYZf7reh9qe27fkn7o9eBRFGbh8Gfao6DpQB5Re2N1pt41veQ7XU/xd/cUs d39nIWWHeo6EPjFet3+mW9/bGK7gWRAMKzfeH0NcpN4LsGfCSSIWOB3ApDKFjqAlgBgnaQrz8zfM n+NTW+tzRziGdg6d/MGaaPA93GwksbpN3YZIJqYaHqTlUv7JgFP+uQZx9aXNYLFu6vLm4AjtpvLA XkD0PSum0OWzttOiydz7AXdjncT1P51yt1BbwXfnW8pkCKAwPHT19ulV9N1EiQwO/J+6f6VomJnX avcxXK5QmIjkMpwa5kRzSOfnaZmPBc5P1q+XtyoDzDjt6fWqNzOiS7EQDA79896HYSJYZ7q1c7CN 44JAxg10OiiSZxNeyl1B4QM2M+/NYVnNHI2yVeQcIf61PdaiIMBFA7EhuKEJnSavrkVnbloSFKsA DniqNjqlnrxTYBBIRg56Zrib7UWv7wW287IzvdifSrNoklr/AKTbiQwxrlmA4+vvVRk4O8RSgpq0 jtr3Rfs6K8k0GG6YkGf1rPWxkjfKMMfnWI/iO3JYPKz44BLZ/Q1Ol+gAkRFA7OxGP0PWuiOLl1Rz ywkejOnsm8ra8hwckZI5rsNI1RoXVw3ToK8sbxRbxBWd0lK9doY/hnFL/wALCt7UZWDdjsHx/OuX EVJVHpsdeFp06S11Z9HWd5HeQCSM/UelEt/awTxwS3EUcshwiM4BY+w718yXfxd17yHi03ZZK4wX X5nx+PA/KqfgZNV8T+PtNMs81xKk6zySSMWwqkEk/l+tYpvqaSSvofWAOaKaDgc0VZB8+kkptQhS xwW79amswj3AAGI4F6+pqlK6IV9QuVH6ZNTaYCBMRzu5G7oPSgDQRi5LHOP4VHf/ABqOcs+2CMqF U5kY9B7UxpjHwGLO3ANNAVVK/wAAOWb+81AEMoEhIx8g/Wpbcr5rKseSo5Y9qgeZmJK+vFW7T5UY kdOhPOaAJJJPNiKkkAc8nFUlCBwOSQCfxp0gbfxlsnmoiqxSF87nzgegoEDMwBwcMeM57V1mi2uN OYzDeXX+LniuUWMzSqM/gK7K0lCWqIvQL1ob0BbnmWqTWwtWWW9El15oRYDCEKDPqOo6fpXPqjrq apxn0ro9Y0z7TqDlxlWZgG9MZxULaS8lxDcgFcKAxPHI60o7lPYDZFYWDHlgCD1/WqVwP3RZsgqN pz710Zt0MQUMpc8ZPWqNzZM5YAfORtxjn61skjJtoylkZZJVQHO/aW9h2/PNR3QxGH25P97OKvSR /Zn7Esx2pmq84ebAZVPHQcj8jSsNM59CVD/7TYwOprZ0yWc200EUzSOI2V4EQllzxn3GTWRqMIt3 RIyVcfM2O3cV33w8hvdRea6kmZhFGURgABuPTp7ZrOTsaRV2edy2s9nJme0uQc5BaMjNQyalNsKL Eyg9z1r2OfX7hEFldRRFo2I3OoLfnXO61bpcRtcxRQlVGWUxrx+lZ85v7B7nmi3d190yHB4IpojJ bnJ78GulD2Uhw1vbn8Mfyrb0t7RWAt9Otg/GCkW9vzOafOR7JnPaP4SvtV2OymC3P/LSQdR/sjvX vPw/0XTvC1qRbRBp5QPMuHHzN7ew9q5rTNP1C+kWWQrCo/vfMcV2axGygDuy+UoyWzwPrWNSU1qb Rpxsdsl3E4HPWivM73x1awSrbWkgllPBdeVj9z6n2opqqzN0UmefS/NMwJJMjgZPpWjEx+YKQoOB 9Ky3DNNBubG35mA+tW7WX5Xk7nGM9q3MC7jEuAD05J601pDMxSMDAGMjoKaSSCP4mPftU9qqojdB jJ69aAKwi2y/McenPSrDzZTanTpzVfmW5ZuNnNPzzgjIoAkciOIE8Hpk1RWTe0nykYHAqyXLtjby B09KpSkJcYB4xj6+poA0rUhpSw+g/CuogcLCqg8YrkdMlCuB69M9uK6KCTC8mkBkXUZFxMB8zBiR xUBjJQDHz9K0J1zdMxAwOee9NaKTYWXGW6KPSkMzrYEyncu714Iz+lVZJWW6YkYTGAevHrWlIhTM SHDYJJHaslweRwcZz/n8q1g7ohlHUSrbSvyqRkYHJqnA4jcg7WB59xVsQCRiwC9M4PU/Tmq3lFd5 YZB4GaUmNIyprb+0dWfaOGYEivYPDdgul+HlEMfBbc3Y81wOlaZvv0AyrdyB2rttUvvslhDbxOFP RgOcispa6GkEZVzpdqmoTT3E5mBclY1+UAe57/pVPUGW5i8hFCRf3VGBS7mkPOc1Yig3EZFXGma8 7Mq10KItnYPyrobHT4rfBCgfSpYIwo6VY3ha0UUibtmlBMIwADWvaXEc8bQyqGRhtZW6Ee9cqZ8d 6sWt6VkGDUT1Licj4g0t9F1mawGfs8vzxNj7yk8A+uDx+FFdrr2nrr2lYUf6XAC8LY6+q/j/ADxR XFJNOx2wlFx1POnkLFuxPJHoB/kVNbbsr37hfU1XRQ0cz+ox+tWYAY5h32qAPrXYeUXJGKkKpyw5 JPrUjSYQhM8jBNNjVjkngVKyfIiqOTzk9qAIkzHGTn+HOKhs5jcSFRnCHHXrVi5ysJUDkjFQ2irC hXHucUCLTrhCV496zpkwOOp+Zj6Cr7PlSWPbAAqpcoVxGpHzcsfYdqACxkVZox3GOK6eA8E/hXJQ kLdIe2a6SC4BizwKVwsWJHSOUO3A6Z9KbOrmPemefQZyKq3EoeJ1Hpx9antrhm0vzJHwzctyflAp bjKV0yhgG4OOhOCfrisu6mjUAZHTn2NVJ9Xgu7l5I33KRgE+nNZt1qUA3fMQfU962jZIhp3L1u/n B8FWQfoaEjCuVcHYxwCexpukMstxHJEcxkkYH61Jqbm3vZIj9wEEY9CKzkUjftzHpuny30mNiKW4 74HA/E1xlz4jup9QFycMufmU/wAQ/wAas67fTNbwWRmOxEDOB13GsBUwcEYPepgupTZ6HYPFdW6T xHcjjIP9K040AFcT4bvjbXf2R2/dTHK+zf8A167dG4re+g4kgOOlNeTHFIz45qpNNxxUORokLJIB 34p9vN845rMluAO4pkN4DJgGpuUdzp8hO1geRRVLQ590gB70Vm9y1scCNqJIh6AYqxAxkmPYAZqn McxrnrgA/hWjp8eQWxy1Vc5LF17hIYAChLnsO1IsmV3OcE9BTZQd+09yBRGDyzUwGtICcc/jURdR ggj5u9SPg9elV5YSlqX/ALg4NAExfAB9Dn9KZuJcufU4+mKYW8zbjpimTsVC46YOaAIXYrIg6Ekj NakUxCKB0AyaxFZpl3AHA4B9eK1YOAo7/wD16nYZeLZAIqysYn05om4DKcfyP+feoLW3dwDgn5jT 7xmsITK3CDr7UReoNHk8k7WsskAYgoSAPbNVXu2aQIMsx7CtPX0QXks0Y++SfzqnaQRwOJ35Oela EnS6HGbBEjdwCPmPPOTW3qlzABBJd7fO27goGCR1ANY+kTQ3d+Xm4iRcuT6VlXl617dSzsx/1jAZ 7DqP0oeoLQklmaeZpH5JNQt0z0K/y/8ArH+dOTmlcYwe2dp+h4NMYiswKuhw6ncp9CK9Dsrv7TZw zDo6Bq86jPPP8IJ/z+Ndf4fkP9kRqf4WYfqaVyo7m7JLhetZV3c7aluLgKpJPSuZ1PUyDsjO6Rug 9PeszdElzfM8vkw/NI36VesoDHyzZP8AWszTYBGvmOdztySa2IpBgU0gex0+jSbXWiqWmzYlAzRS Y1sUH0efaJfLEhZh8u7GPerkVoYieMDtWmvyqBknHGTTriIiPOOcdq3q01DY8+jVczIkwZAByQO1 RiTAxg7asECMhWHfk1UlYtcFVxs9ayNiKVz0xyDyakkcNYurdcYqCQsrSMeNnBp+BLEGB5PagCvC SI8HtwPenPtlXH4VHLIkalicbe3rUcV3A8mc9av2cmroj2kU7XLUECmJQvGMGtO1gXIz2NQR7Nw2 96sSTLAoJPNZqMm7ItySV2b8CwwW24kZxk5rB1G/iubaUOAyl2XHqOR/KqN3qruvlgkBjjHsOtYv 2phpwcjDEFsehr0cPgrK9Q4a+M6QMa70uSZnNlIsiKcFG4KVWTRNSncK+1EJABBHeuq8FlLj7eDy WAHPqM/41PJbA6nAxyFSQFsf59QK8+o7SaR2wTcU2Yktr/ZKHT1+8drSt3OegrEf5Lu4UdCxP4hi P5Guj1Ex3OqyneOnP4D/AOtXLK5klD/89JZP1ppjZdhOVx7fy/8ArVLL/qXHt/8AXqCBgGjPY5z+ opS7MuBwAvJ/ChsaAMFeQHrnb+ZzXUaBMW0rcezt/OuYgiw7MefmzzXoXhrwrdah4enuzmK1t4Wk Z8cu/wB7aPz5NS2XHTc5bW9VKKUi+Zj+lYlmjPKZHOWJ5Jq9qtuI5yMcZqG1XHFQnc3aNWA4XAq0 jVTiPFWI+vWtBG3pzjzV+tFQ2BIlXHrRUsDXkuNsakdS1X4JRcxEADIrEkb5VHoTU9jcbbhYyQFb kmvRrx5oniYefLJC3KMshXGW6+wFU8COQyMQdxxx0ArWv4pF+Zf4xxWJcRMbUpuxnnj1rzz0mJKu RMHGVkHFPWJUsWBzwuQRUow0cAdef4qbqCtDYvLDyY+x6EVUfiJexy9wGe4YtITk5wKVRDCPMLYx z1qq8mWYnAyc02OEzyB5P9WDwvqfWvWgtLWPKk7u9zZtdRbzAdp2YAGepqd7t5WLyEBfSs6P2Gas qFRcvgnHT0rWFOKfNYznVk1y3JA+9g5GC3Cf7vc1BJFui8ukidizTN1J4HoO1OMqpG8rHAAyfoK1 5lbUx5XfQk8Gj7NqN+hJ25H+H9f0redAs8kpGQsZf8QMj+Vcf4Yv2F3cseDMy4/PNdhqn7rTZyD1 ifP0xj+tfPVWnNtH0FPSKTPPZ59lxfzZznftP8qoKfLa124bL/4CllLyADp5h2nHtT0AMkBHTOfy JNCAmiiJcbu2SBVlwBHtAxu44pqcAH2p33mAAzk7QB3J/wA4pXKSOm8DeGH8Ua5HbNuW2X95O4/h XPT6npX0Nc6dBb+HbixtoljhFuyIi9BwayPAPhmPw34chQqPtdwBJOw9SOF+gH9a6mQbo2Ujggin bQm+p8t65DtmPHNZUQwea6XxRAYr6VMY2sRXOKO1ZROxstoatRdqqRjNW4RhhWpNzZ08fvV+tFSa YuZVxRUPcENL/MwPY1Ax3SgbsZ4pS2WzVeXhjXr7nz2x114A2nwNGd3y4BHSsCQt5YB+9nk+lauk XS3OjmJvvRHAPt1rn77VLKzEiyS5O7IwCc+1eZNcsmj1oO8UywAXjZI8qE7k8kZ4pmqXH2PSlR/m eXgiubn8SyecwgAVeDk9/wAKl1LUPtyWxzlgnzH3zV0Yc01cirPlg7FZdmeFqQHOD1HpUK9KkD4G K9aJ5Uiyr7RSB8k7jx6VXMhJxRv7Dqaq5NiyHJOM4z19hVDV7vZbi3U4aTr7LUktwlvC0jNhR19z 6CsCW4e4maV/vMenoOwrnxNbljyrc6MNR5pcz2Rp6DP5OrWyn7rSLnNdp4im8vTbkKQGEQ/Xr/Kv PbOXyryKTurAj61v6lqizl4c/wCst1H4gZ/lmvKkeojnVOS7/wBwbR9anRcSAdlXH9P8ahRduF67 f1NaEVsypvkUgHkA96G7DUW3oMeQRpk8+gHU1v8AgjS5NS8QQXM8eIIXDKCOrdvy61mQf6wE9a9F 8Dxh9Sts/wB9f51nzXN/ZW1Z7jEuyJUH8IAp5GaB0pa3OU8H+Iunm11y5AXCs29foea8/UfOR0Fe 3fFXTg8NveKOoMbH6cj+teKSLtkI7g1jazsdUXeJNHnAq1GelUkbJxVqM1oI6HRvmuUHqaKXw4C+ owr1ywFFQxnH2/iPY2y5jbb/AH1Ga1luorlPMikDD2NFFd9GpJvU8mvTildGrZXS2drCp+7NIwY+ 3ArB1fS9kUgY8KSR70UVhJ++zopr3EcnFETc7s52kda0gSg+bG7JJ9jmiitMP8TM8R8KQ4S8cmnC TPWiiu9HAxwJxk8CmTXMcEeWOB6dzRRRVk4xbRVKKlNJmPc3T3LgnhR0XsKhyBRRXlSk5O7PUikl ZB5m3mni5KzLNIRleAPUUUVJQ5L6ZLhZYAEZSCGZQ3I9jxVx9S1C5cvLNvJ6kqP6Ciimop7hzNbE sM9yCDtU/UV13hzxZPo91FMbNJQjAkbyOlFFP2cR+0l3PS7b4w2rqBNpMyt/sSg/0Fa0PxT0OTG+ K7j+qA4/I0UUNElHxV4s0HW/D81vBcP54IeNWiYZP5ehNeM3NrP5rFImYZ6gUUVDirmsJNKxGsE6 9YZB/wABNTJkcEEfWiigpM6rwgyR6vDLJ/q4zvb6Dmiiioe5dj//2Q== prosody-modules-c53cc1ae4788/mod_lib_ldap/dev/t/XMPP/0002755000175500017550000000000013164216767022026 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_lib_ldap/dev/t/XMPP/TestUtils.pm0000644000175500017550000000007013163400720024276 0ustar debacledebaclepackage XMPP::TestUtils; use strict; use warnings; 1; prosody-modules-c53cc1ae4788/mod_lib_ldap/dev/slapd.conf0000644000175500017550000000112013163400720022720 0ustar debacledebacleinclude /etc/openldap/schema/core.schema # I needed the following two schema definitions for posixGroup; if you don't # need it, don't include them include /etc/openldap/schema/cosine.schema include /etc/openldap/schema/nis.schema # needed for inetOrgPerson so I can test jpegPhoto include /etc/openldap/schema/inetorgperson.schema pidfile /var/run/openldap/slapd.pid argsfile /var/run/openldap/slapd.args database bdb suffix "dc=example,dc=com" rootdn "cn=Manager,dc=example,dc=com" rootpw prosody directory /var/lib/openldap/openldap-data index objectClass eq prosody-modules-c53cc1ae4788/mod_privilege/0002755000175500017550000000000013164216767020441 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_privilege/mod_privilege.lua0000644000175500017550000004023513163400720023752 0ustar debacledebacle-- XEP-0356 (Privileged Entity) -- Copyright (C) 2015-2016 Jérôme Poisson -- -- This module is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- Some parts come from mod_remote_roster (module by Waqas Hussain and Kim Alvefur, see https://code.google.com/p/prosody-modules/) -- TODO: manage external (for "roster" presence permission) when the account with the roster is offline local jid = require("util.jid") local set = require("util.set") local st = require("util.stanza") local roster_manager = require("core.rostermanager") local usermanager_user_exists = require "core.usermanager".user_exists; local hosts = prosody.hosts local full_sessions = prosody.full_sessions; local priv_session = module:shared("/*/privilege/session") if priv_session.connected_cb == nil then -- set used to have connected event listeners -- which allows a host to react on events from -- other hosts priv_session.connected_cb = set.new() end local connected_cb = priv_session.connected_cb -- the folowing sets are used to forward presence stanza -- the folowing sets are used to forward presence stanza local presence_man_ent = set.new() local presence_roster = set.new() local _ALLOWED_ROSTER = set.new({'none', 'get', 'set', 'both'}) local _ROSTER_GET_PERM = set.new({'get', 'both'}) local _ROSTER_SET_PERM = set.new({'set', 'both'}) local _ALLOWED_MESSAGE = set.new({'none', 'outgoing'}) local _ALLOWED_PRESENCE = set.new({'none', 'managed_entity', 'roster'}) local _PRESENCE_MANAGED = set.new({'managed_entity', 'roster'}) local _TO_CHECK = {roster=_ALLOWED_ROSTER, message=_ALLOWED_MESSAGE, presence=_ALLOWED_PRESENCE} local _PRIV_ENT_NS = 'urn:xmpp:privilege:1' local _FORWARDED_NS = 'urn:xmpp:forward:0' local _MODULE_HOST = module:get_host() module:log("debug", "Loading privileged entity module "); --> Permissions management <-- local privileges = module:get_option("privileged_entities", {}) local function get_session_privileges(session, host) if not session.privileges then return nil end return session.privileges[host] end local function advertise_perm(session, to_jid, perms) -- send stanza to advertise permissions -- as expained in § 4.2 local message = st.message({from=module.host, to=to_jid}) :tag("privilege", {xmlns=_PRIV_ENT_NS}) for _, perm in pairs({'roster', 'message', 'presence'}) do if perms[perm] then message:tag("perm", {access=perm, type=perms[perm]}):up() end end session.send(message) end local function set_presence_perm_set(to_jid, perms) -- fill the presence sets according to perms if _PRESENCE_MANAGED:contains(perms.presence) then presence_man_ent:add(to_jid) end if perms.presence == 'roster' then presence_roster:add(to_jid) end end local function advertise_presences(session, to_jid, perms) -- send presence status for already conencted entities -- as explained in § 7.1 -- people in roster are probed only for active sessions -- TODO: manage roster load for inactive sessions if not perms.presence then return; end local to_probe = {} for _, user_session in pairs(full_sessions) do if user_session.presence and _PRESENCE_MANAGED:contains(perms.presence) then local presence = st.clone(user_session.presence) presence.attr.to = to_jid module:log("debug", "sending current presence for "..tostring(user_session.full_jid)) session.send(presence) end if perms.presence == "roster" then -- we reset the cache to avoid to miss a presence that just changed priv_session.last_presence = nil if user_session.roster then local bare_jid = jid.bare(user_session.full_jid) for entity, item in pairs(user_session.roster) do if entity~=false and entity~="pending" and (item.subscription=="both" or item.subscription=="to") then local _, host = jid.split(entity) if not hosts[host] then -- we don't probe jid from hosts we manage -- using a table with entity as key avoid probing several time the same one to_probe[entity] = bare_jid end end end end end end -- now we probe peoples for "roster" presence permission for probe_to, probe_from in pairs(to_probe) do module:log("debug", "probing presence for %s (on behalf of %s)", tostring(probe_to), tostring(probe_from)) local probe = st.presence({from=probe_from, to=probe_to, type="probe"}) prosody.core_route_stanza(nil, probe) end end local function on_auth(event) -- Check if entity is privileged according to configuration, -- and set session.privileges accordingly local session = event.session local bare_jid = jid.join(session.username, session.host) if not session.privileges then session.privileges = {} end local ent_priv = privileges[bare_jid] if ent_priv ~= nil then module:log("debug", "Entity is privileged") for perm_type, allowed_values in pairs(_TO_CHECK) do local value = ent_priv[perm_type] if value ~= nil then if not allowed_values:contains(value) then module:log('warn', 'Invalid value for '..perm_type..' privilege: ['..value..']') module:log('warn', 'Setting '..perm_type..' privilege to none') ent_priv[perm_type] = nil end if value == 'none' then ent_priv[perm_type] = nil end end end -- extra checks for presence permission if ent_priv.presence == 'roster' and not _ROSTER_GET_PERM:contains(ent_priv.roster) then module:log("warn", "Can't allow roster presence privilege without roster \"get\" privilege") module:log("warn", "Setting presence permission to none") ent_priv.presence = nil end if session.type == "component" then -- we send the message stanza only for component -- it will be sent at first for other entities advertise_perm(session, bare_jid, ent_priv) set_presence_perm_set(bare_jid, ent_priv) advertise_presences(session, bare_jid, ent_priv) end end session.privileges[_MODULE_HOST] = ent_priv end local function on_presence(event) -- Permission are already checked at this point, -- we only advertise them to the entity local session = event.origin local session_privileges = get_session_privileges(session, _MODULE_HOST) if session_privileges then advertise_perm(session, session.full_jid, session_privileges) set_presence_perm_set(session.full_jid, session_privileges) advertise_presences(session, session.full_jid, session_privileges) end end local function on_component_auth(event) -- react to component-authenticated event from this host -- and call the on_auth methods from all other hosts -- needed for the component to get delegations advertising for callback in connected_cb:items() do callback(event) end end if module:get_host_type() ~= "component" then connected_cb:add(on_auth) end module:hook('authentication-success', on_auth) module:hook('component-authenticated', on_component_auth) module:hook('presence/initial', on_presence) --> roster permission <-- -- get module:hook("iq-get/bare/jabber:iq:roster:query", function(event) local session, stanza = event.origin, event.stanza; if not stanza.attr.to then -- we don't want stanzas addressed to /self return; end local node, host = jid.split(stanza.attr.to); local session_privileges = get_session_privileges(session, host) if session_privileges and _ROSTER_GET_PERM:contains(session_privileges.roster) then module:log("debug", "Roster get from allowed privileged entity received") -- following code is adapted from mod_remote_roster local roster = roster_manager.load_roster(node, host); local reply = st.reply(stanza):query("jabber:iq:roster"); for entity_jid, item in pairs(roster) do if entity_jid and entity_jid ~= "pending" then reply:tag("item", { jid = entity_jid, subscription = item.subscription, ask = item.ask, name = item.name, }); for group in pairs(item.groups) do reply:tag("group"):text(group):up(); end reply:up(); -- move out from item end end -- end of code adapted from mod_remote_roster session.send(reply); else module:log("warn", "Entity "..tostring(session.full_jid).." try to get roster without permission") session.send(st.error_reply(stanza, 'auth', 'forbidden')) end return true end); -- set module:hook("iq-set/bare/jabber:iq:roster:query", function(event) local session, stanza = event.origin, event.stanza; if not stanza.attr.to then -- we don't want stanzas addressed to /self return; end local from_node, from_host = jid.split(stanza.attr.to); local session_privileges = get_session_privileges(session, from_host) if session_privileges and _ROSTER_SET_PERM:contains(session_privileges.roster) then module:log("debug", "Roster set from allowed privileged entity received") -- following code is adapted from mod_remote_roster if not(usermanager_user_exists(from_node, from_host)) then return; end local roster = roster_manager.load_roster(from_node, from_host); if not(roster) then return; end local query = stanza.tags[1]; for _, item in ipairs(query.tags) do if item.name == "item" and item.attr.xmlns == "jabber:iq:roster" and item.attr.jid -- Protection against overwriting roster.pending, until we move it and item.attr.jid ~= "pending" then local item_jid = jid.prep(item.attr.jid); local _, host, resource = jid.split(item_jid); if not resource then if item_jid ~= stanza.attr.to then -- not self-item_jid if item.attr.subscription == "remove" then local r_item = roster[item_jid]; if r_item then roster[item_jid] = nil; if roster_manager.save_roster(from_node, from_host, roster) then session.send(st.reply(stanza)); roster_manager.roster_push(from_node, from_host, item_jid); else roster[item_jid] = item; session.send(st.error_reply(stanza, "wait", "internal-server-error", "Unable to save roster")); end else session.send(st.error_reply(stanza, "modify", "item-not-found")); end else local subscription = item.attr.subscription; if subscription ~= "both" and subscription ~= "to" and subscription ~= "from" and subscription ~= "none" then -- TODO error on invalid subscription = roster[item_jid] and roster[item_jid].subscription or "none"; end local r_item = {name = item.attr.name, groups = {}}; if r_item.name == "" then r_item.name = nil; end r_item.subscription = subscription; if subscription ~= "both" and subscription ~= "to" then r_item.ask = roster[item_jid] and roster[item_jid].ask; end for _, child in ipairs(item) do if child.name == "group" then local text = table.concat(child); if text and text ~= "" then r_item.groups[text] = true; end end end local olditem = roster[item_jid]; roster[item_jid] = r_item; if roster_manager.save_roster(from_node, from_host, roster) then -- Ok, send success session.send(st.reply(stanza)); -- and push change to all resources roster_manager.roster_push(from_node, from_host, item_jid); else -- Adding to roster failed roster[item_jid] = olditem; session.send(st.error_reply(stanza, "wait", "internal-server-error", "Unable to save roster")); end end else -- Trying to add self to roster session.send(st.error_reply(stanza, "cancel", "not-allowed")); end else -- Invalid JID added to roster module:log("warn", "resource: %s , host: %s", tostring(resource), tostring(host)) session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error? end else -- Roster set didn't include a single item, or its name wasn't 'item' session.send(st.error_reply(stanza, "modify", "bad-request")); end end -- for loop end -- end of code adapted from mod_remote_roster else -- The permission is not granted module:log("warn", "Entity "..tostring(session.full_jid).." try to set roster without permission") session.send(st.error_reply(stanza, 'auth', 'forbidden')) end return true end); --> message permission <-- module:hook("message/host", function(event) local session, stanza = event.origin, event.stanza; local privilege_elt = stanza:get_child('privilege', _PRIV_ENT_NS) if privilege_elt==nil then return; end local _, to_host = jid.split(stanza.attr.to) local session_privileges = get_session_privileges(session, to_host) if session_privileges and session_privileges.message=="outgoing" then if #privilege_elt.tags==1 and privilege_elt.tags[1].name == "forwarded" and privilege_elt.tags[1].attr.xmlns==_FORWARDED_NS then local message_elt = privilege_elt.tags[1]:get_child('message', 'jabber:client') if message_elt ~= nil then local _, from_host, from_resource = jid.split(message_elt.attr.from) if from_resource == nil and hosts[from_host] then -- we only accept bare jids from one of the server hosts -- at this point everything should be alright, we can send the message prosody.core_route_stanza(nil, message_elt) else -- trying to send a message from a forbidden entity module:log("warn", "Entity "..tostring(session.full_jid).." try to send a message from "..tostring(message_elt.attr.from)) session.send(st.error_reply(stanza, 'auth', 'forbidden')) end else -- incorrect message child session.send(st.error_reply(stanza, "modify", "bad-request", "invalid forwarded element")); end else -- incorrect forwarded child session.send(st.error_reply(stanza, "modify", "bad-request", "invalid element")); end; else -- The permission is not granted module:log("warn", "Entity "..tostring(session.full_jid).." try to send message without permission") session.send(st.error_reply(stanza, 'auth', 'forbidden')) end return true end); --> presence permission <-- local function same_tags(tag1, tag2) -- check if two tags are equivalent if tag1.name ~= tag2.name then return false; end if #tag1 ~= #tag2 then return false; end for name, value in pairs(tag1.attr) do if tag2.attr[name] ~= value then return false; end end for i=1,#tag1 do if type(tag1[i]) == "string" then if tag1[i] ~= tag2[i] then return false; end else if not same_tags(tag1[i], tag2[i]) then return false; end end end return true end local function same_presences(presence1, presence2) -- check that 2 stanzas are equivalent (except for "to" attribute) -- /!\ if the id change but everything else is equivalent, this method return false -- this behaviour may change in the future if presence1.attr.from ~= presence2.attr.from or presence1.attr.id ~= presence2.attr.id or presence1.attr.type ~= presence2.attr.type then return false end if presence1.attr.id and presence1.attr.id == presence2.attr.id then return true; end if #presence1 ~= #presence2 then return false; end for i=1,#presence1 do if type(presence1[i]) == "string" then if presence1[i] ~= presence2[i] then return false; end else if not same_tags(presence1[i], presence2[i]) then return false; end end end return true end local function forward_presence(presence, to_jid) local presence_fwd = st.clone(presence) presence_fwd.attr.to = to_jid module:log("debug", "presence forwarded to "..to_jid..": "..tostring(presence_fwd)) module:send(presence_fwd) -- cache used to avoid to send several times the same stanza priv_session.last_presence = presence end module:hook("presence/bare", function(event) if presence_man_ent:empty() and presence_roster:empty() then return; end local stanza = event.stanza if stanza.attr.type == nil or stanza.attr.type == "unavailable" then if not stanza.attr.to then for entity in presence_man_ent:items() do if stanza.attr.from ~= entity then forward_presence(stanza, entity); end end else -- directed presence -- we ignore directed presences from our own host, as we already have them local _, from_host = jid.split(stanza.attr.from) if hosts[from_host] then return; end -- we don't send several time the same presence, as recommended in §7 #2 if priv_session.last_presence and same_presences(priv_session.last_presence, stanza) then return end for entity in presence_roster:items() do if stanza.attr.from ~= entity then forward_presence(stanza, entity); end end end end end, 150) prosody-modules-c53cc1ae4788/mod_privilege/README.markdown0000644000175500017550000001045013163400720023117 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: 'XEP-0356 (Privileged Entity) implementation' ... Introduction ============ Privileged Entity is an extension which allows entity/component to have privileged access to server (set/get roster, send message on behalf of server, access presence informations). It can be used to build services independently of server (e.g.: PEP service). Details ======= You can have all the details by reading the [XEP-0356](http://xmpp.org/extensions/xep-0356.html). Usage ===== To use the module, like usual add **"privilege"** to your modules\_enabled. Note that if you use it with a local component, you also need to activate the module in your component section: modules_enabled = { [...] "privilege"; } [...] Component "youcomponent.yourdomain.tld" component_secret = "yourpassword" modules_enabled = {"privilege"} then specify privileged entities **in your host section** like that: VirtualHost "yourdomain.tld" privileged_entities = { ["romeo@montaigu.lit"] = { roster = "get"; presence = "managed_entity"; }, ["juliet@capulet.lit"] = { roster = "both"; message = "outgoing"; presence = "roster"; }, } Here *romeo@montaigu.lit* can **get** roster of anybody on the host, and will **have presence for any user** of the host, while *juliet@capulet.lit* can **get** and **set** a roster, **send messages** on the behalf of the server, and **access presence of anybody linked to the host** (not only people on the server, but also people in rosters of users of the server). **/! Be extra careful when you give a permission to an entity/component, it's a powerful access, only do it if you absoly trust the component/entity, and you know where the software is coming from** Configuration ============= All the permissions give access to all accounts of the virtual host. -------- ------------------------------------------------ ---------------------- roster none *(default)* No access to rosters get Allow **read** access to rosters set Allow **write** access to rosters both Allow **read** and **write** access to rosters -------- ------------------------------------------------ ---------------------- message ------- ------------------ ------------------------------------------------------------ none *(default)* Can't send message from server outgoing Allow to send message on behalf of server (from bare jids) ------------------ ------------------------------------------------------------ presence -------- ------------------ ------------------------------------------------------------------------------------------------ none *(default)* Do not have extra presence information managed\_entity Receive presence stanzas (except subscriptions) from host users roster Receive all presence stanzas (except subsciptions) from host users and people in their rosters ------------------ ------------------------------------------------------------------------------------------------ Compatibility ============= If you use it with Prosody 0.9 and with a component, you need to patch core/mod\_component.lua to fire a new signal. To do it, copy the following patch in a, for example, /tmp/component.patch file: ``` {.patch} diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua --- a/plugins/mod_component.lua +++ b/plugins/mod_component.lua @@ -85,6 +85,7 @@ session.type = "component"; module:log("info", "External component successfully authenticated"); session.send(st.stanza("handshake")); + module:fire_event("component-authenticated", { session = session }); return true; end ``` Then, at the root of prosody, enter: `patch -p1 < /tmp/component.patch` ----- ---------------------------------------------------- 0.10 Works 0.9 Need a patched core/mod\_component.lua (see above) ----- ---------------------------------------------------- Note ==== This module is often used with mod\_delegation (c.f. XEP for more details) prosody-modules-c53cc1ae4788/mod_incidents_handling/0002755000175500017550000000000013164216767022277 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_incidents_handling/README.markdown0000644000175500017550000000224413163400720024757 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: Incidents Handling plugin ... Introduction ============ This module implements [XEP-268](http://xmpp.org/extensions/xep-0268.html). Details ======= It will let you manage reports, inquiries, requests and responses through an Adhoc interface. The following new adhoc admin commands will be available: - List Incidents -- List all available incidents and let's you reply requests. - Send Incident Inquiry -- Inquiry a remote server about an incident. - Send Incident Report -- Send an incident report to a remote server. - Send Incident Request -- Send an incident request to a remote server. Each Adhoc form provides syntax instructions through `` elements (they may currently be stripped by Prosody), although it's encouraged to read the [IODEF specifications](https://tools.ietf.org/html/rfc5070). Usage ===== Copy the module folder into your prosody modules directory. Place the module between your enabled modules either into the global or a vhost section. Optional configuration directives: ``` {.lua} incidents_expire_time = 86400 -- Expiral of "closed" incidents in seconds. ``` Info ==== - to be 0.9, works. prosody-modules-c53cc1ae4788/mod_incidents_handling/incidents_handling/0002755000175500017550000000000013164216767026123 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_incidents_handling/incidents_handling/incidents_handling.lib.lua0000644000175500017550000003771513163400720033211 0ustar debacledebacle-- 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 prosody-modules-c53cc1ae4788/mod_incidents_handling/incidents_handling/mod_incidents_handling.lua0000644000175500017550000003175013163400720033274 0ustar debacledebacle-- 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, setmetatable = pairs, os.time, setmetatable 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 prosody-modules-c53cc1ae4788/mod_track_muc_joins/0002755000175500017550000000000013164216767021625 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_track_muc_joins/README.markdown0000644000175500017550000000333613163400720024310 0ustar debacledebacle--- summary: Keep track of joined chat rooms ... # Introduction This module attempts to keep track of what MUC chat rooms users have joined. It's not very useful on its own, but can be used by other modules to influence decisions. # Usage Rooms joined and the associated nickname is kept in a table field `rooms_joined` on the users session. An example: ``` lua local jid_bare = require"util.jid".bare; module:hook("message/full", function (event) local stanza = event.stanza; local session = prosody.full_sessions[stanza.attr.to]; if not session then return -- No such session end local joined_rooms = session.joined_rooms; if not joined_rooms then return -- This session hasn't joined any rooms at all end -- joined_rooms is a map of room JID -> room nickname local nickname = joined_rooms[jid_bare(stanza.attr.from)]; if nickname then session.log("info", "Got a MUC message from %s", stanza.attr.from); local body = stanza:get_child_text("body"); if body and body:find(nickname, 1, true) then session.log("info", "The message contains my nickname!"); end end end); ``` # Known issues [XEP 45 § 7.2.3 Presence Broadcast][enter-pres] has the following text: > In particular, if roomnicks are locked down then the service MUST do > one of the following. > > \[...\] > > If the user has connected using a MUC client (...), then the service > MUST allow the client to enter the room, modify the nick in accordance > with the lockdown policy, and **include a status code of "210"** in > the presence broadcast that it sends to the new occupant. This case is not yet handled. [enter-pres]: http://xmpp.org/extensions/xep-0045.html#enter-pres prosody-modules-c53cc1ae4788/mod_track_muc_joins/mod_track_muc_joins.lua0000644000175500017550000000260513163400720026321 0ustar debacledebaclelocal jid_bare = require "util.jid".bare; local jid_split = require "util.jid".split; local sessions = prosody.full_sessions; module:hook("presence/full", function (event) local stanza = event.stanza; local session = sessions[stanza.attr.to]; if not session then return end; if not session.directed then return end -- hasn't sent presence yet local log = session.log or module._log; local muc_x = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); if not muc_x then return end -- Not MUC related local from_jid = stanza.attr.from; local room = jid_bare(from_jid); local _,_,nick = jid_split(from_jid); local joined = stanza.attr.type; if joined == nil then joined = nick; elseif joined == "unavailable" then joined = nil; else -- Ignore errors and whatever return; end if joined and not session.directed[from_jid] then return; -- Never sent presence there, can't be a MUC join end -- Check for status code 110, meaning it's their own reflected presence for status in muc_x:childtags("status") do log("debug", "Status code %d", status.attr.code); if status.attr.code == "110" then log("debug", "%s room %s", joined and "Joined" or "Left", room); local rooms = session.rooms_joined; if not rooms then if not joined then return; end session.rooms_joined = { [room] = joined }; else rooms[room] = joined; end return; end end end, 1); prosody-modules-c53cc1ae4788/mod_twitter/0002755000175500017550000000000013164216767020155 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_twitter/mod_twitter.lua0000644000175500017550000003533313163400720023205 0ustar debacledebacle-- for Prosody -- via dersd if module:get_host_type() ~= "component" then error(module.name.." should be loaded as a component, check out http://prosody.im/doc/components", 0); end local jid_split = require "util.jid".split; local st = require "util.stanza"; local componentmanager = require "core.componentmanager"; local datamanager = require "util.datamanager"; local timer = require "util.timer"; local http = require "net.http"; local json = require "util.json"; local base64 = require "util.encodings".base64; local component_host = module:get_host(); local component_name = module.name; local data_cache = {}; function print_r(obj) return require("util.serialization").serialize(obj); end function dmsg(jid, msg) module:log("debug", msg or "nil"); if jid ~= nil then module:send(st.message({to=jid, from=component_host, type='chat'}):tag("body"):text(msg or "nil"):up()); end end function substring(string, start_string, ending_string) local s_value_start, s_value_finish = nil, nil; if start_string ~= nil then _, s_value_start = string:find(start_string); if s_value_start == nil then -- error return nil; end else return nil; end if ending_string ~= nil then _, s_value_finish = string:find(ending_string, s_value_start+1); if s_value_finish == nil then -- error return nil; end else s_value_finish = string:len()+1; end return string:sub(s_value_start+1, s_value_finish-1); end local http_timeout = 30; local http_queue = setmetatable({}, { __mode = "k" }); -- auto-cleaning nil elements data_cache['prosody_os'] = prosody.platform; data_cache['prosody_version'] = prosody.version; local http_headers = { ["User-Agent"] = "Prosody ("..data_cache['prosody_version'].."; "..data_cache['prosody_os']..")" --"ELinks (0.4pre5; Linux 2.4.27 i686; 80x25)", }; function http_action_callback(response, code, request, xcallback) if http_queue == nil or http_queue[request] == nil then return; end local id = http_queue[request]; http_queue[request] = nil; if xcallback == nil then dmsg(nil, "http_action_callback reports that xcallback is nil"); else xcallback(id, response, request); end return true; end function http_add_action(tid, url, method, post, fcallback) local request = http.request(url, { headers = http_headers or {}, body = http.formencode(post or {}), method = method or "GET" }, function(response_body, code, response, request) http_action_callback(response_body, code, request, fcallback) end); http_queue[request] = tid; timer.add_task(http_timeout, function() http.destroy_request(request); end); return true; end local users = setmetatable({}, {__mode="k"}); local user = {}; user.__index = user; user.dosync = false; user.valid = false; user.data = {}; function user:login() userdata = datamanager.load(self.jid, component_host, "data"); if userdata ~= nil then self.data = userdata; if self.data['_twitter_sess'] ~= nil then http_headers['Cookie'] = "_twitter_sess="..self.data['_twitter_sess']..";"; end module:send(st.presence({to=self.jid, from=component_host})); self:twitterAction("VerifyCredentials"); if self.data.dosync == 1 then self.dosync = true; timer.add_task(self.data.refreshrate, function() return users[self.jid]:sync(); end) end else module:send(st.message({to=self.jid, from=component_host, type='chat'}):tag("body"):text("You are not signed in.")); end end function user:logout() datamanager.store(self.jid, component_host, "data", self.data); self.dosync = false; module:send(st.presence({to=self.jid, from=component_host, type='unavailable'})); end function user:sync() if self.dosync then table.foreach(self.data.synclines, function(ind, line) self:twitterAction(line.name, {sinceid=line.sinceid}) end); return self.data.refreshrate; end end function user:signin() if datamanager.load(self.jid, component_host, "data") == nil then datamanager.store(self.jid, component_host, "data", {login=self.data.login, password=self.data.password, refreshrate=60, dosync=1, synclines={{name='HomeTimeline', sinceid=0}}, syncstatus=0}) module:send(st.presence{to=self.jid, from=component_host, type='subscribe'}); module:send(st.presence{to=self.jid, from=component_host, type='subscribed'}); end end function user:signout() if datamanager.load(self.jid, component_host, "data") ~= nil then datamanager.store(self.jid, component_host, "data", nil); module:send(st.presence({to=self.jid, from=component_host, type='unavailable'})); module:send(st.presence({to=self.jid, from=component_host, type='unsubscribe'})); module:send(st.presence({to=self.jid, from=component_host, type='unsubscribed'})); end end local twitterApiUrl = "http://api.twitter.com"; local twitterApiVersion = "1"; local twitterApiDataType = "json"; local twitterActionUrl = function(action) return twitterApiUrl.."/"..twitterApiVersion.."/"..action.."."..twitterApiDataType end; local twitterActionMap = { PublicTimeline = { url = twitterActionUrl("statuses/public_timeline"), method = "GET", needauth = false, }, HomeTimeline = { url = twitterActionUrl("statuses/home_timeline"), method = "GET", needauth = true, }, FriendsTimeline = { url = twitterActionUrl("statuses/friends_timeline"), method = "GET", needauth = true, }, UserTimeline = { url = twitterActionUrl("statuses/friends_timeline"), method = "GET", needauth = true, }, VerifyCredentials = { url = twitterActionUrl("account/verify_credentials"), method = "GET", needauth = true, }, UpdateStatus = { url = twitterActionUrl("statuses/update"), method = "POST", needauth = true, }, Retweet = { url = twitterActionUrl("statuses/retweet/%tweetid"), method = "POST", needauth = true, } } function user:twitterAction(line, params) local action = twitterActionMap[line]; if action then local url = action.url; local post = {}; --if action.needauth and not self.valid and line ~= "VerifyCredentials" then -- return --end if action.needauth then http_headers['Authorization'] = "Basic "..base64.encode(self.data.login..":"..self.data.password); --url = string.gsub(url, "http\:\/\/", string.format("http://%s:%s@", self.data.login, self.data.password)); end if params and type(params) == "table" then post = params; end if action.method == "GET" and post ~= {} then url = url.."?"..http.formencode(post); end http_add_action(line, url, action.method, post, function(...) self:twitterActionResult(...) end); else module:send(st.message({to=self.jid, from=component_host, type='chat'}):tag("body"):text("Wrong twitter action!"):up()); end end local twitterActionResultMap = { PublicTimeline = {exec=function(jid, response) --module:send(st.message({to=jid, from=component_host, type='chat'}):tag("body"):text(print_r(response)):up()); return end}, HomeTimeline = {exec=function(jid, response) --module:send(st.message({to=jid, from=component_host, type='chat'}):tag("body"):text(print_r(response)):up()); return end}, FriendsTimeline = {function(jid, response) return end}, UserTimeline = {exec=function(jid, response) return end}, VerifyCredentials = {exec=function(jid, response) if response ~= nil and response.id ~= nil then users[jid].valid = true; users[jid].id = response.id; end return end}, UpdateStatus = {exec=function(jid, response) return end}, Retweet = {exec=function(jid, response) return end} } function user:twitterActionResult(id, response, request) if request ~= nil and request.responseheaders['set-cookie'] ~= nil and request.responseheaders['location'] ~= nil then --self.data['_twitter_sess'] = substring(request.responseheaders['set-cookie'], "_twitter_sess=", ";"); --http_add_action(id, request.responseheaders['location'], "GET", {}, function(...) self:twitterActionResult(...) end); return true; end local result, tmp_json = pcall(function() json.decode(response or "{}") end); if result and id ~= nil then twitterActionResultMap[id]:exec(self.jid, tmp_json); end return true; end function iq_success(event) local origin, stanza = event.origin, event.stanza; local reply = data_cache.success; if reply == nil then reply = st.iq({type='result', from=stanza.attr.to or component_host}); data_cache.success = reply; end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; origin.send(reply); return true; end function iq_disco_info(event) local origin, stanza = event.origin, event.stanza; local from = {}; from.node, from.host, from.resource = jid_split(stanza.attr.from); local bjid = from.node.."@"..from.host; local reply = data_cache.disco_info; if reply == nil then reply = st.iq({type='result', from=stanza.attr.to or component_host}):query("http://jabber.org/protocol/disco#info") :tag("identity", {category='gateway', type='chat', name=component_name}):up(); reply = reply:tag("feature", {var="urn:xmpp:receipts"}):up(); reply = reply:tag("feature", {var="http://jabber.org/protocol/commands"}):up(); reply = reply:tag("feature", {var="jabber:iq:register"}):up(); --reply = reply:tag("feature", {var="jabber:iq:time"}):up(); --reply = reply:tag("feature", {var="jabber:iq:version"}):up(); --reply = reply:tag("feature", {var="http://jabber.org/protocol/stats"}):up(); data_cache.disco_info = reply; end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; origin.send(reply); return true; end function iq_disco_items(event) local origin, stanza = event.origin, event.stanza; local reply = data_cache.disco_items; if reply == nil then reply = st.iq({type='result', from=stanza.attr.to or component_host}):query("http://jabber.org/protocol/disco#items"); data_cache.disco_items = reply; end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; origin.send(reply); return true; end function iq_register(event) local origin, stanza = event.origin, event.stanza; if stanza.attr.type == "get" then local reply = data_cache.registration_form; if reply == nil then reply = st.iq({type='result', from=stanza.attr.to or component_host}) :tag("query", { xmlns="jabber:iq:register" }) :tag("instructions"):text("Enter your twitter data"):up() :tag("username"):up() :tag("password"):up(); data_cache.registration_form = reply end reply.attr.id = stanza.attr.id; reply.attr.to = stanza.attr.from; origin.send(reply); elseif stanza.attr.type == "set" then local from = {}; from.node, from.host, from.resource = jid_split(stanza.attr.from); local bjid = from.node.."@"..from.host; local username, password = "", ""; local reply; for _, tag in ipairs(stanza.tags[1].tags) do if tag.name == "remove" then users[bjid]:signout(); iq_success(event); return true; end if tag.name == "username" then username = tag[1]; end if tag.name == "password" then password = tag[1]; end end if username ~= nil and password ~= nil then users[bjid] = setmetatable({}, user); users[bjid].jid = bjid; users[bjid].data.login = username; users[bjid].data.password = password; users[bjid]:signin(); users[bjid]:login(); end iq_success(event); return true; end end function presence_stanza_handler(event) local origin, stanza = event.origin, event.stanza; local to = {}; local from = {}; local pres = {}; to.node, to.host, to.resource = jid_split(stanza.attr.to); from.node, from.host, from.resource = jid_split(stanza.attr.from); pres.type = stanza.attr.type; for _, tag in ipairs(stanza.tags) do pres[tag.name] = tag[1]; end local from_bjid = nil; if from.node ~= nil and from.host ~= nil then from_bjid = from.node.."@"..from.host; elseif from.host ~= nil then from_bjid = from.host; end if pres.type == nil then if users[from_bjid] ~= nil then -- Status change if pres['status'] ~= nil and users[from_bjid]['data']['sync_status'] then users[from_bjid]:twitterAction("UpdateStatus", {status=pres['status']}); end else -- User login request users[from_bjid] = setmetatable({}, user); users[from_bjid].jid = from_bjid; users[from_bjid]:login(); end origin.send(st.presence({to=from_bjid, from=component_host})); elseif pres.type == 'subscribe' and users[from_bjid] ~= nil then origin.send(st.presence{to=from_bjid, from=component_host, type='subscribed'}); elseif pres.type == 'unsubscribed' and users[from_bjid] ~= nil then users[from_bjid]:logout(); users[from_bjid]:signout(); users[from_bjid] = nil; elseif pres.type == 'unavailable' and users[from_bjid] ~= nil then users[from_bjid]:logout(); users[from_bjid] = nil; end return true; end function confirm_message_delivery(event) local reply = st.message({id=event.stanza.attr.id, to=event.stanza.attr.from, from=event.stanza.attr.to or component_host}):tag("received", {xmlns = "urn:xmpp:receipts"}); origin.send(reply); return true; end function message_stanza_handler(event) local origin, stanza = event.origin, event.stanza; local to = {}; local from = {}; local msg = {}; to.node, to.host, to.resource = jid_split(stanza.attr.to); from.node, from.host, from.resource = jid_split(stanza.attr.from); local bjid = nil; if from.node ~= nil and from.host ~= nil then from_bjid = from.node.."@"..from.host; elseif from.host ~= nil then from_bjid = from.host; end local to_bjid = nil; if to.node ~= nil and to.host ~= nil then to_bjid = to.node.."@"..to.host; elseif to.host ~= nil then to_bjid = to.host; end for _, tag in ipairs(stanza.tags) do msg[tag.name] = tag[1]; if tag.attr.xmlns == "urn:xmpp:receipts" then confirm_message_delivery({origin=origin, stanza=stanza}); end -- can handle more xmlns end -- Now parse the message if stanza.attr.to == component_host then if msg.body == "!myinfo" then if users[from_bjid] ~= nil then origin.send(st.message({to=stanza.attr.from, from=component_host, type='chat'}):tag("body"):text(print_r(users[from_bjid])):up()); end end -- Other messages go to twitter user:twitterAction("UpdateStatus", {status=msg.body}); else -- Message to uid@host/resource end return true; end module:hook("presence/host", presence_stanza_handler); module:hook("message/host", message_stanza_handler); module:hook("iq/host/jabber:iq:register:query", iq_register); module:hook("iq/host/http://jabber.org/protocol/disco#info:query", iq_disco_info); module:hook("iq/host/http://jabber.org/protocol/disco#items:query", iq_disco_items); module:hook("iq/host", function(data) -- IQ to a local host recieved local origin, stanza = data.origin, data.stanza; if stanza.attr.type == "get" or stanza.attr.type == "set" then return module:fire_event("iq/host/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data); else module:fire_event("iq/host/"..stanza.attr.id, data); return true; end end); module.unload = function() componentmanager.deregister_component(component_host); end component = componentmanager.register_component(component_host, function() return; end); prosody-modules-c53cc1ae4788/mod_twitter/README.markdown0000644000175500017550000000175513163400720022643 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: 'Simple example of working component and HTTP polling.' ... Introduction ============ Twitter has simple API to use, so I tried to deal with it via Prosody. I didn't manage to finish this module, but it is nice example of component that accepts registrations, unregistrations, does HTTP polling and so on. Maybe someone will finnish this idea. Details ======= It does require some non-prosody Lua libraries: LuaJSON Configuration ============= At the moment no configuration needed, but you can configure some variables inside code. TODO ==== - Send latest tweets to XMPP user - Reply user's messages to Twitter - OAuth support - User configuration (forms) - discuss about using cjson - [!!!!] rewrite to be compatible with 0.9+ - drop? (since it is mod\_twitter in spectrum) Compatibility ============= ------- --------------------- trunk Currently Not Works 0.9 Currently Not Works 0.8 Works ------- --------------------- prosody-modules-c53cc1ae4788/mod_profile/0002755000175500017550000000000013164216767020113 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_profile/README.markdown0000644000175500017550000000170113163400720022570 0ustar debacledebacle--- labels: - 'Stage-Alpha' summary: 'Replacement for mod\_vcard with vcard4 support and PEP integration' --- Introduction ============ This module provides a replacement for mod\_vcard. In addition to the ageing protocol defined by [XEP-0054](http://xmpp.org/extensions/xep-0054.html), it also supports the [new vCard 4 based protocol](http://xmpp.org/extensions/xep-0292.html) and integrates with [Personal Eventing Protocol](http://xmpp.org/extensions/xep-0163.html). The vCard 4, [User Avatar](http://xmpp.org/extensions/xep-0084.html) and [User Nickname](http://xmpp.org/extensions/xep-0172.html) PEP nodes are updated when the vCard is changed.. Configuration ============= modules_enabled = { -- "pep"; -- These two modules must be removed -- "vcard"; "profile"; } Compatibility ============= Requires Prosody **trunk** as of 2014-05-29. Won't work in 0.10. It depends on the new mod\_pep\_plus for PEP support. prosody-modules-c53cc1ae4788/mod_profile/mod_profile.lua0000644000175500017550000001577413163400720023110 0ustar debacledebacle-- mod_profile local st = require"util.stanza"; local jid_split = require"util.jid".split; local jid_bare = require"util.jid".bare; local is_admin = require"core.usermanager".is_admin; local vcard = require"util.vcard"; local base64 = require"util.encodings".base64; local sha1 = require"util.hashes".sha1; local t_insert, t_remove = table.insert, table.remove; local pep_plus; if module:get_host_type() == "local" and module:get_option_boolean("vcard_to_pep", true) then pep_plus = module:depends"pep_plus"; end local storage = module:open_store(); local legacy_storage = module:open_store("vcard"); local function get_item(vcard, name) local item; for i=1, #vcard do item=vcard[i]; if item.name == name then return item, i; end end end local magic_mime = { ["\137PNG\r\n\026\n"] = "image/png"; ["\255\216"] = "image/jpeg"; ["GIF87a"] = "image/gif"; ["GIF89a"] = "image/gif"; [" -- License: MPLv2 -- -- Requirements: -- Prosody config: -- storage = { roster = "memory" } -- modules_disabled = { "roster" } -- Dependencies: -- Prosody 0.9 -- lua-cjson (Debian/Ubuntu/LuaRocks: lua-cjson) local http = require "net.http"; local json = require "cjson"; local it = require "util.iterators"; local set = require "util.set"; local rm = require "core.rostermanager"; local st = require "util.stanza"; local array = require "util.array"; local host = module.host; local sessions = hosts[host].sessions; local roster_url = module:get_option_string("http_roster_url", "http://localhost/%s"); -- Send a roster push to the named user, with the given roster, for the specified -- contact's roster entry. Used to notify clients of changes/removals. local function roster_push(username, roster, contact_jid) local stanza = st.iq({type="set"}) :tag("query", {xmlns = "jabber:iq:roster" }); local item = roster[contact_jid]; if item then stanza:tag("item", {jid = contact_jid, subscription = item.subscription, name = item.name, ask = item.ask}); for group in pairs(item.groups) do stanza:tag("group"):text(group):up(); end else stanza:tag("item", {jid = contact_jid, subscription = "remove"}); end stanza:up():up(); -- move out from item for _, session in pairs(hosts[host].sessions[username].sessions) do if session.interested then session.send(stanza); end end end -- Send latest presence from the named local user to a contact. local function send_presence(username, contact_jid, available) module:log("debug", "Sending %savailable presence from %s to contact %s", (available and "" or "un"), username, contact_jid); for resource, session in pairs(sessions[username].sessions) do local pres; if available then pres = st.clone(session.presence); pres.attr.to = contact_jid; else pres = st.presence({ to = contact_jid, from = session.full_jid, type = "unavailable" }); end module:send(pres); end end -- Converts a 'friend' object from the API to a Prosody roster item object local function friend_to_roster_item(friend) return { name = friend.name; subscription = "both"; groups = friend.groups or {}; }; end -- Returns a handler function to consume the data returned from -- the API, compare it to the user's current roster, and perform -- any actions necessary (roster pushes, presence probes) to -- synchronize them. local function updated_friends_handler(username, cb) return (function (ok, code, friends) if not ok then cb(false, code); end local user = sessions[username]; local roster = user.roster; local old_contacts = set.new(array.collect(it.keys(roster))); local new_contacts = set.new(array.collect(it.keys(friends))); -- These two entries are not real contacts, ignore them old_contacts:remove(false); old_contacts:remove("pending"); module:log("debug", "New friends list of %s: %s", username, json.encode(friends)); -- Calculate which contacts have been added/removed since -- the last time we fetched the roster local added_contacts = new_contacts - old_contacts; local removed_contacts = old_contacts - new_contacts; local added, removed = 0, 0; -- Add new contacts and notify connected clients for contact_jid in added_contacts do module:log("debug", "Processing new friend of %s: %s", username, contact_jid); roster[contact_jid] = friend_to_roster_item(friends[contact_jid]); roster_push(username, roster, contact_jid); send_presence(username, contact_jid, true); added = added + 1; end -- Remove contacts and notify connected clients for contact_jid in removed_contacts do module:log("debug", "Processing removed friend of %s: %s", username, contact_jid); roster[contact_jid] = nil; roster_push(username, roster, contact_jid); send_presence(username, contact_jid, false); removed = removed + 1; end module:log("debug", "User %s: added %d new contacts, removed %d contacts", username, added, removed); if cb ~= nil then cb(true); end end); end -- Fetch the named user's roster from the API, call callback (cb) -- with status and result (friends list) when received. function fetch_roster(username, cb) local x = {headers = {}}; x["headers"]["ACCEPT"] = "application/json, text/plain, */*"; module:log("debug", "Fetching roster at URL: %s", roster_url:format(username)); local ok, err = http.request( roster_url:format(username), x, function (roster_data, code) if code ~= 200 then module:log("error", "Error fetching roster from %s (code %d): %s", roster_url:format(username), code, tostring(roster_data):sub(1, 40):match("^[^\r\n]+")); if code ~= 0 then cb(nil, code, roster_data); end return; end module:log("debug", "Successfully fetched roster for %s", username); module:log("debug", "The roster data is %s", roster_data); cb(true, code, json.decode(roster_data)); end ); if not ok then module:log("error", "Failed to connect to roster API at %s: %s", roster_url:format(username), err); cb(false, 0, err); end end -- Fetch the named user's roster from the API, synchronize it with -- the user's current roster. Notify callback (cb) with true/false -- depending on success or failure. function refresh_roster(username, cb) local user = sessions[username]; if not (user and user.roster) then module:log("debug", "User's (%q) roster updated, but they are not online - ignoring", username); cb(true); return; end fetch_roster(username, updated_friends_handler(username, cb)); end --- Roster protocol handling --- -- Build a reply to a "roster get" request local function build_roster_reply(stanza, roster_data) local roster = st.reply(stanza) :tag("query", { xmlns = "jabber:iq:roster" }); for jid, item in pairs(roster_data) do if jid and jid ~= "pending" then roster:tag("item", { jid = jid, subscription = item.subscription, ask = item.ask, name = item.name, }); for group in pairs(item.groups) do roster:tag("group"):text(group):up(); end roster:up(); -- move out from item end end return roster; end -- Handle clients requesting their roster (generally at login) -- This will not work if mod_roster is loaded (in 0.9). module:hook("iq-get/self/jabber:iq:roster:query", function(event) local session, stanza = event.origin, event.stanza; session.interested = true; -- resource is interested in roster updates local roster = session.roster; if roster[false].downloaded then return session.send(build_roster_reply(stanza, roster)); end -- It's possible that we can call this more than once for a new roster -- Should happen rarely (multiple clients of the same user request the -- roster in the time it takes the API to respond). Currently we just -- issue multiple requests, as it's harmless apart from the wasted -- requests. fetch_roster(session.username, function (ok, code, friends) if not ok then session.send(st.error_reply(stanza, "cancel", "internal-server-error")); session:close("internal-server-error"); return; end -- Are we the first callback to handle the downloaded roster? local first = roster[false].downloaded == nil; if first then -- Fill out new roster for jid, friend in pairs(friends) do roster[jid] = friend_to_roster_item(friend); end end roster[false].downloaded = true; -- Send full roster to client session.send(build_roster_reply(stanza, roster)); if not first then -- We already had a roster, make sure to handle any changes... updated_friends_handler(session.username, nil)(ok, code, friends); end end); return true; end); -- Prevent client from making changes to the roster. This will not -- work if mod_roster is loaded (in 0.9). module:hook("iq-set/self/jabber:iq:roster:query", function(event) local session, stanza = event.origin, event.stanza; return session.send(st.error_reply(stanza, "cancel", "service-unavailable")); end); --- HTTP endpoint to trigger roster refresh --- -- Handles updating for a single user: GET /roster_admin/refresh/USERNAME function handle_refresh_single(event, username) refresh_roster(username, function (ok, code, err) event.response.headers["Content-Type"] = "application/json"; event.response:send(json.encode({ status = ok and "ok" or "error"; message = err or "roster update complete"; })); end); return true; end -- Handles updating for multiple users: POST /roster_admin/refresh -- Payload should be a JSON array of usernames, e.g. ["user1", "user2", "user3"] function handle_refresh_multi(event) local users = json.decode(event.request.body); if not users then module:log("warn", "Multi-user refresh attempted with missing/invalid payload"); event.response:send(400); return true; end local count, count_err = 0, 0; local function cb(ok) count = count + 1; if not ok then count_err = count_err + 1; end if count == #users then event.response.headers["Content-Type"] = "application/json"; event.response:send(json.encode({ status = "ok"; message = "roster update complete"; updated = count - count_err; errors = count_err; })); end end for _, username in ipairs(users) do refresh_roster(username, cb); end return true; end module:provides("http", { route = { ["POST /refresh"] = handle_refresh_multi; ["GET /refresh/*"] = handle_refresh_single; }; }); prosody-modules-c53cc1ae4788/mod_measure_malloc/0002755000175500017550000000000013164216767021443 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_measure_malloc/mod_measure_malloc.lua0000644000175500017550000000057513163400720025761 0ustar debacledebaclemodule:set_global(); local measure = require"core.statsmanager".measure; local pposix = require"util.pposix"; local measures = {}; setmetatable(measures, { __index = function (t, k) local m = measure("sizes", "memory."..k); t[k] = m; return m; end }); module:hook("stats-update", function () local m = measures; for k, v in pairs(pposix.meminfo()) do m[k](v); end end); prosody-modules-c53cc1ae4788/mod_measure_malloc/README.markdown0000644000175500017550000000025213163400720024120 0ustar debacledebacle--- labels: summary: Report malloc() stats ... Description =========== This module collects stats from `util.pposix.meminfo` usage and reports using Prosody 0.10 APIs prosody-modules-c53cc1ae4788/mod_storage_muconference_readonly/0002755000175500017550000000000013164216767024545 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_storage_muconference_readonly/mod_storage_muconference_readonly.lua0000644000175500017550000001025613163400720034162 0ustar debacledebacle -- luacheck: ignore 212/self local sql = require "util.sql"; local xml_parse = require "util.xml".parse; local resolve_relative_path = require "util.paths".resolve_relative_path; local stanza_preserialize = require "util.stanza".preserialize; local unpack = unpack local function iterator(result) return function(result_) local row = result_(); if row ~= nil then return unpack(row); end end, result, nil; end local default_params = { driver = "MySQL" }; local engine; local host = module.host; local room, store; local function get_best_affiliation(a, b) if a == 'owner' or b == 'owner' then return 'owner'; elseif a == 'administrator' or b == 'administrator' then return 'administrator'; elseif a == 'outcast' or b == 'outcast' then return 'outcast'; elseif a == 'member' or b == 'member' then return 'member'; end assert(false); end local function keyval_store_get() if store == "config" then local room_jid = room.."@"..host; local result; for row in engine:select("SELECT `name`,`desc`,`topic`,`public`,`secret` FROM `rooms` WHERE `jid`=? LIMIT 1", room_jid or "") do result = row end local name = result[1]; local desc = result[2]; local subject = result[3]; local public = result[4]; local hidden = public == 0 and true or nil; local secret = result[5]; if secret == '' then secret = nil end local affiliations = {}; for row in engine:select("SELECT `jid_user`,`affil` FROM `rooms_lists` WHERE `jid_room`=?", room_jid or "") do local jid_user = row[1]; local affil = row[2]; -- mu-conference has a bug where full JIDs get stored… local bare_jid = jid_user:gsub('/.*', ''); local old_affil = affiliations[bare_jid]; -- mu-conference has a bug where it can record multiple affiliations… if old_affil ~= nil and old_affil ~= affil then affil = get_best_affiliation(old_affil, affil); end -- terminology is clearly “admin”, not “administrator”. if affil == 'administrator' then affil = 'admin'; end affiliations[bare_jid] = affil; end return { jid = room_jid, _data = { persistent = true, name = name, description = desc, subject = subject, password = secret, hidden = hidden, }, _affiliations = affiliations, }; end end --- Key/value store API (default store type) local keyval_store = {}; keyval_store.__index = keyval_store; function keyval_store:get(roomname) room, store = roomname, self.store; local ok, result = engine:transaction(keyval_store_get); if not ok then module:log("error", "Unable to read from database %s store for %s: %s", store, roomname or "", result); return nil, result; end return result; end function keyval_store:users() local host_length = host:len() + 1; local ok, result = engine:transaction(function() return engine:select("SELECT SUBSTRING_INDEX(jid, '@', 1) FROM `rooms`"); end); if not ok then return ok, result end return iterator(result); end local stores = { keyval = keyval_store; }; --- Implement storage driver API -- FIXME: Some of these operations need to operate on the archive store(s) too local driver = {}; function driver:open(store, typ) local store_mt = stores[typ or "keyval"]; if store_mt then return setmetatable({ store = store }, store_mt); end return nil, "unsupported-store"; end function driver:stores(roomname) local query = "SELECT 'config'"; if roomname == true or not roomname then roomname = ""; end local ok, result = engine:transaction(function() return engine:select(query, host, roomname); end); if not ok then return ok, result end return iterator(result); end --- Initialization local function normalize_params(params) assert(params.driver and params.database, "Configuration error: Both the SQL driver and the database need to be specified"); return params; end function module.load() if prosody.prosodyctl then return; end local engines = module:shared("/*/sql/connections"); local params = normalize_params(module:get_option("sql", default_params)); engine = engines[sql.db2uri(params)]; if not engine then module:log("debug", "Creating new engine"); engine = sql:create_engine(params); engines[sql.db2uri(params)] = engine; end module:provides("storage", driver); end prosody-modules-c53cc1ae4788/mod_storage_muconference_readonly/README.markdown0000644000175500017550000000210513163400720027221 0ustar debacledebacle--- labels: - 'Type-Storage' - 'Stage-Alpha' summary: MU-Conference SQL Read-only Storage Module ... Introduction ============ This is a storage backend using MU-Conference’s SQL storage. It depends on [LuaDBI][doc:depends#luadbi] This module only works in read-only, and was made to be used by [mod_migrate] to migrate from MU-Conference’s SQL storage. You may need to convert your 'rooms' and 'rooms\_lists' tables to utf8mb4 before running that script, in order not to end up with mojibake. Note that MySQL doesn’t support having more than 191 characters in the jid field in this case, so you may have to change the table schema as well. Configuration ============= Copy the module to the prosody modules/plugins directory. In Prosody's configuration file, set: storage = "muconference_readonly" MUConferenceSQL options are the same as the [SQL ones][doc:modules:mod_storage_sql#usage]. Compatibility ============= ------- --------------------------- trunk Works 0.10 Untested, but should work 0.9 Does not work ------- --------------------------- prosody-modules-c53cc1ae4788/mod_auth_ccert/0002755000175500017550000000000013164216767020574 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_auth_ccert/mod_auth_ccert.lua0000644000175500017550000000513313163400720024236 0ustar debacledebacle-- Copyright (C) 2013 Kim Alvefur -- -- This file is MIT/X11 licensed. local jid_compare = require "util.jid".compare; local jid_split = require "util.jid".prepped_split; local new_sasl = require "util.sasl".new; local now = os.time; local log = module._log; local subject_alternative_name = "2.5.29.17"; local id_on_xmppAddr = "1.3.6.1.5.5.7.8.5"; local oid_emailAddress = "1.2.840.113549.1.9.1"; local cert_match = module:get_option("certificate_match", "xmppaddr"); local username_extractor = {}; function username_extractor.xmppaddr(cert, authz, session) local extensions = cert:extensions(); local SANs = extensions[subject_alternative_name]; local xmppAddrs = SANs and SANs[id_on_xmppAddr]; if not xmppAddrs then (session.log or log)("warn", "Client certificate contains no xmppAddrs"); return nil, false; end for i=1,#xmppAddrs do if authz == "" or jid_compare(authz, xmppAddrs[i]) then (session.log or log)("debug", "xmppAddrs[%d] %q matches authz %q", i, xmppAddrs[i], authz) local username, host = jid_split(xmppAddrs[i]); if host == module.host then return username, true end end end end function username_extractor.email(cert) local subject = cert:subject(); for i=1,#subject do local ava = subject[i]; if ava.oid == oid_emailAddress then local username, host = jid_split(ava.value); if host == module.host then return username, true end end end end local find_username = username_extractor[cert_match]; if not find_username then module:log("error", "certificate_match = %q is not supported"); return end function get_sasl_handler(session) return new_sasl(module.host, { external = session.secure and function(authz) if not session.secure then -- getpeercertificate() on a TCP connection would be bad, abort! (session.log or log)("error", "How did you manage to select EXTERNAL without TLS?"); return nil, false; end local sock = session.conn:socket(); local cert = sock:getpeercertificate(); if not cert then (session.log or log)("warn", "No certificate provided"); return nil, false; end if not cert:validat(now()) then (session.log or log)("warn", "Client certificate expired") return nil, "expired"; end local chain_valid, chain_errors = sock:getpeerverification(); if not chain_valid then (session.log or log)("warn", "Invalid client certificate chain"); for i, error in ipairs(chain_errors) do (session.log or log)("warn", "%d: %s", i, table.concat(error, ", ")); end return nil, false; end return find_username(cert, authz, session); end }); end module:provides "auth"; prosody-modules-c53cc1ae4788/mod_auth_ccert/README.markdown0000644000175500017550000000135213163400720023253 0ustar debacledebacle--- labels: - 'Stage-Alpha' - 'Type-Auth' summary: Client Certificate authentication module ... Introduction ============ This module implements PKI-style client certificate authentication. You will therefore need your own Certificate Authority. How to set that up is beyond the current scope of this document. Configuration ============= authentication = "ccert" certificate_match = "xmppaddr" -- or "email" c2s_ssl = { cafile = "/path/to/your/ca.pem"; capath = false; -- Disable capath inherited from built-in default } Compatibility ============= ----------------- -------------- trunk Works 0.10 and later Works 0.9 and earlier Doesn't work ----------------- -------------- prosody-modules-c53cc1ae4788/mod_csi_pump/0002755000175500017550000000000013164216767020272 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_csi_pump/README.markdown0000644000175500017550000000072713163400720022756 0ustar debacledebacle--- description: Lossless CSI module labels: - 'Stage-Alpha' --- Stanzas are queued in a buffer until either an "important" stanza is encountered or the buffer becomes full. Then all queued stanzas are sent at the same time. This way, nothing is lost or reordered while still allowing for power usage savings by not requiring mobile clients to bring up their radio for unimportant stanzas. Use with other CSI plugins such as [mod_throttle_presence] is *not* supported. prosody-modules-c53cc1ae4788/mod_csi_pump/mod_csi_pump.lua0000644000175500017550000000431413163400720023432 0ustar debacledebacle-- Copyright (C) 2016 Kim Alvefur -- module:depends"csi" module:depends"track_muc_joins" local jid = require "util.jid"; local new_queue = require "util.queue".new; local function new_pump(output, ...) -- luacheck: ignore 212/self local q = new_queue(...); local flush = true; function q:pause() flush = false; end function q:resume() flush = true; return q:flush(); end local push = q.push; function q:push(item) local ok = push(self, item); if not ok then q:flush(); output(item, self); elseif flush then return q:flush(); end return true; end function q:flush() local item = self:pop(); while item do output(item, self); item = self:pop(); end return true; end return q; end -- TODO delay stamps -- local dt = require "util.datetime"; local function is_important(stanza, session) local st_name = stanza.name; if not st_name then return false; end local st_type = stanza.attr.type; if st_name == "presence" then -- TODO check for MUC status codes? if st_type == nil or st_type == "unavailable" then return false; end return true; elseif st_name == "message" then if st_type == "headline" then return false; end local body = stanza:get_child_text("body"); if st_type == "groupchat" then if stanza:get_child_text("subject") then return true; end if not body then return false; end if body:find(session.username, 1, true) then return true; end local rooms = session.rooms_joined; if not rooms then return false; end local room_nick = rooms[jid.bare(stanza.attr.from)]; if room_nick and body:find(room_nick, 1, true) then return true; end return false; end return body; end return true; end module:hook("csi-client-inactive", function (event) local session = event.origin; if session.pump then session.pump:pause(); else session._orig_send = session.send; local pump = new_pump(session.send, 100); pump:pause(); session.pump = pump; function session.send(stanza) pump:push(stanza); if is_important(stanza, session) then pump:flush(); end return true; end end end); module:hook("csi-client-active", function (event) local session = event.origin; if session.pump then session.pump:resume(); end end); prosody-modules-c53cc1ae4788/mod_munin/0002755000175500017550000000000013164216767017601 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_munin/mod_munin.lua0000644000175500017550000000707213163400720022254 0ustar debacledebaclemodule:set_global(); local s_format = string.format; local array = require"util.array"; local it = require"util.iterators"; local mt = require"util.multitable"; local meta = mt.new(); meta.data = module:shared"meta"; local data = mt.new(); data.data = module:shared"data"; local munin_listener = {}; local munin_commands = {}; local node_name = module:get_option_string("munin_node_name", "localhost"); local ignore_stats = module:get_option_set("munin_ignored_stats", { }); local function clean_fieldname(name) return (name:gsub("[^A-Za-z0-9_]", "_"):gsub("^[^A-Za-z_]", "_%1")); end function munin_listener.onconnect(conn) -- require"core.statsmanager".collect(); conn:write("# munin node at "..node_name.."\n"); end function munin_listener.onincoming(conn, line) line = line and line:match("^[^\r\n]+"); if type(line) ~= "string" then return end -- module:log("debug", "incoming: %q", line); local command = line:match"^%w+"; command = munin_commands[command]; if not command then conn:write("# Unknown command.\n"); return; end local ok, err = pcall(command, conn, line); if not ok then module:log("error", "Error running %q: %s", line, err); conn:close(); end end function munin_listener.ondisconnect() end function munin_commands.cap(conn) conn:write("cap\n"); end function munin_commands.list(conn) conn:write(array(it.keys(data.data)):concat(" ") .. "\n"); end function munin_commands.config(conn, line) -- TODO what exactly? local stat = line:match("%s(%S+)"); if not stat then conn:write("# Unknown service\n.\n"); return end for _, _, k, value in meta:iter(stat, "", nil) do conn:write(s_format("%s %s\n", k, value)); end for _, name, k, value in meta:iter(stat, nil, nil) do if name ~= "" and not ignore_stats:contains(name) then conn:write(s_format("%s.%s %s\n", name, k, value)); end end conn:write(".\n"); end function munin_commands.fetch(conn, line) local stat = line:match("%s(%S+)"); if not stat then conn:write("# Unknown service\n.\n"); return end for _, name, value in data:iter(stat, nil) do if not ignore_stats:contains(name) then conn:write(s_format("%s.value %.12f\n", name, value)); end end conn:write(".\n"); end function munin_commands.quit(conn) conn:close(); end module:hook("stats-updated", function (event) local all_stats, this = event.stats_extra; local host, sect, name, typ, key; for stat, value in pairs(event.changed_stats) do if not ignore_stats:contains(stat) then this = all_stats[stat]; -- module:log("debug", "changed_stats[%q] = %s", stat, tostring(value)); host, sect, name, typ = stat:match("^/([^/]+)/([^/]+)/(.+):(%a+)$"); if host == nil then sect, name, typ, host = stat:match("^([^.]+)%.([^:]+):(%a+)$"); elseif host == "*" then host = nil; end if sect:find("^mod_measure_.") then sect = sect:sub(13); elseif sect:find("^mod_statistics_.") then sect = sect:sub(16); end key = clean_fieldname(s_format("%s_%s_%s", host or "global", sect, typ)); if not meta:get(key) then if host then meta:set(key, "", "graph_title", s_format("%s %s on %s", sect, typ, host)); else meta:set(key, "", "graph_title", s_format("Global %s %s", sect, typ, host)); end meta:set(key, "", "graph_vlabel", this and this.units or typ); meta:set(key, "", "graph_category", sect); meta:set(key, name, "label", name); elseif not meta:get(key, name, "label") then meta:set(key, name, "label", name); end data:set(key, name, value); end end end); module:provides("net", { listener = munin_listener; default_mode = "*l"; default_port = 4949; }); prosody-modules-c53cc1ae4788/mod_munin/README.markdown0000644000175500017550000000445613163400720022270 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: Implementation of the Munin node protocol ... Summary ======= This module implements the Munin reporting protocol, allowing you to collect statistics directly from Prosody into Munin. Configuration ============= There is only one recommended option, `munin_node_name`, which specifies the name that Prosody will identify itself by to the Munin server. You may want to set this to the same hostname as in the [SRV record][doc:dns] for the machine. ```lua modules_enabled = { -- your other modules "munin", } munin_node_name = "xmpp.example.com" ``` You will also want to enable statistics collection by setting: ```lua statistics_interval = 300 -- every 5 minutes, same as munin ``` ## Summary All these must be in [the global section][doc:configure#overview]. Option Type Default ----------------------- -------- --------------------------- munin\_node\_name string `"localhost"` munin\_ignored\_stats set `{ }` munin\_ports set `{ 4949 }` munin\_interfaces set `{ "0.0.0.0", "::" }`[^1] [^1]: Varies depending on availability of IPv4 and IPv6 ## Ports and interfaces `mod_munin` listens on port `4949` on all local interfaces by default. This can be changed with the standard [port and network configuration][doc:ports]: ``` lua -- defaults: munin_ports = { 4949 } munin_interfaces = { "::", "0.0.0.0" } ``` If you already have a `munin-node` instance running, you can set a different port to avoid the conflict. ## Configuring Munin Simply add `munin_node_name` surrounded by brackets to `/etc/munin/munin.conf`: ``` ini [xmpp.example.com] address xmpp.example.com port 4949 ``` You can leave out `address` if it equal to the name in brackets, and leave out the `port` if it is the default (`4949`). Setting `address` to an IP address may sometimes be useful as the Munin collection server is not delayed by DNS lookups in case of network issues. If you set a different port, or if the hostname to connect to is different from this hostname, make sure to add `port` and/or `address` options. See [Munin documentation][muninconf] for more information. Compatibility ============= **Requires** Prosody 0.10 or above [muninconf]: http://guide.munin-monitoring.org/en/stable-2.0/reference/munin.conf.html prosody-modules-c53cc1ae4788/mod_admin_web/0002755000175500017550000000000013164216767020400 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_admin_web/README.markdown0000644000175500017550000000263313163400720023062 0ustar debacledebacle--- labels: - 'Stage-Beta' summary: Web administration interface ... Introduction ============ This module provides a basic web administration interface. It currently gives you access to Ad-Hoc commands on any virtual host or component that you are set as an administrator for in the Prosody config file. It also provides a live list of all S2S and C2S connections. Installation ============ 1. Copy the admin\_web directory into a directory Prosody will check for plugins. (cf. [Installing modules](http://prosody.im/doc/installing_modules)) 2. Execute the contained get\_deps.sh script from within the admin\_web directory. (Requires wget, tar, and a basic shell) Configuration Details ===================== "admin\_web" needs to be added to the modules\_enabled table of the host you want to load this module on. By default the interface will then be reachable under `http://example.com:5280/admin`, or `https://example.com:5281/admin`. The module will automatically enable two other modules if they aren't already: mod\_bosh (used to connect to the server from the web), and mod\_admin\_adhoc (which provides admin commands over XMPP). VirtualHost "example.com" modules_enabled = { ..... "admin_web"; ..... } Compatibility ============= --------- --------------- trunk Works 0.9 Works \<= 0.8 Not supported --------- --------------- prosody-modules-c53cc1ae4788/mod_admin_web/admin_web/0002755000175500017550000000000013164216767022325 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_admin_web/admin_web/mod_admin_web.lua0000644000175500017550000002234313163400720025575 0ustar debacledebacle-- Copyright (C) 2010 Florian Zeitz -- -- This file is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- -- -- -- -- -- -- -- / -- -- -- / -- local st = require "util.stanza"; local uuid_generate = require "util.uuid".generate; local is_admin = require "core.usermanager".is_admin; local pubsub = require "util.pubsub"; local jid_bare = require "util.jid".bare; local hosts = prosody.hosts; local incoming_s2s = prosody.incoming_s2s; module:set_global(); local service = {}; 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 idmap = {}; local function add_client(session, host) local name = session.full_jid; local id = idmap[name]; if not id then id = uuid_generate(); idmap[name] = id; end local item = st.stanza("item", { id = id }):tag("session", {xmlns = xmlns_c2s_session, jid = name}):up(); if session.secure then local encrypted = item:tag("encrypted"); local sock = session.conn and session.conn.socket and session.conn:socket() local info = sock and sock.info and sock:info(); for k, v in pairs(info or {}) do encrypted:tag("info", { name = k }):text(tostring(v)):up(); end end if session.compressed then item:tag("compressed"):up(); end service[host]:publish(xmlns_c2s_session, host, id, item); module:log("debug", "Added client " .. name); end local function del_client(session, host) local name = session.full_jid; local id = idmap[name]; if id then local notifier = st.stanza("retract", { id = id }); service[host]:retract(xmlns_c2s_session, host, id, notifier); end end local function add_host(session, type, host) local name = (type == "out" and session.to_host) or (type == "in" and session.from_host); local id = idmap[name.."_"..type]; if not id then id = uuid_generate(); idmap[name.."_"..type] = id; end local item = st.stanza("item", { id = id }):tag("session", {xmlns = xmlns_s2s_session, jid = name}) :tag(type):up(); if session.secure then local encrypted = item:tag("encrypted"); local sock = session.conn and session.conn.socket and session.conn:socket() local info = sock and sock.info and sock:info(); for k, v in pairs(info or {}) do encrypted:tag("info", { name = k }):text(tostring(v)):up(); end if session.cert_identity_status == "valid" then encrypted:tag("valid"); else encrypted:tag("invalid"); end end if session.compressed then item:tag("compressed"):up(); end service[host]:publish(xmlns_s2s_session, host, id, item); module:log("debug", "Added host " .. name .. " s2s" .. type); end local function del_host(session, type, host) local name = (type == "out" and session.to_host) or (type == "in" and session.from_host); local id = idmap[name.."_"..type]; if id then local notifier = st.stanza("retract", { id = id }); service[host]:retract(xmlns_s2s_session, host, id, notifier); end end local function get_affiliation(jid, host) local bare_jid = jid_bare(jid); if is_admin(bare_jid, host) then return "member"; else return "none"; end 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", { name = "admin"; route = { ["GET"] = function(event) event.response.headers.location = event.request.path .. "/"; return 301; end; ["GET /*"] = serve_file; } }); -- Setup adminsub service 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(kind, { node = node }) :add_child(item); for jid in pairs(jids) do module:log("debug", "Sending notification to %s", jid); message.attr.to = jid; module:send(message); end end service[module.host] = pubsub.new({ broadcaster = simple_broadcast; normalize_jid = jid_bare; get_affiliation = function(jid) return get_affiliation(jid, module.host) end; capabilities = { member = { create = false; publish = false; retract = false; get_nodes = true; subscribe = true; unsubscribe = true; get_subscription = true; get_subscriptions = true; get_items = true; subscribe_other = false; unsubscribe_other = false; get_subscription_other = false; get_subscriptions_other = false; be_subscribed = true; be_unsubscribed = true; set_affiliation = false; }; owner = { create = true; publish = true; retract = true; get_nodes = true; subscribe = true; unsubscribe = true; get_subscription = true; get_subscriptions = true; get_items = true; subscribe_other = true; unsubscribe_other = true; get_subscription_other = true; get_subscriptions_other = true; be_subscribed = true; be_unsubscribed = true; set_affiliation = true; }; }; }); -- Create node for s2s sessions local ok, err = service[module.host]:create(xmlns_s2s_session, true); if not ok then module:log("warn", "Could not create node " .. xmlns_s2s_session .. ": " .. tostring(err)); else service[module.host]:set_affiliation(xmlns_s2s_session, true, module.host, "owner") end -- Add outgoing s2s sessions for _, session in pairs(hosts[module.host].s2sout) do if session.type ~= "s2sout_unauthed" then add_host(session, "out", module.host); end end -- Add incomming s2s sessions for session in pairs(incoming_s2s) do if session.to_host == module.host then add_host(session, "in", module.host); end end -- Create node for c2s sessions ok, err = service[module.host]:create(xmlns_c2s_session, true); if not ok then module:log("warn", "Could not create node " .. xmlns_c2s_session .. ": " .. tostring(err)); else service[module.host]:set_affiliation(xmlns_c2s_session, true, module.host, "owner") end -- Add c2s sessions for _, user in pairs(hosts[module.host].sessions or {}) do for _, session in pairs(user.sessions or {}) do add_client(session, module.host); end end -- Register adminsub handler module:hook("iq/host/http://prosody.im/adminsub:adminsub", function(event) local origin, stanza = event.origin, event.stanza; local adminsub = stanza.tags[1]; local action = adminsub.tags[1]; local reply; if action.name == "subscribe" then local ok, ret = service[module.host]:add_subscription(action.attr.node, stanza.attr.from, stanza.attr.from); if ok then reply = st.reply(stanza) :tag("adminsub", { xmlns = xmlns_adminsub }); else reply = st.error_reply(stanza, "cancel", ret); end elseif action.name == "unsubscribe" then local ok, ret = service[module.host]:remove_subscription(action.attr.node, stanza.attr.from, stanza.attr.from); if ok then reply = st.reply(stanza) :tag("adminsub", { xmlns = xmlns_adminsub }); else reply = st.error_reply(stanza, "cancel", ret); end elseif action.name == "items" then local node = action.attr.node; local ok, ret = service[module.host]:get_items(node, stanza.attr.from); if not ok then origin.send(st.error_reply(stanza, "cancel", ret)); return true; end local data = st.stanza("items", { node = node }); for _, entry in pairs(ret) do data:add_child(entry); end if data then reply = st.reply(stanza) :tag("adminsub", { xmlns = xmlns_adminsub }) :add_child(data); else reply = st.error_reply(stanza, "cancel", "item-not-found"); end elseif action.name == "adminfor" then local data = st.stanza("adminfor"); for host_name in pairs(hosts) do if is_admin(stanza.attr.from, host_name) then data:tag("item"):text(host_name):up(); end end reply = st.reply(stanza) :tag("adminsub", { xmlns = xmlns_adminsub }) :add_child(data); else reply = st.error_reply(stanza, "feature-not-implemented"); end origin.send(reply); return true; end); -- Add/remove c2s sessions module:hook("resource-bind", function(event) add_client(event.session, module.host); end); module:hook("resource-unbind", function(event) del_client(event.session, module.host); service[module.host]:remove_subscription(xmlns_c2s_session, module.host, event.session.full_jid); service[module.host]:remove_subscription(xmlns_s2s_session, module.host, event.session.full_jid); end); -- Add/remove s2s sessions module:hook("s2sout-established", function(event) add_host(event.session, "out", module.host); end); module:hook("s2sin-established", function(event) add_host(event.session, "in", module.host); end); module:hook("s2sout-destroyed", function(event) del_host(event.session, "out", module.host); end); module:hook("s2sin-destroyed", function(event) del_host(event.session, "in", module.host); end); end prosody-modules-c53cc1ae4788/mod_admin_web/admin_web/get_deps.sh0000755000175500017550000000155313163400720024437 0ustar debacledebacle#!/bin/sh JQUERY_VERSION="1.10.2" STROPHE_VERSION="1.1.2" BOOTSTRAP_VERSION="1.4.0" ADHOC_COMMITISH="87bfedccdb91e2ff7cfb165e989e5259c155b513" cd www_files/js rm -f jquery-$JQUERY_VERSION.min.js wget http://code.jquery.com/jquery-$JQUERY_VERSION.min.js || exit 1 rm -f adhoc.js wget -O adhoc.js "http://git.babelmonkeys.de/?p=adhocweb.git;a=blob_plain;f=js/adhoc.js;hb=$ADHOC_COMMITISH" || exit 1 rm -f strophe.min.js wget https://raw.github.com/strophe/strophe.im/gh-pages/strophejs/downloads/strophejs-$STROPHE_VERSION.tar.gz && tar xzf strophejs-$STROPHE_VERSION.tar.gz strophejs-$STROPHE_VERSION/strophe.min.js --strip-components=1 && rm strophejs-$STROPHE_VERSION.tar.gz || exit 1 cd ../css rm -f bootstrap-$BOOTSTRAP_VERSION.min.css wget https://raw.github.com/twbs/bootstrap/v$BOOTSTRAP_VERSION/bootstrap.min.css -O bootstrap-$BOOTSTRAP_VERSION.min.css || exit 1 prosody-modules-c53cc1ae4788/mod_admin_web/admin_web/www_files/0002755000175500017550000000000013164216767024333 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_admin_web/admin_web/www_files/index.html0000644000175500017550000000465713163400720026321 0ustar debacledebacle Prosody Webadmin

Incoming S2S connections:

    Outgoing S2S connections:

      Client connections:

        prosody-modules-c53cc1ae4788/mod_admin_web/admin_web/www_files/js/0002755000175500017550000000000013164216767024747 5ustar debacledebacleprosody-modules-c53cc1ae4788/mod_admin_web/admin_web/www_files/js/main.js0000644000175500017550000002442013163400720026210 0ustar debacledebaclevar BOSH_SERVICE = '/http-bind/'; var show_log = false; Strophe.addNamespace('C2SSTREAM', 'http://prosody.im/streams/c2s'); Strophe.addNamespace('S2SSTREAM', 'http://prosody.im/streams/s2s'); Strophe.addNamespace('ADMINSUB', 'http://prosody.im/adminsub'); Strophe.addNamespace('CAPS', 'http://jabber.org/protocol/caps'); var localJID = null; var connection = null; var adminsubHost = null; var adhocControl = new Adhoc('#adhocDisplay', function() {}); function log(msg) { var entry = $('
        ').append(document.createTextNode(msg)); $('#log').append(entry); } function rawInput(data) { log('RECV: ' + data); } function rawOutput(data) { log('SENT: ' + data); } function _cbNewS2S(e) { var items, item, entry, tmp, retract, id, jid, infos, info, metadata; items = e.getElementsByTagName('item'); for (i = 0; i < items.length; i++) { item = items[i]; id = item.attributes.getNamedItem('id').value; jid = item.getElementsByTagName('session')[0].attributes.getNamedItem('jid').value; infos = item.getElementsByTagName('info'); entry = $('
      • ' + jid + '
      • '); if (tmp = item.getElementsByTagName('encrypted')[0]) { if (tmp.getElementsByTagName('valid')[0]) { entry.append(' (secure) (encrypted)'); } else { entry.append(' (encrypted)'); } } if (item.getElementsByTagName('compressed')[0]) { entry.append(' (compressed)'); } metadata = $('