# HG changeset patch # User Matthew Wild # Date 1343395799 -3600 # Node ID 9d5731af2c27fd441884ce60ba290a1c286d5480 # Parent 9bbd99f2057aae05f6c43109bf8eb2fc3a84d0e0# Parent 3c37445f26ac8ba7ab75d9da6d3b08964a016bcd Merge with Oliver Gerlich diff -r 9bbd99f2057a -r 9d5731af2c27 mod_adhoc_cmd_admin/mod_adhoc_cmd_admin.lua --- a/mod_adhoc_cmd_admin/mod_adhoc_cmd_admin.lua Mon Jun 11 22:32:45 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,512 +0,0 @@ --- Copyright (C) 2009-2010 Florian Zeitz --- --- This file is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -local _G = _G; - -local prosody = _G.prosody; -local hosts = prosody.hosts; - -local t_concat = table.concat; - -local usermanager_user_exists = require "core.usermanager".user_exists; -local usermanager_create_user = require "core.usermanager".create_user; -local usermanager_get_password = require "core.usermanager".get_password; -local usermanager_set_password = require "core.usermanager".set_password or - function (username, password, host) return usermanager_create_user(username, password, host) end; -local is_admin = require "core.usermanager".is_admin; - -local rm_load_roster = require "core.rostermanager".load_roster; - -local st, jid, uuid = require "util.stanza", require "util.jid", require "util.uuid"; -local timer_add_task = require "util.timer".add_task; -local dataforms_new = require "util.dataforms".new; -module:log("debug", module:get_name()); -local adhoc_new = module:require "adhoc".new; - -local add_user_layout = dataforms_new{ - title = "Adding a User"; - instructions = "Fill out this form to add a user."; - - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for the account to be added" }; - { name = "password", type = "text-private", label = "The password for this account" }; - { name = "password-verify", type = "text-private", label = "Retype password" }; -}; - -local change_user_password_layout = dataforms_new{ - title = "Changing a User Password"; - instructions = "Fill out this form to change a user's password."; - - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for this account" }; - { name = "password", type = "text-private", required = true, label = "The password for this account" }; -}; - -local delete_user_layout = dataforms_new{ - title = "Deleting a User"; - instructions = "Fill out this form to delete a user."; - - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) to delete" }; -}; - -local end_user_session_layout = dataforms_new{ - title = "Ending a User Session"; - instructions = "Fill out this form to end a user's session."; - - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) for which to end sessions" }; -}; - -local get_user_password_layout = dataforms_new{ - title = "Getting User's Password"; - instructions = "Fill out this form to get a user's password."; - - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for which to retrieve the password" }; -}; - -local get_user_password_result_layout = dataforms_new{ - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "accountjid", type = "jid-single", label = "JID" }; - { name = "password", type = "text-single", label = "Password" }; -}; - -local get_user_roster_layout = dataforms_new{ - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for which to retrieve the roster" }; -}; - -local get_user_roster_result_layout = dataforms_new{ - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "accountjid", type = "jid-single", label = "This is the roster for" }; - { name = "roster", type = "text-multi", label = "Roster XML" }; -}; - -local get_user_stats_layout = dataforms_new{ - title = "Get User Statistics"; - instructions = "Fill out this form to gather user statistics."; - - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for statistics" }; -}; - -local get_user_stats_result_layout = dataforms_new{ - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "ipaddresses", type = "text-multi", label = "IP Addresses" }; - { name = "rostersize", type = "text-single", label = "Roster size" }; - { name = "onlineresources", type = "text-multi", label = "Online Resources" }; -}; - -local get_online_users_layout = dataforms_new{ - title = "Getting List of Online Users"; - instructions = "How many users should be returned at most?"; - - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "max_items", type = "list-single", label = "Maximum number of users", - value = { "25", "50", "75", "100", "150", "200", "all" } }; - { name = "details", type = "boolean", label = "Show details" }; -}; - -local get_online_users_result_layout = dataforms_new{ - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "onlineuserjids", type = "text-multi", label = "The list of all online users" }; -}; - -local announce_layout = dataforms_new{ - title = "Making an Announcement"; - instructions = "Fill out this form to make an announcement to all\nactive users of this service."; - - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "subject", type = "text-single", label = "Subject" }; - { name = "announcement", type = "text-multi", required = true, label = "Announcement" }; -}; - -local shut_down_service_layout = dataforms_new{ - title = "Shutting Down the Service"; - instructions = "Fill out this form to shut down the service."; - - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; - { name = "delay", type = "list-single", label = "Time delay before shutting down", - value = { {label = "30 seconds", value = "30"}, - {label = "60 seconds", value = "60"}, - {label = "90 seconds", value = "90"}, - {label = "2 minutes", value = "120"}, - {label = "3 minutes", value = "180"}, - {label = "4 minutes", value = "240"}, - {label = "5 minutes", value = "300"}, - }; - }; - { name = "announcement", type = "text-multi", label = "Announcement" }; -}; - -function add_user_command_handler(self, data, state) - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - local fields = add_user_layout:data(data.form); - if not fields.accountjid then - return { status = "completed", error = { message = "You need to specify a JID." } }; - end - local username, host, resource = jid.split(fields.accountjid); - if data.to ~= host then - return { status = "completed", error = { message = "Trying to add a user on " .. host .. " but command was sent to " .. data.to}}; - end - if (fields["password"] == fields["password-verify"]) and username and host then - if usermanager_user_exists(username, host) then - return { status = "completed", error = { message = "Account already exists" } }; - else - if usermanager_create_user(username, fields.password, host) then - module:log("info", "Created new account " .. username.."@"..host); - return { status = "completed", info = "Account successfully created" }; - else - return { status = "completed", error = { message = "Failed to write data to disk" } }; - end - end - else - module:log("debug", (fields.accountjid or "") .. " " .. (fields.password or "") .. " " - .. (fields["password-verify"] or "")); - return { status = "completed", error = { message = "Invalid data.\nPassword mismatch, or empty username" } }; - end - else - return { status = "executing", form = add_user_layout }, "executing"; - end -end - -function change_user_password_command_handler(self, data, state) - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - local fields = change_user_password_layout:data(data.form); - if not fields.accountjid or fields.accountjid == "" or not fields.password then - return { status = "completed", error = { message = "Please specify username and password" } }; - end - local username, host, resource = jid.split(fields.accountjid); - if data.to ~= host then - return { status = "completed", error = { message = "Trying to change the password of a user on " .. host .. " but command was sent to " .. data.to}}; - end - if usermanager_user_exists(username, host) and usermanager_set_password(username, fields.password, host) then - return { status = "completed", info = "Password successfully changed" }; - else - return { status = "completed", error = { message = "User does not exist" } }; - end - else - return { status = "executing", form = change_user_password_layout }, "executing"; - end -end - -function disconnect_user(match_jid) - local node, hostname, givenResource = jid.split(match_jid); - local host = hosts[hostname]; - local sessions = host.sessions[node] and host.sessions[node].sessions; - for resource, session in pairs(sessions or {}) do - if not givenResource or (resource == givenResource) then - module:log("debug", "Disconnecting "..node.."@"..hostname.."/"..resource); - session:close(); - end - end - return true; -end - -function delete_user_command_handler(self, data, state) - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - local fields = delete_user_layout:data(data.form); - local failed = {}; - local succeeded = {}; - for _, aJID in ipairs(fields.accountjids) do - local username, host, resource = jid.split(aJID); - if (host == data.to) and usermanager_user_exists(username, host) and disconnect_user(aJID) and usermanager_create_user(username, nil, host) then - module:log("debug", "User " .. aJID .. " has been deleted"); - succeeded[#succeeded+1] = aJID; - else - module:log("debug", "Tried to delete non-existant user "..aJID); - failed[#failed+1] = aJID; - end - end - return {status = "completed", info = (#succeeded ~= 0 and - "The following accounts were successfully deleted:\n"..t_concat(succeeded, "\n").."\n" or "").. - (#failed ~= 0 and - "The following accounts could not be deleted:\n"..t_concat(failed, "\n") or "") }; - else - return { status = "executing", form = delete_user_layout }, "executing"; - end -end - -function end_user_session_handler(self, data, state) - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - - local fields = end_user_session_layout:data(data.form); - local failed = {}; - local succeeded = {}; - for _, aJID in ipairs(fields.accountjids) do - local username, host, resource = jid.split(aJID); - if (host == data.to) and usermanager_user_exists(username, host) and disconnect_user(aJID) then - succeeded[#succeeded+1] = aJID; - else - failed[#failed+1] = aJID; - end - end - return {status = "completed", info = (#succeeded ~= 0 and - "The following accounts were successfully disconnected:\n"..t_concat(succeeded, "\n").."\n" or "").. - (#failed ~= 0 and - "The following accounts could not be disconnected:\n"..t_concat(failed, "\n") or "") }; - else - return { status = "executing", form = end_user_session_layout }, "executing"; - end -end - -function get_user_password_handler(self, data, state) - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - local fields = get_user_password_layout:data(data.form); - if not fields.accountjid then - return { status = "completed", error = { message = "Please specify a JID." } }; - end - local user, host, resource = jid.split(fields.accountjid); - local accountjid = ""; - local password = ""; - if host ~= data.to then - return { status = "completed", error = { message = "Tried to get password for a user on " .. host .. " but command was sent to " .. data.to } }; - elseif usermanager_user_exists(user, host) then - accountjid = fields.accountjid; - password = usermanager_get_password(user, host); - else - return { status = "completed", error = { message = "User does not exist" } }; - end - return { status = "completed", result = { layout = get_user_password_result_layout, values = {accountjid = accountjid, password = password} } }; - else - return { status = "executing", form = get_user_password_layout }, "executing"; - end -end - -function get_user_roster_handler(self, data, state) - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - - local fields = get_user_roster_layout:data(data.form); - - if not fields.accountjid then - return { status = "completed", error = { message = "Please specify a JID" } }; - end - - local user, host, resource = jid.split(fields.accountjid); - if host ~= data.to then - return { status = "completed", error = { message = "Tried to get roster for a user on " .. host .. " but command was sent to " .. data.to } }; - elseif not usermanager_user_exists(user, host) then - return { status = "completed", error = { message = "User does not exist" } }; - end - local roster = rm_load_roster(user, host); - - local query = st.stanza("query", { xmlns = "jabber:iq:roster" }); - for jid in pairs(roster) do - if jid ~= "pending" and jid then - query:tag("item", { - jid = jid, - subscription = roster[jid].subscription, - ask = roster[jid].ask, - name = roster[jid].name, - }); - for group in pairs(roster[jid].groups) do - query:tag("group"):text(group):up(); - end - query:up(); - end - end - - local query_text = query:__tostring(); -- TODO: Use upcoming pretty_print() function - query_text = query_text:gsub("><", ">\n<"); - - local result = get_user_roster_result_layout:form({ accountjid = user.."@"..host, roster = query_text }, "result"); - result:add_child(query); - return { status = "completed", other = result }; - else - return { status = "executing", form = get_user_roster_layout }, "executing"; - end -end - -function get_user_stats_handler(self, data, state) - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - - local fields = get_user_stats_layout:data(data.form); - - if not fields.accountjid then - return { status = "completed", error = { message = "Please specify a JID." } }; - end - - local user, host, resource = jid.split(fields.accountjid); - if host ~= data.to then - return { status = "completed", error = { message = "Tried to get stats for a user on " .. host .. " but command was sent to " .. data.to } }; - elseif not usermanager_user_exists(user, host) then - return { status = "completed", error = { message = "User does not exist" } }; - end - local roster = rm_load_roster(user, host); - local rostersize = 0; - local IPs = ""; - local resources = ""; - for jid in pairs(roster) do - if jid ~= "pending" and jid then - rostersize = rostersize + 1; - end - end - for resource, session in pairs((hosts[host].sessions[user] and hosts[host].sessions[user].sessions) or {}) do - resources = resources .. "\n" .. resource; - IPs = IPs .. "\n" .. session.ip; - end - return { status = "completed", result = {layout = get_user_stats_result_layout, values = {ipaddresses = IPs, rostersize = tostring(rostersize), - onlineresources = resources}} }; - else - return { status = "executing", form = get_user_stats_layout }, "executing"; - end -end - -function get_online_users_command_handler(self, data, state) - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - - local fields = get_online_users_layout:data(data.form); - - local max_items = nil - if fields.max_items ~= "all" then - max_items = tonumber(fields.max_items); - end - local count = 0; - local users = {}; - for username, user in pairs(hosts[data.to].sessions or {}) do - if (max_items ~= nil) and (count >= max_items) then - break; - end - users[#users+1] = username.."@"..data.to; - count = count + 1; - if fields.details then - for resource, session in pairs(user.sessions or {}) do - local status, priority = "unavailable", tostring(session.priority or "-"); - if session.presence then - status = session.presence:child_with_name("show"); - if status then - status = status:get_text() or "[invalid!]"; - else - status = "available"; - end - end - users[#users+1] = " - "..resource..": "..status.."("..priority..")"; - end - end - end - return { status = "completed", result = {layout = get_online_users_result_layout, values = {onlineuserjids=t_concat(users, "\n")}} }; - else - return { status = "executing", form = get_online_users_layout }, "executing"; - end -end - -function send_to_online(message, server) - if server then - sessions = { [server] = hosts[server] }; - else - sessions = hosts; - end - - local c = 0; - for domain, session in pairs(sessions) do - for user in pairs(session.sessions or {}) do - c = c + 1; - message.attr.from = domain; - message.attr.to = user.."@"..domain; - core_post_stanza(session, message); - end - end - - return c; -end - -function announce_handler(self, data, state) - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - - local fields = announce_layout:data(data.form); - - if (not fields.announcement) or (#fields.announcement == 0) then - return { status = "completed", error = { message = "Please specify some announcement text." } }; - end - - module:log("info", "Sending server announcement to all online users"); - local message = st.message({type = "headline"}, fields.announcement):up() - :tag("subject"):text(fields.subject or "Announcement"); - - local count = send_to_online(message, data.to); - - module:log("info", "Announcement sent to %d online users", count); - return { status = "completed", info = "Announcement sent." }; - else - return { status = "executing", form = announce_layout }, "executing"; - end - - return true; -end - -function shut_down_service_handler(self, data, state) - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - - local fields = shut_down_service_layout:data(data.form); - - if fields.announcement and #fields.announcement > 0 then - local message = st.message({type = "headline"}, fields.announcement):up() - :tag("subject"):text("Server is shutting down"); - send_to_online(message); - end - - timer_add_task(tonumber(fields.delay or "5"), prosody.shutdown); - - return { status = "completed", info = "Server is about to shut down" }; - else - return { status = "executing", form = shut_down_service_layout }, "executing"; - end - - return true; -end - -local add_user_desc = adhoc_new("Add User", "http://jabber.org/protocol/admin#add-user", add_user_command_handler, "admin"); -local announce_desc = adhoc_new("Send Announcement to Online Users", "http://jabber.org/protocol/admin#announce", announce_handler, "admin"); -local change_user_password_desc = adhoc_new("Change User Password", "http://jabber.org/protocol/admin#change-user-password", change_user_password_command_handler, "admin"); -local delete_user_desc = adhoc_new("Delete User", "http://jabber.org/protocol/admin#delete-user", delete_user_command_handler, "admin"); -local end_user_session_desc = adhoc_new("End User Session", "http://jabber.org/protocol/admin#end-user-session", end_user_session_handler, "admin"); -local get_user_password_desc = adhoc_new("Get User Password", "http://jabber.org/protocol/admin#get-user-password", get_user_password_handler, "admin"); -local get_user_roster_desc = adhoc_new("Get User Roster","http://jabber.org/protocol/admin#get-user-roster", get_user_roster_handler, "admin"); -local get_user_stats_desc = adhoc_new("Get User Statistics","http://jabber.org/protocol/admin#user-stats", get_user_stats_handler, "admin"); -local get_online_users_desc = adhoc_new("Get List of Online Users", "http://jabber.org/protocol/admin#get-online-users", get_online_users_command_handler, "admin"); -local shut_down_service_desc = adhoc_new("Shut Down Service", "http://jabber.org/protocol/admin#shutdown", shut_down_service_handler, "admin"); - -module:add_item("adhoc", add_user_desc); -module:add_item("adhoc", announce_desc); -module:add_item("adhoc", change_user_password_desc); -module:add_item("adhoc", delete_user_desc); -module:add_item("adhoc", end_user_session_desc); -module:add_item("adhoc", get_user_password_desc); -module:add_item("adhoc", get_user_roster_desc); -module:add_item("adhoc", get_user_stats_desc); -module:add_item("adhoc", get_online_users_desc); -module:add_item("adhoc", shut_down_service_desc); diff -r 9bbd99f2057a -r 9d5731af2c27 mod_adhoc_cmd_modules/mod_adhoc_cmd_modules.lua --- a/mod_adhoc_cmd_modules/mod_adhoc_cmd_modules.lua Mon Jun 11 22:32:45 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,137 +0,0 @@ --- Copyright (C) 2009-2010 Florian Zeitz --- --- This file is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -local _G = _G; - -local prosody = _G.prosody; -local hosts = prosody.hosts; - -require "util.iterators"; -local dataforms_new = require "util.dataforms".new; -local array = require "util.array"; -local modulemanager = require "modulemanager"; -local adhoc_new = module:require "adhoc".new; - -function list_modules_handler(self, data, state) - local result = dataforms_new { - title = "List of loaded modules"; - - { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#list" }; - { name = "modules", type = "text-multi", label = "The following modules are loaded:" }; - }; - - local modules = array.collect(keys(hosts[data.to].modules)):sort():concat("\n"); - - return { status = "completed", result = { layout = result; values = { modules = modules } } }; -end - -function load_module_handler(self, data, state) - local layout = dataforms_new { - title = "Load module"; - instructions = "Specify the module to be loaded"; - - { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#load" }; - { name = "module", type = "text-single", required = true, label = "Module to be loaded:"}; - }; - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - local fields = layout:data(data.form); - if (not fields.module) or (fields.module == "") then - return { status = "completed", error = { - message = "Please specify a module." - } }; - end - if modulemanager.is_loaded(data.to, fields.module) then - return { status = "completed", info = "Module already loaded" }; - end - local ok, err = modulemanager.load(data.to, fields.module); - if ok then - return { status = "completed", info = 'Module "'..fields.module..'" successfully loaded on host "'..data.to..'".' }; - else - return { status = "completed", error = { message = 'Failed to load module "'..fields.module..'" on host "'..data.to.. - '". Error was: "'..tostring(err or "")..'"' } }; - end - else - local modules = array.collect(keys(hosts[data.to].modules)):sort(); - return { status = "executing", form = layout }, "executing"; - end -end - --- TODO: Allow reloading multiple modules (depends on list-multi) -function reload_modules_handler(self, data, state) - local layout = dataforms_new { - title = "Reload module"; - instructions = "Select the module to be reloaded"; - - { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#reload" }; - { name = "module", type = "list-single", required = true, label = "Module to be reloaded:"}; - }; - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - local fields = layout:data(data.form); - if (not fields.module) or (fields.module == "") then - return { status = "completed", error = { - message = "Please specify a module. (This means your client misbehaved, as this field is required)" - } }; - end - local ok, err = modulemanager.reload(data.to, fields.module); - if ok then - return { status = "completed", info = 'Module "'..fields.module..'" successfully reloaded on host "'..data.to..'".' }; - else - return { status = "completed", error = { message = 'Failed to reload module "'..fields.module..'" on host "'..data.to.. - '". Error was: "'..tostring(err)..'"' } }; - end - else - local modules = array.collect(keys(hosts[data.to].modules)):sort(); - return { status = "executing", form = { layout = layout; values = { module = modules } } }, "executing"; - end -end - --- TODO: Allow unloading multiple modules (depends on list-multi) -function unload_modules_handler(self, data, state) - local layout = dataforms_new { - title = "Unload module"; - instructions = "Select the module to be unloaded"; - - { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#unload" }; - { name = "module", type = "list-single", required = true, label = "Module to be unloaded:"}; - }; - if state then - if data.action == "cancel" then - return { status = "canceled" }; - end - local fields = layout:data(data.form); - if (not fields.module) or (fields.module == "") then - return { status = "completed", error = { - message = "Please specify a module. (This means your client misbehaved, as this field is required)" - } }; - end - local ok, err = modulemanager.unload(data.to, fields.module); - if ok then - return { status = "completed", info = 'Module "'..fields.module..'" successfully unloaded on host "'..data.to..'".' }; - else - return { status = "completed", error = { message = 'Failed to unload module "'..fields.module..'" on host "'..data.to.. - '". Error was: "'..tostring(err)..'"' } }; - end - else - local modules = array.collect(keys(hosts[data.to].modules)):sort(); - return { status = "executing", form = { layout = layout; values = { module = modules } } }, "executing"; - end -end - -local list_modules_desc = adhoc_new("List loaded modules", "http://prosody.im/protocol/modules#list", list_modules_handler, "admin"); -local load_module_desc = adhoc_new("Load module", "http://prosody.im/protocol/modules#load", load_module_handler, "admin"); -local reload_modules_desc = adhoc_new("Reload module", "http://prosody.im/protocol/modules#reload", reload_modules_handler, "admin"); -local unload_modules_desc = adhoc_new("Unload module", "http://prosody.im/protocol/modules#unload", unload_modules_handler, "admin"); - -module:add_item("adhoc", list_modules_desc); -module:add_item("adhoc", load_module_desc); -module:add_item("adhoc", reload_modules_desc); -module:add_item("adhoc", unload_modules_desc); diff -r 9bbd99f2057a -r 9d5731af2c27 mod_adhoc_cmd_ping/mod_adhoc_cmd_ping.lua --- a/mod_adhoc_cmd_ping/mod_adhoc_cmd_ping.lua Mon Jun 11 22:32:45 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,18 +0,0 @@ --- Copyright (C) 2009 Thilo Cestonaro --- --- This file is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -local st = require "util.stanza"; -local adhoc_new = module:require "adhoc".new; - -function ping_command_handler (self, data, state) - local now = os.date("%Y-%m-%dT%X"); - return { info = "Pong\n"..now, status = "completed" }; -end - -local descriptor = adhoc_new("Ping", "ping", ping_command_handler); - -module:add_item ("adhoc", descriptor); - diff -r 9bbd99f2057a -r 9d5731af2c27 mod_adhoc_cmd_uptime/mod_adhoc_cmd_uptime.lua --- a/mod_adhoc_cmd_uptime/mod_adhoc_cmd_uptime.lua Mon Jun 11 22:32:45 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ --- Copyright (C) 2009 Florian Zeitz --- --- This file is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - -local _G = _G; -local prosody = _G.prosody; -local st = require "util.stanza"; -local adhoc_new = module:require "adhoc".new; - -function uptime() - local t = os.time()-prosody.start_time; - local seconds = t%60; - t = (t - seconds)/60; - local minutes = t%60; - t = (t - minutes)/60; - local hours = t%24; - t = (t - hours)/24; - local days = t; - return string.format("This server has been running for %d day%s, %d hour%s and %d minute%s (since %s)", - days, (days ~= 1 and "s") or "", hours, (hours ~= 1 and "s") or "", - minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time)); -end - -function uptime_command_handler (self, data, state) - return { info = uptime(), status = "completed" }; -end - -local descriptor = adhoc_new("Get uptime", "uptime", uptime_command_handler); - -module:add_item ("adhoc", descriptor); - diff -r 9bbd99f2057a -r 9d5731af2c27 mod_admin_web/admin_web/mod_admin_web.lua --- a/mod_admin_web/admin_web/mod_admin_web.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_admin_web/admin_web/mod_admin_web.lua Fri Jul 27 14:29:59 2012 +0100 @@ -21,7 +21,6 @@ local uuid_generate = require "util.uuid".generate; local is_admin = require "core.usermanager".is_admin; local pubsub = require "util.pubsub"; -local httpserver = require "net.httpserver"; local jid_bare = require "util.jid".bare; local lfs = require "lfs"; local open = io.open; @@ -31,17 +30,12 @@ local service = {}; -local http_base = module.path:gsub("/[^/]+$","") .. "/www_files"; +local http_base = module.path:gsub("/[^/]+$","") .. "/www_files/"; local xmlns_adminsub = "http://prosody.im/adminsub"; local xmlns_c2s_session = "http://prosody.im/streams/c2s"; local xmlns_s2s_session = "http://prosody.im/streams/s2s"; -local response_301 = { status = "301 Moved Permanently" }; -local response_400 = { status = "400 Bad Request", body = "

Bad Request

Sorry, we didn't understand your request :(" }; -local response_403 = { status = "403 Forbidden", body = "

Forbidden

You don't have permission to view the contents of this directory :(" }; -local response_404 = { status = "404 Not Found", body = "

Page Not Found

Sorry, we couldn't find what you were looking for :(" }; - local mime_map = { html = "text/html"; xml = "text/xml"; @@ -110,238 +104,222 @@ end end -local function preprocess_path(path) - if path:sub(1,1) ~= "/" then - path = "/"..path; - end - local level = 0; - for component in path:gmatch("([^/]+)/") do - if component == ".." then - level = level - 1; - elseif component ~= "." then - level = level + 1; +function serve_file(event, path) + local full_path = http_base .. path; + + if stat(full_path, "mode") == "directory" then + if stat(full_path.."/index.html", "mode") == "file" then + return serve_file(event, path.."/index.html"); end - if level < 0 then - return nil; - end + return 403; end - return path; -end -function serve_file(path, base) - local full_path = http_base..path; - if stat(full_path, "mode") == "directory" then - if not path:find("/$") then - local response = response_301; - response.headers = { ["Location"] = base .. "/" }; - return response; - end - if stat(full_path.."/index.html", "mode") == "file" then - return serve_file(path.."/index.html"); - end - return response_403; + local f, err = open(full_path, "rb"); + if not f then + return 404; end - local f, err = open(full_path, "rb"); - if not f then return response_404; end + local data = f:read("*a"); f:close(); if not data then - return response_403; + return 403; end - local ext = path:match("%.([^.]*)$"); - local mime = mime_map[ext]; -- Content-Type should be nil when not known - return { - headers = { ["Content-Type"] = mime; }; - body = data; - }; -end -local function handle_file_request(method, body, request) - local path = preprocess_path(request.url.path); - if not path then return response_400; end - path_stripped = path:gsub("^/[^/]+", ""); -- Strip /admin/ - return serve_file(path_stripped, path); -end - -function module.load() - local http_conf = config.get("*", "core", "webadmin_http_ports"); - - httpserver.new_from_config(http_conf, handle_file_request, { base = "admin" }); + local ext = path:match("%.([^.]*)$"); + event.response.headers.content_type = mime_map[ext]; -- Content-Type should be nil when not known + return data; end -prosody.events.add_handler("server-started", function () - for host_name, host_table in pairs(hosts) do - service[host_name] = pubsub.new({ - broadcaster = function(node, jids, item) return simple_broadcast(node, jids, item, host_name) end; - normalize_jid = jid_bare; - get_affiliation = function(jid) return get_affiliation(jid, host_name) end; - capabilities = { - member = { - create = false; - publish = false; - retract = false; - get_nodes = true; +function module.add_host(module) + -- Setup HTTP server + module:depends("http"); + 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 ok, err; + service[module.host] = pubsub.new({ + broadcaster = function(node, jids, item) return simple_broadcast(node, jids, item, module.host) end; + 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 = 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; - subscribe_other = false; - unsubscribe_other = false; - get_subscription_other = false; - get_subscriptions_other = false; + set_affiliation = false; + }; - be_subscribed = true; - be_unsubscribed = true; + owner = { + create = true; + publish = true; + retract = true; + get_nodes = true; - set_affiliation = false; - }; + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; - owner = { - create = true; - publish = true; - retract = true; - get_nodes = true; + subscribe_other = true; + unsubscribe_other = true; + get_subscription_other = true; + get_subscriptions_other = true; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = true; + }; + }; + }); - subscribe = true; - unsubscribe = true; - get_subscription = true; - get_subscriptions = true; - get_items = true; + -- Create node for s2s sessions + 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 - subscribe_other = true; - unsubscribe_other = true; - get_subscription_other = true; - get_subscriptions_other = true; + -- Add outgoing s2s sessions + for remotehost, session in pairs(hosts[module.host].s2sout) do + if session.type ~= "s2sout_unauthed" then + add_host(session, "out", module.host); + end + end - be_subscribed = true; - be_unsubscribed = true; + -- 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 - set_affiliation = true; - }; - }; - }); + -- 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 - if not select(2, service[host_name]:get_nodes(true))[xmlns_s2s_session] then - local ok, errmsg = service[host_name]:create(xmlns_s2s_session, true); - if not ok then - module:log("warn", "Could not create node " .. xmlns_s2s_session .. ": " .. tostring(errmsg)); + -- Add c2s sessions + for username, user in pairs(hosts[module.host].sessions or {}) do + for resource, 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 - service[host_name]:set_affiliation(xmlns_s2s_session, true, host_name, "owner") - end - end - - for remotehost, session in pairs(host_table.s2sout) do - if session.type ~= "s2sout_unauthed" then - add_host(session, "out", host_name); + reply = st.error_reply(stanza, "cancel", ret); end - end - for session in pairs(incoming_s2s) do - if session.to_host == host_name then - add_host(session, "in", host_name); + 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 - end - - if not select(2, service[host_name]:get_nodes(true))[xmlns_c2s_session] then - local ok, errmsg = service[host_name]:create(xmlns_c2s_session, true); + 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 - module:log("warn", "Could not create node " .. xmlns_c2s_session .. ": " .. tostring(errmsg)); - else - service[host_name]:set_affiliation(xmlns_c2s_session, true, host_name, "owner") + return origin.send(st.error_reply(stanza, "cancel", ret)); end - end - - for username, user in pairs(host_table.sessions or {}) do - for resource, session in pairs(user.sessions or {}) do - add_client(session, host_name); - end - end - host_table.events.add_handler("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[host_name]: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[host_name]: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[host_name]:get_items(node, stanza.attr.from); - if not ok then - return origin.send(st.error_reply(stanza, "cancel", ret)); - 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 + 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, "feature-not-implemented"); + 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 - return origin.send(reply); - end); + reply = st.reply(stanza) + :tag("adminsub", { xmlns = xmlns_adminsub }) + :add_child(data); + else + reply = st.error_reply(stanza, "feature-not-implemented"); + end + return origin.send(reply); + end); - host_table.events.add_handler("resource-bind", function(event) - add_client(event.session, host_name); - end); - - host_table.events.add_handler("resource-unbind", function(event) - del_client(event.session, host_name); - service[host_name]:remove_subscription(xmlns_c2s_session, host_name, event.session.full_jid); - service[host_name]:remove_subscription(xmlns_s2s_session, host_name, event.session.full_jid); - end); + -- Add/remove c2s sessions + module:hook("resource-bind", function(event) + add_client(event.session, module.host); + end); - host_table.events.add_handler("s2sout-established", function(event) - add_host(event.session, "out", host_name); - 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); - host_table.events.add_handler("s2sin-established", function(event) - add_host(event.session, "in", host_name); - end); + -- Add/remove s2s sessions + module:hook("s2sout-established", function(event) + add_host(event.session, "out", module.host); + end); - host_table.events.add_handler("s2sout-destroyed", function(event) - del_host(event.session, "out", host_name); - end); + module:hook("s2sin-established", function(event) + add_host(event.session, "in", module.host); + end); - host_table.events.add_handler("s2sin-destroyed", function(event) - del_host(event.session, "in", host_name); - end); + module:hook("s2sout-destroyed", function(event) + del_host(event.session, "out", module.host); + end); - end -end); + module:hook("s2sin-destroyed", function(event) + del_host(event.session, "in", module.host); + end); +end function simple_broadcast(node, jids, item, host) item = st.clone(item); diff -r 9bbd99f2057a -r 9d5731af2c27 mod_admin_web/admin_web/mod_admin_web_timber.lua --- a/mod_admin_web/admin_web/mod_admin_web_timber.lua Mon Jun 11 22:32:45 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,345 +0,0 @@ --- 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 lfs = require "lfs"; -local open = io.open; -local stat = lfs.attributes; - -module:set_global(); - -local service = {}; - -local http_base = module.path:gsub("/[^/]+$","") .. "/www_files/"; - -local xmlns_adminsub = "http://prosody.im/adminsub"; -local xmlns_c2s_session = "http://prosody.im/streams/c2s"; -local xmlns_s2s_session = "http://prosody.im/streams/s2s"; - -local mime_map = { - html = "text/html"; - xml = "text/xml"; - js = "text/javascript"; - css = "text/css"; -}; - -local idmap = {}; - -function add_client(session, host) - 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 - item:tag("encrypted"):up(); - 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 - -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 - -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 - if session.cert_identity_status == "valid" then - item:tag("encrypted"):tag("valid"):up():up(); - else - item:tag("encrypted"):tag("invalid"):up():up(); - 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 - -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 - -function serve_file(event, path) - local full_path = http_base .. path; - - if stat(full_path, "mode") == "directory" then - if stat(full_path.."/index.html", "mode") == "file" then - return serve_file(event, path.."/index.html"); - end - return 403; - end - - local f, err = open(full_path, "rb"); - if not f then - return 404; - end - - local data = f:read("*a"); - f:close(); - if not data then - return 403; - end - - local ext = path:match("%.([^.]*)$"); - event.response.headers.content_type = mime_map[ext]; -- Content-Type should be nil when not known - return data; -end - -function module.add_host(module) - -- Setup HTTP server - module:depends("http"); - 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 ok, err; - service[module.host] = pubsub.new({ - broadcaster = function(node, jids, item) return simple_broadcast(node, jids, item, module.host) end; - 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 - 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 remotehost, 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 username, user in pairs(hosts[module.host].sessions or {}) do - for resource, 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 - return origin.send(st.error_reply(stanza, "cancel", ret)); - 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 - return origin.send(reply); - 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 - -function simple_broadcast(node, jids, item, host) - item = st.clone(item); - item.attr.xmlns = nil; -- Clear the pubsub namespace - local message = st.message({ from = host, type = "headline" }) - :tag("event", { xmlns = xmlns_adminsub .. "#event" }) - :tag("items", { node = node }) - :add_child(item); - for jid in pairs(jids) do - module:log("debug", "Sending notification to %s", jid); - message.attr.to = jid; - core_post_stanza(hosts[host], message); - end -end - -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 diff -r 9bbd99f2057a -r 9d5731af2c27 mod_archive/mod_archive.lua --- a/mod_archive/mod_archive.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_archive/mod_archive.lua Fri Jul 27 14:29:59 2012 +0100 @@ -37,16 +37,8 @@ dm.store(node, host, PREFS_DIR, st.preserialize(data)); end -local function os_date() - return os.date("!*t"); -end - local date_time = datetime.datetime; -local function date_format(s) - return os.date("%Y-%m-%dT%H:%M:%SZ", s); -end - local function date_parse(s) local year, month, day, hour, min, sec = s:match("(....)-?(..)-?(..)T(..):(..):(..)Z"); return os.time({year=year, month=month, day=day, hour=hour, min=min, sec=sec}); @@ -95,10 +87,9 @@ local thread = msg:child_with_name("thread"); local data = dm.list_load(node, host, ARCHIVE_DIR); local tag = isfrom and "from" or "to"; - local with = isfrom and msg.attr.to or msg.attr.from; - local utc = os_date(); - local utc_secs = os.time(utc); - local utc_datetime = date_format(utc_secs); + local with = isfrom and msg.attr.from or msg.attr.to; + local utc_datetime = date_time(); + local utc_secs = date_parse(utc_datetime); if data then -- The collection list are in REVERSE chronological order for k, v in ipairs(data) do @@ -761,25 +752,29 @@ return AUTO_ARCHIVING_ENABLED; end -local function msg_handler(data) +local function msg_handler(data, local_jid, other_jid, isfrom) module:log("debug", "-- Enter msg_handler()"); local origin, stanza = data.origin, data.stanza; local body = stanza:child_with_name("body"); local thread = stanza:child_with_name("thread"); if body then - local from_node, from_host = jid.split(stanza.attr.from); - local to_node, to_host = jid.split(stanza.attr.to); - if hosts[from_host] and um.user_exists(from_node, from_host) and apply_pref(from_node, from_host, stanza.attr.to, thread) then - store_msg(stanza, from_node, from_host, true); - end - if hosts[to_host] and um.user_exists(to_node, to_host) and apply_pref(to_node, to_host, stanza.attr.from, thread) then - store_msg(stanza, to_node, to_host, false); + local local_node, local_host = jid.split(local_jid); + if hosts[local_host] and um.user_exists(local_node, local_host) and apply_pref(local_node, local_host, other_jid, thread) then + store_msg(stanza, local_node, local_host, isfrom); end end return nil; end +local function message_handler(data) + msg_handler(data, data.stanza.attr.to, data.stanza.attr.from, true) +end + +local function premessage_handler(data) + msg_handler(data, data.stanza.attr.from, data.stanza.attr.to, false) +end + -- Preferences module:hook("iq/self/urn:xmpp:archive:pref", preferences_handler); module:hook("iq/self/urn:xmpp:archive:itemremove", itemremove_handler); @@ -794,10 +789,10 @@ -- Replication module:hook("iq/self/urn:xmpp:archive:modified", modified_handler); -module:hook("message/full", msg_handler, 10); -module:hook("message/bare", msg_handler, 10); -module:hook("pre-message/full", msg_handler, 10); -module:hook("pre-message/bare", msg_handler, 10); +module:hook("message/full", message_handler, 10); +module:hook("message/bare", message_handler, 10); +module:hook("pre-message/full", premessage_handler, 10); +module:hook("pre-message/bare", premessage_handler, 10); -- TODO exactmatch -- TODO JID match diff -r 9bbd99f2057a -r 9d5731af2c27 mod_auth_joomla/mod_auth_joomla.lua --- a/mod_auth_joomla/mod_auth_joomla.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_auth_joomla/mod_auth_joomla.lua Fri Jul 27 14:29:59 2012 +0100 @@ -12,6 +12,7 @@ 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; @@ -79,7 +80,7 @@ end local function get_password(username) - local stmt, err = getsql("SELECT `password` FROM `jos_users` WHERE `username`=?", 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; @@ -89,7 +90,8 @@ local function getCryptedPassword(plaintext, salt) - return md5(plaintext..salt); + local salted = plaintext..salt; + return md5(salted, true); end local function joomlaCheckHash(password, hash) local crypt, salt = hash:match("^([^:]*):(.*)$"); @@ -118,7 +120,7 @@ end function provider.set_password(username, password) local hash = joomlaCreateHash(password); - local stmt, err = setsql("UPDATE `jos_users` SET `password`=? WHERE `username`=?", hash, username); + 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) diff -r 9bbd99f2057a -r 9d5731af2c27 mod_carbons/mod_carbons.lua --- a/mod_carbons/mod_carbons.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_carbons/mod_carbons.lua Fri Jul 27 14:29:59 2012 +0100 @@ -81,7 +81,7 @@ end local msg = st.clone(stanza); - msg.attr.xmlns = msg.attr.xmlns or "jabber:client"; + msg.attr.xmlns = "jabber:client"; local fwd = st.message{ from = bare_jid, type = orig_type, } :tag(c2s and "sent" or "received", { xmlns = xmlns_carbons }):up() :tag("forwarded", { xmlns = xmlns_forward }) diff -r 9bbd99f2057a -r 9d5731af2c27 mod_client_certs/mod_client_certs.lua --- a/mod_client_certs/mod_client_certs.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_client_certs/mod_client_certs.lua Fri Jul 27 14:29:59 2012 +0100 @@ -17,34 +17,6 @@ local digest_algo = "sha1"; local base64 = require "util.encodings".base64; -local function enable_cert(username, cert, info) - local certs = dm_load(username, module.host, dm_table) or {}; - - info.pem = cert:pem(); - local digest = cert:digest(digest_algo); - info.digest = digest; - certs[info.id] = info; - - dm_store(username, module.host, dm_table, certs); - return true -end - -local function disable_cert(username, name) - local certs = dm_load(username, module.host, dm_table) or {}; - - local info = certs[name]; - local cert; - if info then - certs[name] = nil; - cert = x509.cert_from_pem(info.pem); - else - return nil, "item-not-found" - end - - dm_store(username, module.host, dm_table, certs); - return cert; -- So we can compare it with stuff -end - local function get_id_on_xmpp_addrs(cert) local id_on_xmppAddrs = {}; for k,ext in pairs(cert:extensions()) do @@ -61,7 +33,81 @@ module:log("debug", "Found JIDs: (%d) %s", #id_on_xmppAddrs, table.concat(id_on_xmppAddrs, ", ")); return id_on_xmppAddrs; end - + +local function enable_cert(username, cert, info) + -- Check the certificate. Is it not expired? Does it include id-on-xmppAddr? + + --[[ the method expired doesn't exist in luasec .. yet? + if cert:expired() then + module:log("debug", "This certificate is already expired."); + return nil, "This certificate is expired."; + end + --]] + + if not cert:valid_at(os.time()) then + module:log("debug", "This certificate is not valid at this moment."); + end + + local valid_id_on_xmppAddrs; + local require_id_on_xmppAddr = true; + if require_id_on_xmppAddr then + valid_id_on_xmppAddrs = get_id_on_xmpp_addrs(cert); + + local found = false; + for i,k in pairs(valid_id_on_xmppAddrs) do + if jid_bare(k) == (username .. "@" .. module.host) then + found = true; + break; + end + end + + if not found then + return nil, "This certificate is has no valid id-on-xmppAddr field."; + end + end + + local certs = dm_load(username, module.host, dm_table) or {}; + + info.pem = cert:pem(); + local digest = cert:digest(digest_algo); + info.digest = digest; + certs[info.id] = info; + + dm_store(username, module.host, dm_table, certs); + return true +end + +local function disable_cert(username, name, disconnect) + local certs = dm_load(username, module.host, dm_table) or {}; + + local info = certs[name]; + + if not info then + return nil, "item-not-found" + end + + certs[name] = nil; + + if disconnect then + module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", username); + local sessions = hosts[module.host].sessions[username].sessions; + local disabled_cert_pem = info.pem; + + for _, session in pairs(sessions) do + if session and session.conn then + local cert = session.conn:socket():getpeercertificate(); + + if cert and cert:pem() == disabled_cert_pem then + module:log("debug", "Found a session that should be closed: %s", tostring(session)); + session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."}; + end + end + end + end + + dm_store(username, module.host, dm_table, certs); + return info; +end module:hook("iq/self/"..xmlns_saslcert..":items", function(event) local origin, stanza = event.origin, event.stanza; @@ -106,7 +152,7 @@ end local can_manage = key_info:get_child("no-cert-management", xmlns_saslcert) ~= nil; - local x509cert = key_info:get_child_text("x509cert"); + local x509cert = key_info:get_child_text("x509cert"):gsub("^%s*(.-)%s*$", "%1"); local cert = x509.cert_from_pem( "-----BEGIN CERTIFICATE-----\n" @@ -119,46 +165,18 @@ return true; end - -- Check the certificate. Is it not expired? Does it include id-on-xmppAddr? - - --[[ the method expired doesn't exist in luasec .. yet? - if cert:expired() then - module:log("debug", "This certificate is already expired."); - origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is expired.")); - return true - end - --]] - - if not cert:valid_at(os.time()) then - module:log("debug", "This certificate is not valid at this moment."); - end - - local valid_id_on_xmppAddrs; - local require_id_on_xmppAddr = true; - if require_id_on_xmppAddr then - valid_id_on_xmppAddrs = get_id_on_xmpp_addrs(cert); - - local found = false; - for i,k in pairs(valid_id_on_xmppAddrs) do - if jid_bare(k) == jid_bare(origin.full_jid) then - found = true; - break; - end - end - - if not found then - origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is has no valid id-on-xmppAddr field.")); - return true -- REJECT?! - end - end - - enable_cert(origin.username, cert, { + local ok, err = enable_cert(origin.username, cert, { id = id, name = name, x509cert = x509cert, no_cert_management = can_manage, }); + if not ok then + origin.send(st.error_reply(stanza, "cancel", "bad-request", err)); + return true -- REJECT?! + end + module:log("debug", "%s added certificate named %s", origin.full_jid, name); origin.send(st.reply(stanza)); @@ -182,24 +200,8 @@ return true end - local disabled_cert = disable_cert(origin.username, name); - - if disabled_cert and disable.name == "revoke" then - module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", origin.full_jid); - local sessions = hosts[module.host].sessions[origin.username].sessions; - local disabled_cert_pem = disabled_cert:pem(); + disable_cert(origin.username, name, disable.name == "revoke"); - for _, session in pairs(sessions) do - if session and session.conn then - local cert = session.conn:socket():getpeercertificate(); - - if cert and cert:pem() == disabled_cert_pem then - module:log("debug", "Found a session that should be closed: %s", tostring(session)); - session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."}; - end - end - end - end origin.send(st.reply(stanza)); return true @@ -209,6 +211,151 @@ module:hook("iq/self/"..xmlns_saslcert..":disable", handle_disable); module:hook("iq/self/"..xmlns_saslcert..":revoke", handle_disable); +-- Ad-hoc command +local adhoc_new = module:require "adhoc".new; +local dataforms_new = require "util.dataforms".new; + +local function generate_error_message(errors) + local errmsg = {}; + for name, err in pairs(errors) do + errmsg[#errmsg + 1] = name .. ": " .. err; + end + return table.concat(errmsg, "\n"); +end + +local choose_subcmd_layout = dataforms_new { + title = "Certificate management"; + instructions = "What action do you want to perform?"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#subcmd" }; + { name = "subcmd", type = "list-single", label = "Actions", required = true, + value = { {label = "Add certificate", value = "add"}, + {label = "List certificates", value = "list"}, + {label = "Disable certificate", value = "disable"}, + {label = "Revoke certificate", value = "revoke"}, + }; + }; +}; + +local add_layout = dataforms_new { + title = "Adding a certificate"; + instructions = "Enter the certificate in PEM format"; + + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#add" }; + { name = "name", type = "text-single", label = "Name", required = true }; + { name = "cert", type = "text-multi", label = "PEM certificate", required = true }; + { name = "manage", type = "boolean", label = "Can manage certificates", value = true }; +}; + + +local disable_layout_stub = dataforms_new { { name = "cert", type = "list-single", label = "Certificate", required = true } }; + + +local function adhoc_handler(self, data, state) + if data.action == "cancel" then return { status = "canceled" }; end + + if not state or data.action == "prev" then + return { status = "executing", form = choose_subcmd_layout, actions = { "next" } }, {}; + end + + if not state.subcmd then + local fields, errors = choose_subcmd_layout:data(data.form); + if errors then + return { status = "completed", error = { message = generate_error_message(errors) } }; + end + local subcmd = fields.subcmd + + if subcmd == "add" then + return { status = "executing", form = add_layout, actions = { "prev", "next", "complete" } }, { subcmd = "add" }; + elseif subcmd == "list" then + local list_layout = dataforms_new { + title = "List of certificates"; + }; + + local certs = dm_load(jid_split(data.from), module.host, dm_table) or {}; + + for digest, info in pairs(certs) do + list_layout[#list_layout + 1] = { name = info.id, type = "text-multi", label = info.name, value = info.x509cert }; + end + + return { status = "completed", result = list_layout }; + else + local layout = dataforms_new { + { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#" .. subcmd }; + { name = "cert", type = "list-single", label = "Certificate", required = true }; + }; + + if subcmd == "disable" then + layout.title = "Disabling a certificate"; + layout.instructions = "Select the certificate to disable"; + elseif subcmd == "revoke" then + layout.title = "Revoking a certificate"; + layout.instructions = "Select the certificate to revoke"; + end + + local certs = dm_load(jid_split(data.from), module.host, dm_table) or {}; + + local values = {}; + for digest, info in pairs(certs) do + values[#values + 1] = { label = info.name, value = info.id }; + end + + return { status = "executing", form = { layout = layout, values = { cert = values } }, actions = { "prev", "next", "complete" } }, + { subcmd = subcmd }; + end + end + + if state.subcmd == "add" then + local fields, errors = add_layout:data(data.form); + if errors then + return { status = "completed", error = { message = generate_error_message(errors) } }; + end + + local name = fields.name; + local x509cert = fields.cert:gsub("^%s*(.-)%s*$", "%1"); + + local cert = x509.cert_from_pem( + "-----BEGIN CERTIFICATE-----\n" + .. x509cert .. + "\n-----END CERTIFICATE-----\n"); + + if not cert then + return { status = "completed", error = { message = "Could not parse X.509 certificate" } }; + end + + local ok, err = enable_cert(jid_split(data.from), cert, { + id = cert:digest(digest_algo), + name = name, + x509cert = x509cert, + no_cert_management = not fields.manage + }); + + if not ok then + return { status = "completed", error = { message = err } }; + end + + module:log("debug", "%s added certificate named %s", data.from, name); + + return { status = "completed", info = "Successfully added certificate " .. name .. "." }; + else + local fields, errors = disable_layout_stub:data(data.form); + if errors then + return { status = "completed", error = { message = generate_error_message(errors) } }; + end + + local info = disable_cert(jid_split(data.from), fields.cert, state.subcmd == "revoke" ); + + if state.subcmd == "revoke" then + return { status = "completed", info = "Revoked certificate " .. info.name .. "." }; + else + return { status = "completed", info = "Disabled certificate " .. info.name .. "." }; + end + end +end + +local cmd_desc = adhoc_new("Manage certificates", "http://prosody.im/protocol/certs", adhoc_handler, "user"); +module:provides("adhoc", cmd_desc); + -- Here comes the SASL EXTERNAL stuff local now = os.time; diff -r 9bbd99f2057a -r 9d5731af2c27 mod_compat_bind/mod_compat_bind.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_compat_bind/mod_compat_bind.lua Fri Jul 27 14:29:59 2012 +0100 @@ -0,0 +1,13 @@ +-- 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; + core_process_stanza(event.origin, fixed_stanza); + return true; +end); diff -r 9bbd99f2057a -r 9d5731af2c27 mod_compat_muc_admin/mod_compat_muc_admin.lua --- a/mod_compat_muc_admin/mod_compat_muc_admin.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_compat_muc_admin/mod_compat_muc_admin.lua Fri Jul 27 14:29:59 2012 +0100 @@ -23,6 +23,10 @@ ["service-unavailable"] = true; ["malformed error"] = true; }; +local function get_error_condition(stanza) + local _, condition = stanza:get_error(); + return condition or "malformed error"; +end local function is_kickable_error(stanza) local cond = get_error_condition(stanza); return kickable_error_conditions[cond] and cond; diff -r 9bbd99f2057a -r 9d5731af2c27 mod_compat_vcard/mod_compat_vcard.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_compat_vcard/mod_compat_vcard.lua Fri Jul 27 14:29:59 2012 +0100 @@ -0,0 +1,19 @@ +-- 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.name == "vCard" and stanza.attr.type == "get" 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); diff -r 9bbd99f2057a -r 9d5731af2c27 mod_host_guard/mod_host_guard.lua --- a/mod_host_guard/mod_host_guard.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_host_guard/mod_host_guard.lua Fri Jul 27 14:29:59 2012 +0100 @@ -8,7 +8,7 @@ local guard_protect = module:get_option_set("host_guard_selective", {}) local guard_block_bl = module:get_option_set("host_guard_blacklist", {}) -local config = require "core.configmanager" +local config = configmanager local error_reply = require "util.stanza".error_reply local function s2s_hook (event) @@ -40,34 +40,38 @@ return nil end -local function handle_activation (host) +local function handle_activation (host, u) if guard_blockall:contains(host) or guard_protect:contains(host) then if hosts[host] and hosts[host].events then hosts[host].events.add_handler("s2sin-established", s2s_hook, 500) hosts[host].events.add_handler("route/remote", rr_hook, 500) hosts[host].events.add_handler("stanza/jabber:server:dialback:result", s2s_hook, 500) - module:log ("debug", "adding host protection for: "..host) + if not u then + module:log ("debug", "adding host protection for: "..host) + else + module:log ("debug", "updating or adding host protection for: "..host) + end end end end -local function handle_deactivation (host) +local function handle_deactivation (host, u, i) if guard_blockall:contains(host) or guard_protect:contains(host) then if hosts[host] and hosts[host].events then hosts[host].events.remove_handler("s2sin-established", s2s_hook) hosts[host].events.remove_handler("route/remote", rr_hook) hosts[host].events.remove_handler("stanza/jabber:server:dialback:result", s2s_hook) - module:log ("debug", "removing host protection for: "..host) + if not u and not i then module:log ("debug", "removing host protection for: "..host) end end end end -local function init_hosts() - for n,table in pairs(hosts) do - hosts[n].events.remove_handler("s2sin-established", s2s_hook) - hosts[n].events.remove_handler("route/remote", rr_hook) - hosts[n].events.remove_handler("stanza/jabber:server:dialback:result", s2s_hook) - if guard_blockall:contains(n) or guard_protect:contains(n) then handle_activation(n) end +local function init_hosts(u, i) + for n in pairs(hosts) do + if guard_blockall:contains(n) or guard_protect:contains(n) then + handle_deactivation(n, u, i) + handle_activation(n, u) + end end end @@ -78,7 +82,7 @@ guard_protect = module:get_option_set("host_guard_selective", {}) guard_block_bl = module:get_option_set("host_guard_blacklist", {}) - init_hosts() + init_hosts(true) end local function setup() @@ -87,7 +91,16 @@ module:hook ("host-deactivated", handle_deactivation) module:hook ("config-reloaded", reload) - init_hosts() + init_hosts(false, true) +end + +function module.unload() + module:log ("debug", "removing host handlers as module is being unloaded...") + for n in pairs(hosts) do + hosts[n].events.remove_handler("s2sin-established", s2s_hook) + hosts[n].events.remove_handler("route/remote", rr_hook) + hosts[n].events.remove_handler("stanza/jabber:server:dialback:result", s2s_hook) + end end if prosody.start_time then diff -r 9bbd99f2057a -r 9d5731af2c27 mod_http_favicon/mod_http_favicon.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_http_favicon/mod_http_favicon.lua Fri Jul 27 14:29:59 2012 +0100 @@ -0,0 +1,42 @@ +module:depends("http"); + +local favicon = require"util.encodings".base64.decode[[ +AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAA +AAAAAAD///8AsuD6TGrE95RiwfabYsH2m2TB9pmU1Phq+vz9A/38+wPx07xq67+emeq+nZvqvp2b +68GilPTfz0z///8AsuD6TACb8v8Am/L/AJvy/wCb8v8Am/L/AJvy/3TI94ntxqiJ35dh/9+XYf/f +l2H/35dh/9+XYf/fl2H/9N/PTGrE95QAm/L/AJvy/wCb8v8Am/L/AJvy/wCb8v8qq/PU5Kh61N+X +Yf/fl2H/35dh/9+XYf/fl2H/35dh/+vBopRiwfabAJvy/wCb8v8Am/L/AJvy/wCb8v8Am/L/Iqjz +3OOkdtzfl2H/35dh/9+XYf/fl2H/35dh/9+XYf/qvp2bYsH2mwCb8v8Am/L/AJvy/wCb8v8Am/L/ +AJvy/yKo89zjpHbc35dh/9+XYf/fl2H/35dh/9+XYf/fl2H/6r6dm2TB9pkAm/L/AJvy/wCb8v8A +m/L/AJvy/wCb8v8kqfPa46V32t+XYf/fl2H/35dh/9+XYf/fl2H/35dh/+u/npmU1PhqAJvy/wCb +8v8Am/L/AJvy/wCb8v8Am/L/Vrz2p+m5lqffl2H/35dh/9+XYf/fl2H/35dh/9+XYf/x07xq+vz9 +A3TI94kqq/PUIqjz3CKo89wkqfPaVrz2p+b0/Bf79O8X6bmWp+Old9rjpHbc46R23OSoetTtxqiJ +/fz7A/38+wPtxqiJ5Kh61OOkdtzjpHbc46V32um5lqf79O8X5vT8F1a89qckqfPaIqjz3CKo89wq +q/PUdMj3ifr8/QPx07xq35dh/9+XYf/fl2H/35dh/9+XYf/fl2H/6bmWp1a89qcAm/L/AJvy/wCb +8v8Am/L/AJvy/wCb8v+U1Phq67+emd+XYf/fl2H/35dh/9+XYf/fl2H/35dh/+Old9okqfPaAJvy +/wCb8v8Am/L/AJvy/wCb8v8Am/L/ZMH2meq+nZvfl2H/35dh/9+XYf/fl2H/35dh/9+XYf/jpHbc +Iqjz3ACb8v8Am/L/AJvy/wCb8v8Am/L/AJvy/2LB9pvqvp2b35dh/9+XYf/fl2H/35dh/9+XYf/f +l2H/46R23CKo89wAm/L/AJvy/wCb8v8Am/L/AJvy/wCb8v9iwfab68GilN+XYf/fl2H/35dh/9+X +Yf/fl2H/35dh/+SoetQqq/PUAJvy/wCb8v8Am/L/AJvy/wCb8v8Am/L/asT3lPTfz0zfl2H/35dh +/9+XYf/fl2H/35dh/9+XYf/txqiJdMj3iQCb8v8Am/L/AJvy/wCb8v8Am/L/AJvy/7Lg+kz///8A +9N/PTOvBopTqvp2b6r6dm+u/npnx07xq/fz7A/r8/QOU1PhqZMH2mWLB9ptiwfabasT3lLLg+kz/ +//8Aw8MAAIABAAAAAAAAAAAAAAAAAAAAAAAAgAEAAIGBAACBgQAAgAEAAAAAAAAAAAAAAAAAAAAA +AACAAQAAw8MAAA==]]; + +local filename = module:get_option_string("favicon"); +if filename then + local fd = assert(module:load_resource(filename)); + favicon = assert(fd:read("*a")); +end + +module:provides("http", { + default_path = "/favicon.ico"; + route = { + GET = { + headers = { + content_type = "image/x-icon"; + }; + body = favicon; + } + } +}); diff -r 9bbd99f2057a -r 9d5731af2c27 mod_inotify_reload/mod_inotify_reload.lua --- a/mod_inotify_reload/mod_inotify_reload.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_inotify_reload/mod_inotify_reload.lua Fri Jul 27 14:29:59 2012 +0100 @@ -44,16 +44,19 @@ local k = host.."\0"..name; watches[k] = { id = id, path = path, name = name, host = host }; watch_ids[id] = k; + module:log("debug", "Watching %s:%s with id %d", name, host, id); return true; end function unwatch_module(name, host) local k = host.."\0"..name; if not watches[k] then + module:log("warn", "Not watching %s:%s", name, host); return nil, "not-watching"; end local id = watches[k].id; local ok, err = inh:rmwatch(id); + module:log("info", "Removed watch %d", id); watches[k] = nil; watch_ids[id] = nil; return ok, err; diff -r 9bbd99f2057a -r 9d5731af2c27 mod_limits/mod_limits.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_limits/mod_limits.lua Fri Jul 27 14:29:59 2012 +0100 @@ -0,0 +1,99 @@ +-- mod_limits: Rate-limiting for Prosody +-- Version: Alpha +-- Author: Matthew Wild + +-- Because we deal we pre-authed sessions and streams we can't be host-specific +module:set_global(); + +local filters = require "util.filters"; +local throttle = require "util.throttle"; +local timer = require "util.timer"; + +local limits_cfg = module:get_option("limits", {}); +local limits_resolution = module:get_option_number("limits_resolution", 1); + +local default_bytes_per_second = 3000; +local default_burst = 2; + +local rate_units = { b = 1, k = 3, m = 6, g = 9, t = 12 } -- Plan for the future. +local function parse_rate(rate, sess_type) + local quantity, unit, exp; + if rate then + quantity, unit = rate:match("^(%d+) ?([^/]+)/s$"); + exp = quantity and rate_units[unit:sub(1,1):lower()]; + end + if not exp then + module:log("error", "Error parsing rate for %s: %q, using default rate (%d bytes/s)", sess_type, rate, default_bytes_per_second); + return default_bytes_per_second; + end + return quantity*(10^exp); +end + +local function parse_burst(burst, sess_type) + if type(burst) == "string" then + burst = burst:match("^(%d+) ?s$"); + end + local n_burst = tonumber(burst); + if not n_burst then + module:log("error", "Unable to parse burst for %s: %q, using default burst interval (%ds)", sess_type, tostring(burst), default_burst); + end + return n_burst or default_burst; +end + +-- Process config option into limits table: +-- limits = { c2s = { bytes_per_second = X, burst_seconds = Y } } +local limits = {}; + +for sess_type, sess_limits in pairs(limits_cfg) do + limits[sess_type] = { + bytes_per_second = parse_rate(sess_limits.rate, sess_type); + burst_seconds = parse_burst(sess_limits.burst, sess_type); + }; +end + +local default_filter_set = {}; + +function default_filter_set.bytes_in(bytes, session) + local throttle = session.throttle; + if throttle then + local ok, balance, outstanding = throttle:poll(#bytes, true); + if not ok then + session.log("debug", "Session over rate limit (%d) with %d (by %d), pausing", throttle.max, #bytes, outstanding); + session.conn:pause(); -- Read no more data from the connection until there is no outstanding data + local outstanding_data = bytes:sub(-outstanding); + bytes = bytes:sub(1, #bytes-outstanding); + timer.add_task(limits_resolution, function () + if not session.conn then return; end + if throttle:peek(#outstanding_data) then + session.log("debug", "Resuming paused session"); session.conn:resume(); + end + -- Handle what we can of the outstanding data + session.data(outstanding_data); + end); + end + end + return bytes; +end + +local type_filters = { + c2s = default_filter_set; + s2sin = default_filter_set; + s2sout = default_filter_set; +}; + +local function filter_hook(session) + local session_type = session.type:match("^[^_]+"); + local filter_set, opts = type_filters[session_type], limits[session_type]; + if opts then + session.throttle = throttle.create(opts.bytes_per_second * opts.burst_seconds, opts.burst_seconds); + filters.add_filter(session, "bytes/in", filter_set.bytes_in, 1000); + end +end + +function module.load() + filters.add_filter_hook(filter_hook); +end + +function module.unload() + filters.remove_filter_hook(filter_hook); +end diff -r 9bbd99f2057a -r 9d5731af2c27 mod_mam/mod_mam.lua --- a/mod_mam/mod_mam.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_mam/mod_mam.lua Fri Jul 27 14:29:59 2012 +0100 @@ -11,6 +11,7 @@ local rsm = module:require "rsm"; local jid_bare = require "util.jid".bare; local jid_split = require "util.jid".split; +local jid_prep = require "util.jid".prep; local host = module.host; local dm_load = require "util.datamanager".load; @@ -125,7 +126,23 @@ module:log("debug", "Archive query, id %s with %s from %s until %s)", tostring(qid), qwith or "anyone", qstart or "the dawn of time", qend or "now"); - qstart, qend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend)) + if qstart or qend then -- Validate timestamps + local vstart, vend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend)) + if (qstart and not qwith) 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 + + 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 = pwith; + end -- Load all the data! local data, err = dm_list_load(origin.username, origin.host, archive_store); @@ -193,10 +210,10 @@ module:log("debug", "Start of matching range found"); qset_matches = true; end - if n >= qmax then - module:log("debug", "Max number of items matched"); - break - end + end + if n >= qmax then + module:log("debug", "Max number of items matched"); + break end end -- That's all folks! diff -r 9bbd99f2057a -r 9d5731af2c27 mod_mam_adhoc/mod_mam_adhoc.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_mam_adhoc/mod_mam_adhoc.lua Fri Jul 27 14:29:59 2012 +0100 @@ -0,0 +1,113 @@ +module:depends"adhoc"; +local dataforms_new = require "util.dataforms".new; +local dm_load = require "util.datamanager".load; +local dm_store = require "util.datamanager".store; +local jid_split = require "util.jid".split; +local t_insert = table.insert; + +local mam_prefs_form = dataforms_new{ + title = "Archive preferences"; + --instructions = ""; + { + name = "default", + label = "Default storage policy", + type = "list-single", + value = { + { value = "always", label = "Always" }, + { value = "never", label = "Never", default = true}, + { value = "roster", label = "Roster" }, + }, + }; + { + name = "always", + label = "Always store messages to/from", + type = "jid-multi" + }; + { + name = "never", + label = "Never store messages to/from", + type = "jid-multi" + }; +}; + +local host = module.host; + +local default_attrs = { + always = true, [true] = "always", + never = false, [false] = "never", + roster = "roster", +} + +local global_default_policy = module:get_option("default_archive_policy", false); +local archive_store = "archive2"; +local prefs_store = archive_store .. "_prefs"; +local function get_prefs(user) + return dm_load(user, host, prefs_store) or + { [false] = global_default_policy }; +end +local function set_prefs(user, prefs) + return dm_store(user, host, prefs_store, prefs); +end + +local function mam_prefs_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 ~= module.host then + return { status = "error", error = { type = "cancel", + condition = "forbidden", message = "Invalid user or hostname." } }; + end + + local fields = mam_prefs_form:data(data.form); + + local default, always, never = fields.default, fields.always, fields.never; + local prefs = {}; + if default then + prefs[false] = default_attrs[default]; + end + if always then + for i=1,#always do + prefs[always[i]] = true; + end + end + if never then + for i=1,#never do + prefs[never[i]] = false; + end + end + + set_prefs(username, prefs); + + return { status = "completed" } + else -- No state, send the form. + local prefs = get_prefs(username); + local values = { + default = { + { value = "always", label = "Always" }; + { value = "never", label = "Never" }; + { value = "roster", label = "Roster" }; + }; + always = {}; + never = {}; + }; + + for jid, p in pairs(prefs) do + if jid then + t_insert(values[p and "always" or "never"], jid); + + elseif p == true then -- Yes, this is ugly. FIXME later. + values.default[1].default = true; + elseif p == false then + values.default[2].default = true; + elseif p == "roster" then + values.default[3].default = true; + end + end + return { status = "executing", actions = { "complete" }, form = { layout = mam_prefs_form, values = values } }, true; + end +end + +module:provides("adhoc", module:require"adhoc".new("Archive settings", "urn:xmpp:mam#configure", mam_prefs_handler)); diff -r 9bbd99f2057a -r 9d5731af2c27 mod_pubsub_feeds/mod_pubsub_feeds.lua --- a/mod_pubsub_feeds/mod_pubsub_feeds.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_pubsub_feeds/mod_pubsub_feeds.lua Fri Jul 27 14:29:59 2012 +0100 @@ -5,7 +5,7 @@ -- Config: -- Component "pubsub.example.com" "pubsub" -- modules_enabled = { --- "pubsub_feed"; +-- "pubsub_feeds"; -- } -- feeds = { -- node -> url -- prosody_blog = "http://blog.prosody.im/feed/atom.xml"; @@ -69,7 +69,7 @@ end end update_config(); -module:hook("config-reloaded", update_config); +module:hook_global("config-reloaded", update_config); local actor = module.host.."/"..module.name; @@ -160,12 +160,13 @@ return module:http_url(nil, "/callback") .. "?node=" .. urlencode(node); end -function subscribe(feed) +function subscribe(feed, want) + want = want or "subscribe"; feed.token = uuid(); - feed.secret = uuid(); + feed.secret = feed.secret or uuid(); local body = formencode{ ["hub.callback"] = format_url(feed.node); - ["hub.mode"] = "subscribe"; --TODO unsubscribe + ["hub.mode"] = want; ["hub.topic"] = feed.url; ["hub.verify"] = "async"; ["hub.verify_token"] = feed.token; @@ -176,7 +177,7 @@ --module:log("debug", "subscription request, body: %s", body); --FIXME The subscription states and related stuff - feed.subscription = "subscribe"; + feed.subscription = want; http.request(feed.hub, { body = body }, function(data, code, req) module:log("debug", "subscription to %s submitted, status %s", feed.node, tostring(code)); if code >= 400 then @@ -200,8 +201,12 @@ --module:log("debug", "Headers: %s", dump(request.headers)); local feed = feed_list[query.node]; + if not feed then + return 404; + end + if method == "GET" then - if query.node and feed then + if query.node then if query["hub.topic"] ~= feed.url then module:log("debug", "Invalid topic: %s", tostring(query["hub.topic"])) return 404 @@ -216,15 +221,14 @@ end if query["hub.verify_token"] ~= feed.token then module:log("debug", "Invalid verify_token: %s", tostring(query["hub.verify_token"])) - return 401 + return 401; end module:log("debug", "Confirming %s request to %s", feed.subscription, feed.url) return query["hub.challenge"]; end return 400; elseif method == "POST" then - local body = request.body; - if #body > 0 and feed then + if #body > 0 then module:log("debug", "got %d bytes PuSHed for %s", #body, query.node); local signature = request.headers.x_hub_signature; if feed.secret then diff -r 9bbd99f2057a -r 9d5731af2c27 mod_readonly/mod_readonly.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_readonly/mod_readonly.lua Fri Jul 27 14:29:59 2012 +0100 @@ -0,0 +1,26 @@ +local st = require "util.stanza"; + +local stores = module:get_option("readonly_stores", { + vcard = { "vcard-temp", "vCard" }; +}); + +local namespaces = {}; +for name, namespace in pairs(stores) do + namespaces[table.concat(namespace, ":")] = name; +end + +function prevent_write(event) + local stanza = event.stanza; + if stanza.attr.type ~= "set" then return; end + local xmlns_and_tag = stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name; + local store_name = namespaces[xmlns_and_tag]; + if store_name then + module:log("warn", "Preventing modification of %s store by %s", store_name, stanza.attr.from); + event.origin.send(st.error_reply(stanza, "cancel", "not-allowed", store_name.." data is read-only")); + return true; -- Block stanza + end +end + +for namespace in pairs(namespaces) do + module:hook("iq/bare/"..namespace, prevent_write, 200); +end diff -r 9bbd99f2057a -r 9d5731af2c27 mod_register_json/mod_register_json.lua --- a/mod_register_json/mod_register_json.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_register_json/mod_register_json.lua Fri Jul 27 14:29:59 2012 +0100 @@ -6,7 +6,7 @@ local jid_prep = require "util.jid".prep local jid_split = require "util.jid".split -local usermanager = require "core.usermanager" +local usermanager = usermanager local b64_decode = require "util.encodings".base64.decode local json_decode = require "util.json".decode local os_time = os.time @@ -78,29 +78,31 @@ module:log("warn", "%s tried to submit registration data for %s but he's not an admin", user, req_body["host"]) return http_response(event, 401, "I obey only to my masters... Have a nice day.") else - -- Checks for both Throttling/Whitelist and Blacklist (basically copycatted from prosody's register.lua code) + -- Blacklist can be checked here. if blacklist:contains(req_body["ip"]) then module:log("warn", "Attempt of reg. submission to the JSON servlet from blacklisted address: %s", req_body["ip"]) ; return http_response(403, "The specified address is blacklisted, sorry sorry.") end - if throttle_time and not whitelist:contains(req_body["ip"]) then - if not recent_ips[req_body["ip"]] then - recent_ips[req_body["ip"]] = os_time() - else - if os_time() - recent_ips[req_body["ip"]] < throttle_time then - recent_ips[req_body["ip"]] = os_time() - module:log("warn", "JSON Registration request from %s has been throttled.", req_body["ip"]) - return http_response(event, 503, "Woah... How many users you want to register..? Request throttled, wait a bit and try again.") - end - recent_ips[req_body["ip"]] = os_time() - end - end -- We first check if the supplied username for registration is already there. -- And nodeprep the username local username = nodeprep(req_body["username"]) - if not usermanager.user_exists(username, req_body["host"]) then - if not username then - module:log("debug", "%s supplied an username containing invalid characters: %s", user, username) - return http_response(event, 406, "Supplied username contains invalid characters, see RFC 6122.") - else + if not username then + module:log("debug", "%s supplied an username containing invalid characters: %s", user, username) + return http_response(event, 406, "Supplied username contains invalid characters, see RFC 6122.") + else + if not usermanager.user_exists(username, req_body["host"]) then + -- if username fails to register successive requests shouldn't be throttled until one is successful. + if throttle_time and not whitelist:contains(req_body["ip"]) then + if not recent_ips[req_body["ip"]] then + recent_ips[req_body["ip"]] = os_time() + else + if os_time() - recent_ips[req_body["ip"]] < throttle_time then + recent_ips[req_body["ip"]] = os_time() + module:log("warn", "JSON Registration request from %s has been throttled.", req_body["ip"]) + return http_response(event, 503, "Woah... How many users you want to register..? Request throttled, wait a bit and try again.") + end + recent_ips[req_body["ip"]] = os_time() + end + end + local ok, error = usermanager.create_user(username, req_body["password"], req_body["host"]) if ok then hosts[req_body["host"]].events.fire_event("user-registered", { username = username, host = req_body["host"], source = "mod_register_json", session = { ip = req_body["ip"] } }) @@ -110,10 +112,10 @@ module:log("error", "user creation failed: "..error) return http_response(event, 500, "Encountered server error while creating the user: "..error) end + else + module:log("debug", "%s registration data submission for %s failed (user already exists)", user, username) + return http_response(event, 409, "User already exists.") end - else - module:log("debug", "%s registration data submission for %s failed (user already exists)", user, username) - return http_response(event, 409, "User already exists.") end end end diff -r 9bbd99f2057a -r 9d5731af2c27 mod_register_redirect/mod_register_redirect.lua --- a/mod_register_redirect/mod_register_redirect.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_register_redirect/mod_register_redirect.lua Fri Jul 27 14:29:59 2012 +0100 @@ -6,7 +6,7 @@ -- 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 = require "core.configmanager" +local cman = configmanager function reg_redirect(event) local stanza, origin = event.stanza, event.origin diff -r 9bbd99f2057a -r 9d5731af2c27 mod_register_web/mod_register_web.lua --- a/mod_register_web/mod_register_web.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_register_web/mod_register_web.lua Fri Jul 27 14:29:59 2012 +0100 @@ -1,4 +1,5 @@ local captcha_options = module:get_option("captcha_options", {}); +local nodeprep = require "util.encodings".stringprep.nodeprep; function generate_captcha(display_options) return (([[ @@ -50,10 +51,11 @@ end function register_user(form) - if usermanager.user_exists(form.username, module.host) then - return nil, "user-exists"; - end - return usermanager.create_user(form.username, form.password, module.host); + local prepped_username = nodeprep(form.username); + if usermanager.user_exists(prepped_username, module.host) then + return nil, "user-exists"; + end + return usermanager.create_user(prepped_username, form.password, module.host); end function generate_success(event, form) diff -r 9bbd99f2057a -r 9d5731af2c27 mod_streamstats/mod_streamstats.lua --- a/mod_streamstats/mod_streamstats.lua Mon Jun 11 22:32:45 2012 +0200 +++ b/mod_streamstats/mod_streamstats.lua Fri Jul 27 14:29:59 2012 +0100 @@ -1,11 +1,14 @@ -local stats = prosody.stats; +module:set_global(); +local stats = module:shared"stats"; local iter = require "util.iterators"; local count, keys = iter.count, iter.keys; -if not stats then - stats = { - stats = {}; conns = {}; - +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 @@ -24,75 +27,78 @@ 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; }; - prosody.stats = stats; - - 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 - - require "util.iterators"; - require "util.timer".add_task(1, function () - stats:set("s2s-in", count(keys(prosody.incoming_s2s))); - return math.random(10, 20); - end); - require "util.timer".add_task(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); - - require "net.connlisteners".register("stats", network); - require "net.connlisteners".start("stats", { port = module:get_option("stats_ports") or 5444, interface = "127.0.0.1" }); +}); + +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:hook("resource-bind", function () - stats:adjust("c2s", 1); +module:add_timer(1, function () + stats:set("s2s-in", count(keys(prosody.incoming_s2s))); + return math.random(10, 20); end); -module:hook("resource-unbind", function () - stats:adjust("c2s", -1); +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); -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; + +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 -end -stats:adjust("c2s", c2s_count); + 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); + 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; +}); diff -r 9bbd99f2057a -r 9d5731af2c27 mod_vjud/mod_vjud.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_vjud/mod_vjud.lua Fri Jul 27 14:29:59 2012 +0100 @@ -0,0 +1,155 @@ +local 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 st = require "util.stanza"; +local template = require "util.template"; + +local get_reply = template[[ + + Fill in one or more fields to search for any matching Jabber users. + + + + + +]].apply({}); +local item_template = template[[ + + {first} + {last} + {nick} + {email} + +]]; + +module:add_feature("jabber:iq:search"); + +local opted_in; +function module.load() + opted_in = dm_load(nil, module.host, "user_index") or {}; +end +function module.unload() + dm_store(nil, module.host, "user_index", opted_in); +end + +local opt_in_layout = dataforms_new{ + title = "Search settings"; + instructions = "Do you want to appear in search results?"; + { + name = "searchable", + label = "Appear in search results?", + type = "boolean", + }, +}; +local vCard_mt = { + __index = function(t, k) + if type(k) ~= "string" then return nil end + 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) + local vCard = dm_load(user, module.host, "vcard"); + if vCard then + vCard = st.deserialize(vCard); + vCard = vcard.from_xep54(vCard); + return setmetatable(vCard, vCard_mt); + end +end + +local at_host = "@"..module.host; + +module:hook("iq/host/jabber:iq:search:query", function(event) + local origin, stanza = event.origin, event.stanza; + + if stanza.attr.type == "get" then + origin.send(st.reply(stanza):add_child(get_reply)); + else -- type == "set" + local query = stanza.tags[1]; + local first, last, nick, email = + (query:get_child_text"first" or false), + (query:get_child_text"last" or false), + (query:get_child_text"nick" or false), + (query:get_child_text"email" or false); + + if not ( first or last or nick or email ) then + origin.send(st.error_reply(stanza, "modify", "not-acceptable", "All fields were empty")); + return true; + end + + local reply = st.reply(stanza):query("jabber:iq:search"); + + local username, hostname = jid_split(email); + if hostname == module.host and username and usermanager.user_exists(username, hostname) then + local vCard = get_user_vcard(username); + if vCard 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 + else + for username in pairs(opted_in) do + local vCard = get_user_vcard(username); + if vCard and ( + (vCard.N and vCard.N[2] == first) or + (vCard.N and vCard.N[1] == last) or + (vCard.NICKNAME and vCard.NICKNAME[1] == nick) or + (vCard.EMAIL and vCard.EMAIL[1] == email)) then + 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); + +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 ~= module.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, data = { 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); + diff -r 9bbd99f2057a -r 9d5731af2c27 mod_vjud/vcard.lib.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_vjud/vcard.lib.lua Fri Jul 27 14:29:59 2012 +0100 @@ -0,0 +1,464 @@ +-- 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 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].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 do + t_insert(prop, item[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; +};