diff options
Diffstat (limited to 'plugins')
34 files changed, 1761 insertions, 313 deletions
diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua new file mode 100644 index 00000000..361f4731 --- /dev/null +++ b/plugins/adhoc/adhoc.lib.lua @@ -0,0 +1,85 @@ +-- 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 st, uuid = require "util.stanza", require "util.uuid"; + +local xmlns_cmd = "http://jabber.org/protocol/commands"; + +local states = {} + +local _M = {}; + +function _cmdtag(desc, status, sessionid, action) + local cmd = st.stanza("command", { xmlns = xmlns_cmd, node = desc.node, status = status }); + if sessionid then cmd.attr.sessionid = sessionid; end + if action then cmd.attr.action = action; end + + return cmd; +end + +function _M.new(name, node, handler, permission) + return { name = name, node = node, handler = handler, cmdtag = _cmdtag, permission = (permission or "user") }; +end + +function _M.handle_cmd(command, origin, stanza) + local sessionid = stanza.tags[1].attr.sessionid or uuid.generate(); + local dataIn = {}; + dataIn.to = stanza.attr.to; + dataIn.from = stanza.attr.from; + dataIn.action = stanza.tags[1].attr.action or "execute"; + dataIn.form = stanza.tags[1]:child_with_ns("jabber:x:data"); + + local data, state = command:handler(dataIn, states[sessionid]); + states[sessionid] = state; + local stanza = st.reply(stanza); + if data.status == "completed" then + states[sessionid] = nil; + cmdtag = command:cmdtag("completed", sessionid); + elseif data.status == "canceled" then + states[sessionid] = nil; + cmdtag = command:cmdtag("canceled", sessionid); + elseif data.status == "error" then + states[sessionid] = nil; + stanza = st.error_reply(stanza, data.error.type, data.error.condition, data.error.message); + origin.send(stanza); + return true; + else + cmdtag = command:cmdtag("executing", sessionid); + end + + for name, content in pairs(data) do + if name == "info" then + cmdtag:tag("note", {type="info"}):text(content):up(); + elseif name == "warn" then + cmdtag:tag("note", {type="warn"}):text(content):up(); + elseif name == "error" then + cmdtag:tag("note", {type="error"}):text(content.message):up(); + elseif name =="actions" then + local actions = st.stanza("actions"); + for _, action in ipairs(content) do + if (action == "prev") or (action == "next") or (action == "complete") then + actions:tag(action):up(); + else + module:log("error", 'Command "'..command.name.. + '" at node "'..command.node..'" provided an invalid action "'..action..'"'); + end + end + cmdtag:add_child(actions); + elseif name == "form" then + cmdtag:add_child((content.layout or content):form(content.data)); + elseif name == "result" then + cmdtag:add_child((content.layout or content):form(content.data, "result")); + elseif name == "other" then + cmdtag:add_child(content); + end + end + stanza:add_child(cmdtag); + origin.send(stanza); + + return true; +end + +return _M; diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua new file mode 100644 index 00000000..41362d81 --- /dev/null +++ b/plugins/adhoc/mod_adhoc.lua @@ -0,0 +1,74 @@ +-- 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 is_admin = require "core.usermanager".is_admin; +local adhoc_handle_cmd = module:require "adhoc".handle_cmd; +local xmlns_cmd = "http://jabber.org/protocol/commands"; +local xmlns_disco = "http://jabber.org/protocol/disco"; +local commands = {}; + +module:add_feature(xmlns_cmd); + +module:hook("iq/host/"..xmlns_disco.."#items:query", function (event) + local origin, stanza = event.origin, event.stanza; + local privileged = is_admin(stanza.attr.from, stanza.attr.to); + if stanza.attr.type == "get" and stanza.tags[1].attr.node + and stanza.tags[1].attr.node == xmlns_cmd then + reply = st.reply(stanza); + reply:tag("query", { xmlns = xmlns_disco.."#items", + node = xmlns_cmd }); + for node, command in pairs(commands) do + if (command.permission == "admin" and privileged) + or (command.permission == "user") then + reply:tag("item", { name = command.name, + node = node, jid = module:get_host() }); + reply:up(); + end + end + origin.send(reply); + return true; + end +end, 500); + +module:hook("iq/host", function (event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type == "set" and stanza.tags[1] + and stanza.tags[1].name == "command" then + local node = stanza.tags[1].attr.node + -- TODO: Is this correct, or should is_admin be changed? + local privileged = is_admin(event.stanza.attr.from) + or is_admin(stanza.attr.from, stanza.attr.to); + if commands[node] then + if commands[node].permission == "admin" + and not privileged then + origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up() + :add_child(commands[node]:cmdtag("canceled") + :tag("note", {type="error"}):text("You don't have permission to execute this command"))); + return true + end + -- User has permission now execute the command + return adhoc_handle_cmd(commands[node], origin, stanza); + end + end +end, 500); + +local function handle_item_added(item) + commands[item.node] = item; +end + +module:hook("item-added/adhoc", function (event) + return handle_item_added(event.item); +end, 500); + +module:hook("item-removed/adhoc", function (event) + commands[event.item.node] = nil; +end, 500); + +-- Pick up any items that are already added +for _, item in ipairs(module:get_host_items("adhoc")) do + handle_item_added(item); +end diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua index d3017f6c..77555bec 100644 --- a/plugins/mod_announce.lua +++ b/plugins/mod_announce.lua @@ -6,14 +6,38 @@ -- COPYING file in the source package for more information. -- -local st, jid, set = require "util.stanza", require "util.jid", require "util.set"; +local st, jid = require "util.stanza", require "util.jid"; local is_admin = require "core.usermanager".is_admin; -local admins = set.new(config.get(module:get_host(), "core", "admins")); -function handle_announcement(data) - local origin, stanza = data.origin, data.stanza; - local host, resource = select(2, jid.split(stanza.attr.to)); +function send_to_online(message, host) + local sessions; + if host then + sessions = { [host] = hosts[host] }; + else + sessions = hosts; + end + + local c = 0; + for hostname, host_session in pairs(sessions) do + if host_session.sessions then + message.attr.from = hostname; + for username in pairs(host_session.sessions) do + c = c + 1; + message.attr.to = username.."@"..hostname; + core_post_stanza(host_session, message); + end + end + end + + return c; +end + + +-- Old <message>-based jabberd-style announcement sending +function handle_announcement(event) + local origin, stanza = event.origin, event.stanza; + local node, host, resource = jid.split(stanza.attr.to); if resource ~= "announce/online" then return; -- Not an announcement @@ -21,25 +45,56 @@ function handle_announcement(data) if not is_admin(stanza.attr.from) then -- Not an admin? Not allowed! - module:log("warn", "Non-admin %s tried to send server announcement", tostring(jid.bare(stanza.attr.from))); + module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from); return; end module:log("info", "Sending server announcement to all online users"); - local host_session = hosts[host]; local message = st.clone(stanza); message.attr.type = "headline"; message.attr.from = host; - local c = 0; - for user in pairs(host_session.sessions) do - c = c + 1; - message.attr.to = user.."@"..host; - core_post_stanza(host_session, message); - end - + local c = send_to_online(message, host); module:log("info", "Announcement sent to %d online users", c); return true; end - module:hook("message/host", handle_announcement); + +-- Ad-hoc command (XEP-0133) +local dataforms_new = require "util.dataforms".new; +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" }; +}; + +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); + + 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 to %d online users"):format(count) }; + else + return { status = "executing", form = announce_layout }, "executing"; + end + + return true; +end + +local adhoc_new = module:require "adhoc".new; +local announce_desc = adhoc_new("Send Announcement to Online Users", "http://jabber.org/protocol/admin#announce", announce_handler, "admin"); +module:add_item("adhoc", announce_desc); + diff --git a/plugins/mod_auth_anonymous.lua b/plugins/mod_auth_anonymous.lua new file mode 100644 index 00000000..35e87a2e --- /dev/null +++ b/plugins/mod_auth_anonymous.lua @@ -0,0 +1,69 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local log = require "util.logger".init("auth_anonymous"); +local new_sasl = require "util.sasl".new; +local datamanager = require "util.datamanager"; + +function new_default_provider(host) + local provider = { name = "anonymous" }; + + function provider.test_password(username, password) + return nil, "Password based auth not supported."; + end + + function provider.get_password(username) + return nil, "Password not available."; + end + + function provider.set_password(username, password) + return nil, "Password based auth not supported."; + end + + function provider.user_exists(username) + return nil, "Only anonymous users are supported."; -- FIXME check if anonymous user is connected? + end + + function provider.create_user(username, password) + return nil, "Account creation/modification not supported."; + end + + function provider.get_sasl_handler() + local realm = module:get_option("sasl_realm") or module.host; + local anonymous_authentication_profile = { + anonymous = function(username, realm) + return true; -- for normal usage you should always return true here + end + }; + return new_sasl(realm, anonymous_authentication_profile); + end + + return provider; +end + +local function dm_callback(username, host, datastore, data) + if host == module.host then + return false; + end + return username, host, datastore, data; +end +local host = hosts[module.host]; +local _saved_disallow_s2s = host.disallow_s2s; +function module.load() + _saved_disallow_s2s = host.disallow_s2s; + host.disallow_s2s = module:get_option("disallow_s2s") ~= false; + datamanager.add_callback(dm_callback); +end +function module.unload() + host.disallow_s2s = _saved_disallow_s2s; + datamanager.remove_callback(dm_callback); +end + +module:add_item("auth-provider", new_default_provider(module.host)); + diff --git a/plugins/mod_auth_cyrus.lua b/plugins/mod_auth_cyrus.lua new file mode 100644 index 00000000..7c23ea21 --- /dev/null +++ b/plugins/mod_auth_cyrus.lua @@ -0,0 +1,61 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local log = require "util.logger".init("auth_cyrus"); + +local cyrus_service_realm = module:get_option("cyrus_service_realm"); +local cyrus_service_name = module:get_option("cyrus_service_name"); +local cyrus_application_name = module:get_option("cyrus_application_name"); + +prosody.unlock_globals(); --FIXME: Figure out why this is needed and + -- why cyrussasl isn't caught by the sandbox +local cyrus_new = require "util.sasl_cyrus".new; +prosody.lock_globals(); +local new_sasl = function(realm) + return cyrus_new( + cyrus_service_realm or realm, + cyrus_service_name or "xmpp", + cyrus_application_name or "prosody" + ); +end + +function new_default_provider(host) + local provider = { name = "cyrus" }; + log("debug", "initializing default authentication provider for host '%s'", host); + + function provider.test_password(username, password) + return nil, "Legacy auth not supported with Cyrus SASL."; + end + + function provider.get_password(username) + return nil, "Passwords unavailable for Cyrus SASL."; + end + + function provider.set_password(username, password) + return nil, "Passwords unavailable for Cyrus SASL."; + end + + function provider.user_exists(username) + return true; + end + + function provider.create_user(username, password) + return nil, "Account creation/modification not available with Cyrus SASL."; + end + + function provider.get_sasl_handler() + local realm = module:get_option("sasl_realm") or module.host; + return new_sasl(realm); + end + + return provider; +end + +module:add_item("auth-provider", new_default_provider(module.host)); + diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua new file mode 100644 index 00000000..2bee141c --- /dev/null +++ b/plugins/mod_auth_internal_hashed.lua @@ -0,0 +1,178 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local datamanager = require "util.datamanager"; +local log = require "util.logger".init("auth_internal_hashed"); +local type = type; +local error = error; +local ipairs = ipairs; +local hashes = require "util.hashes"; +local jid_bare = require "util.jid".bare; +local getAuthenticationDatabaseSHA1 = require "util.sasl.scram".getAuthenticationDatabaseSHA1; +local config = require "core.configmanager"; +local usermanager = require "core.usermanager"; +local generate_uuid = require "util.uuid".generate; +local new_sasl = require "util.sasl".new; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local hosts = hosts; + +-- COMPAT w/old trunk: remove these two lines before 0.8 release +local hmac_sha1 = require "util.hmac".sha1; +local sha1 = require "util.hashes".sha1; + +local to_hex; +do + local function replace_byte_with_hex(byte) + return ("%02x"):format(byte:byte()); + end + function to_hex(binary_string) + return binary_string:gsub(".", replace_byte_with_hex); + end +end + +local from_hex; +do + local function replace_hex_with_byte(hex) + return string.char(tonumber(hex, 16)); + end + function from_hex(hex_string) + return hex_string:gsub("..", replace_hex_with_byte); + end +end + + +local prosody = _G.prosody; + +-- Default; can be set per-user +local iteration_count = 4096; + +function new_hashpass_provider(host) + local provider = { name = "internal_hashed" }; + log("debug", "initializing hashpass authentication provider for host '%s'", host); + + function provider.test_password(username, password) + local credentials = datamanager.load(username, host, "accounts") or {}; + + if credentials.password ~= nil and string.len(credentials.password) ~= 0 then + if credentials.password ~= password then + return nil, "Auth failed. Provided password is incorrect."; + end + + if provider.set_password(username, credentials.password) == nil then + return nil, "Auth failed. Could not set hashed password from plaintext."; + else + return true; + end + end + + if credentials.iteration_count == nil or credentials.salt == nil or string.len(credentials.salt) == 0 then + return nil, "Auth failed. Stored salt and iteration count information is not complete."; + end + + -- convert hexpass to stored_key and server_key + -- COMPAT w/old trunk: remove before 0.8 release + if credentials.hashpass then + local salted_password = from_hex(credentials.hashpass); + credentials.stored_key = sha1(hmac_sha1(salted_password, "Client Key"), true); + credentials.server_key = to_hex(hmac_sha1(salted_password, "Server Key")); + credentials.hashpass = nil + datamanager.store(username, host, "accounts", credentials); + end + + local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, credentials.salt, credentials.iteration_count); + + local stored_key_hex = to_hex(stored_key); + local server_key_hex = to_hex(server_key); + + if valid and stored_key_hex == credentials.stored_key and server_key_hex == credentials.server_key then + return true; + else + return nil, "Auth failed. Invalid username, password, or password hash information."; + end + end + + function provider.set_password(username, password) + local account = datamanager.load(username, host, "accounts"); + if account then + account.salt = account.salt or generate_uuid(); + account.iteration_count = account.iteration_count or iteration_count; + local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, account.salt, account.iteration_count); + local stored_key_hex = to_hex(stored_key); + local server_key_hex = to_hex(server_key); + + account.stored_key = stored_key_hex + account.server_key = server_key_hex + + account.password = nil; + return datamanager.store(username, host, "accounts", account); + end + return nil, "Account not available."; + end + + function provider.user_exists(username) + local account = datamanager.load(username, host, "accounts"); + if not account then + log("debug", "account not found for username '%s' at host '%s'", username, module.host); + return nil, "Auth failed. Invalid username"; + end + return true; + end + + function provider.create_user(username, password) + local salt = generate_uuid(); + local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, iteration_count); + local stored_key_hex = to_hex(stored_key); + local server_key_hex = to_hex(server_key); + return datamanager.store(username, host, "accounts", {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = iteration_count}); + end + + function provider.get_sasl_handler() + local realm = module:get_option("sasl_realm") or module.host; + local testpass_authentication_profile = { + plain_test = function(username, password, realm) + local prepped_username = nodeprep(username); + if not prepped_username then + log("debug", "NODEprep failed on username: %s", username); + return "", nil; + end + return usermanager.test_password(prepped_username, realm, password), true; + end, + scram_sha_1 = function(username, realm) + local credentials = datamanager.load(username, host, "accounts"); + if not credentials then return; end + if credentials.password then + usermanager.set_password(username, credentials.password, host); + credentials = datamanager.load(username, host, "accounts"); + if not credentials then return; end + end + + -- convert hexpass to stored_key and server_key + -- COMPAT w/old trunk: remove before 0.8 release + if credentials.hashpass then + local salted_password = from_hex(credentials.hashpass); + credentials.stored_key = sha1(hmac_sha1(salted_password, "Client Key"), true); + credentials.server_key = to_hex(hmac_sha1(salted_password, "Server Key")); + credentials.hashpass = nil + datamanager.store(username, host, "accounts", credentials); + end + + local stored_key, server_key, iteration_count, salt = credentials.stored_key, credentials.server_key, credentials.iteration_count, credentials.salt; + stored_key = stored_key and from_hex(stored_key); + server_key = server_key and from_hex(server_key); + return stored_key, server_key, iteration_count, salt, true; + end + }; + return new_sasl(realm, testpass_authentication_profile); + end + + return provider; +end + +module:add_item("auth-provider", new_hashpass_provider(module.host)); + diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua new file mode 100644 index 00000000..fdc1216c --- /dev/null +++ b/plugins/mod_auth_internal_plain.lua @@ -0,0 +1,97 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local datamanager = require "util.datamanager"; +local log = require "util.logger".init("auth_internal_plain"); +local type = type; +local error = error; +local ipairs = ipairs; +local hashes = require "util.hashes"; +local jid_bare = require "util.jid".bare; +local config = require "core.configmanager"; +local usermanager = require "core.usermanager"; +local new_sasl = require "util.sasl".new; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local hosts = hosts; + +local prosody = _G.prosody; + +local is_cyrus = usermanager.is_cyrus; + +function new_default_provider(host) + local provider = { name = "internal_plain" }; + log("debug", "initializing default authentication provider for host '%s'", host); + + function provider.test_password(username, password) + log("debug", "test password '%s' for user %s at host %s", password, username, module.host); + if is_cyrus(host) then return nil, "Legacy auth not supported with Cyrus SASL."; end + local credentials = datamanager.load(username, host, "accounts") or {}; + + if password == credentials.password then + return true; + else + return nil, "Auth failed. Invalid username or password."; + end + end + + function provider.get_password(username) + log("debug", "get_password for username '%s' at host '%s'", username, module.host); + if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end + return (datamanager.load(username, host, "accounts") or {}).password; + end + + function provider.set_password(username, password) + if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end + local account = datamanager.load(username, host, "accounts"); + if account then + account.password = password; + return datamanager.store(username, host, "accounts", account); + end + return nil, "Account not available."; + end + + function provider.user_exists(username) + if is_cyrus(host) then return true; end + local account = datamanager.load(username, host, "accounts"); + if not account then + log("debug", "account not found for username '%s' at host '%s'", username, module.host); + return nil, "Auth failed. Invalid username"; + end + return true; + end + + function provider.create_user(username, password) + if is_cyrus(host) then return nil, "Account creation/modification not available with Cyrus SASL."; end + return datamanager.store(username, host, "accounts", {password = password}); + end + + function provider.get_sasl_handler() + local realm = module:get_option("sasl_realm") or module.host; + local getpass_authentication_profile = { + plain = function(username, realm) + local prepped_username = nodeprep(username); + if not prepped_username then + log("debug", "NODEprep failed on username: %s", username); + return "", nil; + end + local password = usermanager.get_password(prepped_username, realm); + if not password then + return "", nil; + end + return password, true; + end + }; + return new_sasl(realm, getpass_authentication_profile); + end + + return provider; +end + +module:add_item("auth-provider", new_default_provider(module.host)); + diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua index 66a79785..e03b2bbd 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -190,6 +190,11 @@ function stream_callbacks.streamopened(request, attr) local r, send_buffer = session.requests, session.send_buffer; local response = { headers = default_headers } function session.send(s) + -- We need to ensure that outgoing stanzas have the jabber:client xmlns + if s.attr and not s.attr.xmlns then + s = st.clone(s); + s.attr.xmlns = "jabber:client"; + end --log("debug", "Sending BOSH data: %s", tostring(s)); local oldest_request = r[1]; if oldest_request then diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua index 7efb4f9c..03331743 100644 --- a/plugins/mod_component.lua +++ b/plugins/mod_component.lua @@ -50,6 +50,8 @@ function handle_component_auth(session, stanza) -- Authenticated now log("info", "Component authenticated: %s", session.host); + session.component_validate_from = module:get_option_boolean("validate_from_addresses") ~= false; + -- If component not already created for this host, create one now if not hosts[session.host].connected then local send = session.send; diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua index 53341492..d890931f 100644 --- a/plugins/mod_compression.lua +++ b/plugins/mod_compression.lua @@ -14,6 +14,7 @@ local xmlns_compression_feature = "http://jabber.org/features/compress" local xmlns_compression_protocol = "http://jabber.org/protocol/compress" local xmlns_stream = "http://etherx.jabber.org/streams"; local compression_stream_feature = st.stanza("compression", {xmlns=xmlns_compression_feature}):tag("method"):text("zlib"):up(); +local add_filter = require "util.filters".add_filter; local compression_level = module:get_option("compression_level"); -- if not defined assume admin wants best compression @@ -94,43 +95,36 @@ end -- setup compression for a stream local function setup_compression(session, deflate_stream) - local old_send = (session.sends2s or session.send); - - local new_send = function(t) - --TODO: Better code injection in the sending process - local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync'); - if status == false then - session:close({ - condition = "undefined-condition"; - text = compressed; - extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); - }); - module:log("warn", "%s", tostring(compressed)); - return; - end - session.conn:write(compressed); - end; - - if session.sends2s then session.sends2s = new_send - elseif session.send then session.send = new_send end + add_filter(session, "bytes/out", function(t) + local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync'); + if status == false then + module:log("warn", "%s", tostring(compressed)); + session:close({ + condition = "undefined-condition"; + text = compressed; + extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); + }); + return; + end + return compressed; + end); end -- setup decompression for a stream local function setup_decompression(session, inflate_stream) - local old_data = session.data - session.data = function(conn, data) - local status, decompressed, eof = pcall(inflate_stream, data); - if status == false then - session:close({ - condition = "undefined-condition"; - text = decompressed; - extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); - }); - module:log("warn", "%s", tostring(decompressed)); - return; - end - old_data(conn, decompressed); - end; + add_filter(session, "bytes/in", function(data) + local status, decompressed, eof = pcall(inflate_stream, data); + if status == false then + module:log("warn", "%s", tostring(decompressed)); + session:close({ + condition = "undefined-condition"; + text = decompressed; + extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); + }); + return; + end + return decompressed; + end); end module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compression_protocol, @@ -148,12 +142,6 @@ module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compressio -- setup decompression for session.data setup_decompression(session, inflate_stream); - local session_reset_stream = session.reset_stream; - session.reset_stream = function(session) - session_reset_stream(session); - setup_decompression(session, inflate_stream); - return true; - end; session:reset_stream(); local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams", ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host}; @@ -195,12 +183,6 @@ module:add_handler({"c2s_unauthed", "c2s", "s2sin_unauthed", "s2sin"}, "compress -- setup decompression for session.data setup_decompression(session, inflate_stream); - local session_reset_stream = session.reset_stream; - session.reset_stream = function(session) - session_reset_stream(session); - setup_decompression(session, inflate_stream); - return true; - end; session.compressed = true; elseif method then session.log("debug", "%s compression selected, but we don't support it.", tostring(method)); diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua index ee0043f1..8bb0afec 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -11,6 +11,7 @@ local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed local jid_split = require "util.jid".split; local jid_bare = require "util.jid".bare; local st = require "util.stanza" +local calculate_hash = require "util.caps".calculate_hash; local disco_items = module:get_option("disco_items") or {}; do -- validate disco_items @@ -35,27 +36,61 @@ module:add_identity("server", "im", "Prosody"); -- FIXME should be in the non-ex module:add_feature("http://jabber.org/protocol/disco#info"); module:add_feature("http://jabber.org/protocol/disco#items"); -module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(event) - local origin, stanza = event.origin, event.stanza; - if stanza.attr.type ~= "get" then return; end - local node = stanza.tags[1].attr.node; - if node and node ~= "" then return; end -- TODO fire event? - - local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info"); +-- Generate and cache disco result and caps hash +local _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash; +local function build_server_disco_info() + local query = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" }); local done = {}; for _,identity in ipairs(module:get_host_items("identity")) do local identity_s = identity.category.."\0"..identity.type; if not done[identity_s] then - reply:tag("identity", identity):up(); + query:tag("identity", identity):up(); done[identity_s] = true; end end for _,feature in ipairs(module:get_host_items("feature")) do if not done[feature] then - reply:tag("feature", {var=feature}):up(); + query:tag("feature", {var=feature}):up(); done[feature] = true; end end + _cached_server_disco_info = query; + _cached_server_caps_hash = calculate_hash(query); + _cached_server_caps_feature = st.stanza("c", { + xmlns = "http://jabber.org/protocol/caps"; + hash = "sha-1"; + node = "http://prosody.im"; + ver = _cached_server_caps_hash; + }); +end +local function clear_disco_cache() + _cached_server_disco_info, _cached_server_caps_feature, _cached_server_caps_hash = nil, nil, nil; +end +local function get_server_disco_info() + if not _cached_server_disco_info then build_server_disco_info(); end + return _cached_server_disco_info; +end +local function get_server_caps_feature() + if not _cached_server_caps_feature then build_server_disco_info(); end + return _cached_server_caps_feature; +end +local function get_server_caps_hash() + if not _cached_server_caps_hash then build_server_disco_info(); end + return _cached_server_caps_hash; +end + +module:hook("item-added/identity", clear_disco_cache); +module:hook("item-added/feature", clear_disco_cache); + +-- Handle disco requests to the server +module:hook("iq/host/http://jabber.org/protocol/disco#info:query", function(event) + local origin, stanza = event.origin, event.stanza; + if stanza.attr.type ~= "get" then return; end + local node = stanza.tags[1].attr.node; + if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then return; end -- TODO fire event? + local reply_query = get_server_disco_info(); + reply_query.node = node; + local reply = st.reply(stanza):add_child(reply_query); origin.send(reply); return true; end); @@ -75,6 +110,13 @@ module:hook("iq/host/http://jabber.org/protocol/disco#items:query", function(eve origin.send(reply); return true; end); + +-- Handle caps stream feature +module:hook("stream-features", function (event) + event.features:add_child(get_server_caps_feature()); +end); + +-- Handle disco requests to user accounts module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(event) local origin, stanza = event.origin, event.stanza; if stanza.attr.type ~= "get" then return; end diff --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua index 5f821cbc..7a876f1d 100644 --- a/plugins/mod_groups.lua +++ b/plugins/mod_groups.lua @@ -29,6 +29,9 @@ function inject_roster_contacts(username, host, roster) if jid ~= bare_jid then if not roster[jid] then roster[jid] = {}; end roster[jid].subscription = "both"; + if groups[group_name][jid] then + roster[jid].name = groups[group_name][jid]; + end if not roster[jid].groups then roster[jid].groups = { [group_name] = true }; end @@ -100,10 +103,13 @@ function module.load() groups[curr_group] = groups[curr_group] or {}; else -- Add JID - local jid = jid_prep(line:match("%S+")); + local entryjid, name = line:match("([^=]*)=?(.*)"); + module:log("debug", "entryjid = '%s', name = '%s'", entryjid, name); + local jid; + jid = jid_prep(entryjid:match("%S+")); if jid then module:log("debug", "New member of %s: %s", tostring(curr_group), tostring(jid)); - groups[curr_group][jid] = true; + groups[curr_group][jid] = name or false; members[jid] = members[jid] or {}; members[jid][#members[jid]+1] = curr_group; end diff --git a/plugins/mod_httpserver.lua b/plugins/mod_httpserver.lua index c55bd20f..654aff06 100644 --- a/plugins/mod_httpserver.lua +++ b/plugins/mod_httpserver.lua @@ -8,9 +8,11 @@ local httpserver = require "net.httpserver"; +local lfs = require "lfs"; local open = io.open; local t_concat = table.concat; +local stat = lfs.attributes; local http_base = config.get("*", "core", "http_path") or "www_files"; @@ -48,7 +50,14 @@ local function preprocess_path(path) end function serve_file(path) - local f, err = open(http_base..path, "rb"); + 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(path.."/index.html"); + end + return response_403; + end + local f, err = open(full_path, "rb"); if not f then return response_404; end local data = f:read("*a"); f:close(); diff --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua index b3001fe5..e90af781 100644 --- a/plugins/mod_iq.lua +++ b/plugins/mod_iq.lua @@ -9,7 +9,6 @@ local st = require "util.stanza"; local jid_split = require "util.jid".split; -local user_exists = require "core.usermanager".user_exists; local full_sessions = full_sessions; local bare_sessions = bare_sessions; @@ -34,16 +33,6 @@ module:hook("iq/bare", function(data) -- IQ to bare JID recieved local origin, stanza = data.origin, data.stanza; - local to = stanza.attr.to; - if to and not bare_sessions[to] then -- quick check for account existance - local node, host = jid_split(to); - if not user_exists(node, host) then -- full check for account existance - if stanza.attr.type == "get" or stanza.attr.type == "set" then - origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); - end - return true; - end - end -- TODO fire post processing events if stanza.attr.type == "get" or stanza.attr.type == "set" then return module:fire_event("iq/bare/"..stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name, data); diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index 0134d736..2e1ca328 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -50,7 +50,7 @@ module:add_iq_handler("c2s_unauthed", "jabber:iq:auth", username = nodeprep(username); resource = resourceprep(resource) local reply = st.reply(stanza); - if usermanager.validate_credentials(session.host, username, password) then + if usermanager.test_password(username, session.host, password) then -- Authentication successful! local success, err = sessionmanager.make_authenticated(session, username); if success then diff --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua new file mode 100644 index 00000000..f323e606 --- /dev/null +++ b/plugins/mod_motd.lua @@ -0,0 +1,25 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2010 Jeff Mitchell +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local host = module:get_host(); +local motd_text = module:get_option("motd_text") or "MOTD: (blank)"; +local motd_jid = module:get_option("motd_jid") or host; + +local st = require "util.stanza"; + +module:hook("resource-bind", + function (event) + local session = event.session; + local motd_stanza = + st.message({ to = session.username..'@'..session.host, from = motd_jid }) + :tag("body"):text(motd_text); + core_route_stanza(hosts[host], motd_stanza); + module:log("debug", "MOTD send to user %s@%s", session.username, session.host); + +end); diff --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua new file mode 100644 index 00000000..24aef9ed --- /dev/null +++ b/plugins/mod_offline.lua @@ -0,0 +1,56 @@ +-- Prosody IM +-- Copyright (C) 2008-2009 Matthew Wild +-- Copyright (C) 2008-2009 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + + +local datamanager = require "util.datamanager"; +local st = require "util.stanza"; +local datetime = require "util.datetime"; +local ipairs = ipairs; +local jid_split = require "util.jid".split; + +module:add_feature("msgoffline"); + +module:hook("message/offline/store", function(event) + local origin, stanza = event.origin, event.stanza; + local to = stanza.attr.to; + local node, host; + if to then + node, host = jid_split(to) + else + node, host = origin.username, origin.host; + end + + stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy(); + local result = datamanager.list_append(node, host, "offline", st.preserialize(stanza)); + stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil; + + return true; +end); + +module:hook("message/offline/broadcast", function(event) + local origin = event.origin; + local node, host = origin.username, origin.host; + + local data = datamanager.list_load(node, host, "offline"); + if not data then return true; end + for _, stanza in ipairs(data) do + stanza = st.deserialize(stanza); + stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = stanza.attr.stamp}):up(); -- XEP-0203 + stanza:tag("x", {xmlns = "jabber:x:delay", from = host, stamp = stanza.attr.stamp_legacy}):up(); -- XEP-0091 (deprecated) + stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil; + origin.send(stanza); + end + return true; +end); + +module:hook("message/offline/delete", function(event) + local origin = event.origin; + local node, host = origin.username, origin.host; + + return datamanager.list_store(node, host, "offline", nil); +end); diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index aa46d2d3..5d476645 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -16,9 +16,7 @@ local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed local pairs, ipairs = pairs, ipairs; local next = next; local type = type; -local load_roster = require "core.rostermanager".load_roster; -local sha1 = require "util.hashes".sha1; -local base64 = require "util.encodings".base64.encode; +local calculate_hash = require "util.caps".calculate_hash; local NULL = {}; local data = {}; @@ -40,8 +38,8 @@ module:add_feature("http://jabber.org/protocol/pubsub#publish"); local function subscription_presence(user_bare, recipient) local recipient_bare = jid_bare(recipient); if (recipient_bare == user_bare) then return true end - local item = load_roster(jid_split(user_bare))[recipient_bare]; - return item and (item.subscription == 'from' or item.subscription == 'both'); + local username, host = jid_split(user_bare); + return is_contact_subscribed(username, host, recipient_bare); end local function publish(session, node, id, item) @@ -118,27 +116,32 @@ module:hook("presence/bare", function(event) -- inbound presence to bare JID recieved local origin, stanza = event.origin, event.stanza; local user = stanza.attr.to or (origin.username..'@'..origin.host); + local t = stanza.attr.type; - if not stanza.attr.to or subscription_presence(user, stanza.attr.from) then - local recipient = stanza.attr.from; - local current = recipients[user] and recipients[user][recipient]; - local hash = get_caps_hash_from_presence(stanza, current); - if current == hash then return; end - if not hash then - if recipients[user] then recipients[user][recipient] = nil; end - else - recipients[user] = recipients[user] or {}; - if hash_map[hash] then - recipients[user][recipient] = hash_map[hash]; - publish_all(user, recipient, origin); + if not t then -- available presence + if not stanza.attr.to or subscription_presence(user, stanza.attr.from) then + local recipient = stanza.attr.from; + local current = recipients[user] and recipients[user][recipient]; + local hash = get_caps_hash_from_presence(stanza, current); + if current == hash then return; end + if not hash then + if recipients[user] then recipients[user][recipient] = nil; end else - recipients[user][recipient] = hash; - origin.send( - st.stanza("iq", {from=stanza.attr.to, to=stanza.attr.from, id="disco", type="get"}) - :query("http://jabber.org/protocol/disco#info") - ); + recipients[user] = recipients[user] or {}; + if hash_map[hash] then + recipients[user][recipient] = hash_map[hash]; + publish_all(user, recipient, origin); + else + recipients[user][recipient] = hash; + origin.send( + st.stanza("iq", {from=stanza.attr.to, to=stanza.attr.from, id="disco", type="get"}) + :query("http://jabber.org/protocol/disco#info") + ); + end end end + elseif t == "unavailable" then + if recipients[user] then recipients[user][stanza.attr.from] = nil; end end end, 10); @@ -205,50 +208,6 @@ module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) end end); -local function calculate_hash(disco_info) - local identities, features, extensions = {}, {}, {}; - for _, tag in pairs(disco_info) do - if tag.name == "identity" then - table.insert(identities, (tag.attr.category or "").."\0"..(tag.attr.type or "").."\0"..(tag.attr["xml:lang"] or "").."\0"..(tag.attr.name or "")); - elseif tag.name == "feature" then - table.insert(features, tag.attr.var or ""); - elseif tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then - local form = {}; - local FORM_TYPE; - for _, field in pairs(tag.tags) do - if field.name == "field" and field.attr.var then - local values = {}; - for _, val in pairs(field.tags) do - val = #val.tags == 0 and table.concat(val); -- FIXME use get_text? - if val then table.insert(values, val); end - end - table.sort(values); - if field.attr.var == "FORM_TYPE" then - FORM_TYPE = values[1]; - elseif #values > 0 then - table.insert(form, field.attr.var.."\0"..table.concat(values, "<")); - else - table.insert(form, field.attr.var); - end - end - end - table.sort(form); - form = table.concat(form, "<"); - if FORM_TYPE then form = FORM_TYPE.."\0"..form; end - table.insert(extensions, form); - end - end - table.sort(identities); - table.sort(features); - table.sort(extensions); - if #identities > 0 then identities = table.concat(identities, "<"):gsub("%z", "/").."<"; else identities = ""; end - if #features > 0 then features = table.concat(features, "<").."<"; else features = ""; end - if #extensions > 0 then extensions = table.concat(extensions, "<"):gsub("%z", "<").."<"; else extensions = ""; end - local S = identities..features..extensions; - local ver = base64(sha1(S)); - return ver, S; -end - module:hook("iq/bare/disco", function(event) local session, stanza = event.origin, event.stanza; if stanza.attr.type == "result" then @@ -285,8 +244,8 @@ module:hook("account-disco-info", function(event) end); module:hook("account-disco-items", function(event) - local session, stanza = event.session, event.stanza; - local bare = session.username..'@'..session.host; + local stanza = event.stanza; + local bare = stanza.attr.to; local user_data = data[bare]; if user_data then diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua index c38f7eba..77b2f2a4 100644 --- a/plugins/mod_posix.lua +++ b/plugins/mod_posix.lua @@ -54,16 +54,16 @@ module:add_event_hook("server-started", function () end); -- Don't even think about it! -module:add_event_hook("server-starting", function () - local suid = module:get_option("setuid"); - if not suid or suid == 0 or suid == "root" then - if pposix.getuid() == 0 and not module:get_option("run_as_root") then - module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!"); - module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root"); - prosody.shutdown("Refusing to run as root"); - end +if not prosody.start_time then -- server-starting + local suid = module:get_option("setuid"); + if not suid or suid == 0 or suid == "root" then + if pposix.getuid() == 0 and not module:get_option("run_as_root") then + module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!"); + module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root"); + prosody.shutdown("Refusing to run as root"); end - end); + end +end local pidfile; local pidfile_handle; @@ -95,8 +95,17 @@ local function write_pidfile() pidfile_handle = nil; prosody.shutdown("Prosody already running"); else - pidfile_handle:write(tostring(pposix.getpid())); - pidfile_handle:flush(); + pidfile_handle:close(); + pidfile_handle, err = io.open(pidfile, "w+"); + if not pidfile_handle then + module:log("error", "Couldn't write pidfile at %s; %s", pidfile, err); + prosody.shutdown("Couldn't write pidfile"); + else + if lfs.lock(pidfile_handle, "w") then + pidfile_handle:write(tostring(pposix.getpid())); + pidfile_handle:flush(); + end + end end end end @@ -141,7 +150,9 @@ if daemonize then write_pidfile(); end end - module:add_event_hook("server-starting", daemonize_server); + if not prosody.start_time then -- server-starting + daemonize_server(); + end else -- Not going to daemonize, so write the pid of this process write_pidfile(); diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua index 4fb8c3e4..8e6ef85a 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -24,20 +24,6 @@ local rostermanager = require "core.rostermanager"; local sessionmanager = require "core.sessionmanager"; local offlinemanager = require "core.offlinemanager"; -local _core_route_stanza = core_route_stanza; -local core_route_stanza; -function core_route_stanza(origin, stanza) - if stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" and stanza.attr.type ~= "error" then - local node, host = jid_split(stanza.attr.to); - host = hosts[host]; - if node and host and host.type == "local" then - handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); - return; - end - end - _core_route_stanza(origin, stanza); -end - local function select_top_resources(user) local priority = 0; local recipients = {}; @@ -64,7 +50,7 @@ end local ignore_presence_priority = module:get_option("ignore_presence_priority"); -function handle_normal_presence(origin, stanza, core_route_stanza) +function handle_normal_presence(origin, stanza) if ignore_presence_priority then local priority = stanza:child_with_name("priority"); if priority and priority[1] ~= "0" then @@ -82,13 +68,13 @@ function handle_normal_presence(origin, stanza, core_route_stanza) for _, res in pairs(user and user.sessions or NULL) do -- broadcast to all resources if res ~= origin and res.presence then -- to resource stanza.attr.to = res.full_jid; - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza, true); end end for jid, item in pairs(roster) do -- broadcast to all interested contacts if item.subscription == "both" or item.subscription == "from" then stanza.attr.to = jid; - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza, true); end end if stanza.attr.type == nil and not origin.presence then -- initial presence @@ -97,13 +83,13 @@ function handle_normal_presence(origin, stanza, core_route_stanza) for jid, item in pairs(roster) do -- probe all contacts we are subscribed to if item.subscription == "both" or item.subscription == "to" then probe.attr.to = jid; - core_route_stanza(origin, probe); + core_post_stanza(origin, probe, true); end end for _, res in pairs(user and user.sessions or NULL) do -- broadcast from all available resources if res ~= origin and res.presence then res.presence.attr.to = origin.full_jid; - core_route_stanza(res, res.presence); + core_post_stanza(res, res.presence, true); res.presence.attr.to = nil; end end @@ -116,7 +102,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza) for jid, item in pairs(roster) do -- resend outgoing subscription requests if item.ask then request.attr.to = jid; - core_route_stanza(origin, request); + core_post_stanza(origin, request, true); end end local offline = offlinemanager.load(node, host); @@ -136,7 +122,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza) if origin.directed then for jid in pairs(origin.directed) do stanza.attr.to = jid; - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza, true); end origin.directed = nil; end @@ -159,7 +145,7 @@ function handle_normal_presence(origin, stanza, core_route_stanza) stanza.attr.to = nil; -- reset it end -function send_presence_of_available_resources(user, host, jid, recipient_session, core_route_stanza, stanza) +function send_presence_of_available_resources(user, host, jid, recipient_session, stanza) local h = hosts[host]; local count = 0; if h and h.type == "local" then @@ -170,7 +156,7 @@ function send_presence_of_available_resources(user, host, jid, recipient_session if pres then if stanza then pres = stanza; pres.attr.from = session.full_jid; end pres.attr.to = jid; - core_route_stanza(session, pres); + core_post_stanza(session, pres, true); pres.attr.to = nil; count = count + 1; end @@ -181,26 +167,29 @@ function send_presence_of_available_resources(user, host, jid, recipient_session return count; end -function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza) +function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare) local node, host = jid_split(from_bare); - if to_bare == origin.username.."@"..origin.host then return; end -- No self contacts + if to_bare == from_bare then return; end -- No self contacts local st_from, st_to = stanza.attr.from, stanza.attr.to; stanza.attr.from, stanza.attr.to = from_bare, to_bare; log("debug", "outbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare); - if stanza.attr.type == "subscribe" then + if stanza.attr.type == "probe" then + stanza.attr.from, stanza.attr.to = st_from, st_to; + return; + elseif stanza.attr.type == "subscribe" then -- 1. route stanza -- 2. roster push (subscription = none, ask = subscribe) if rostermanager.set_contact_pending_out(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); end -- else file error - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza); elseif stanza.attr.type == "unsubscribe" then -- 1. route stanza -- 2. roster push (subscription = none or from) if rostermanager.unsubscribe(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed? end -- else file error - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza); elseif stanza.attr.type == "subscribed" then -- 1. route stanza -- 2. roster_push () @@ -208,20 +197,21 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_ if rostermanager.subscribed(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); end - core_route_stanza(origin, stanza); - send_presence_of_available_resources(node, host, to_bare, origin, core_route_stanza); + core_post_stanza(origin, stanza); + send_presence_of_available_resources(node, host, to_bare, origin); elseif stanza.attr.type == "unsubscribed" then -- 1. route stanza -- 2. roster push (subscription = none or to) if rostermanager.unsubscribed(node, host, to_bare) then rostermanager.roster_push(node, host, to_bare); end - core_route_stanza(origin, stanza); + core_post_stanza(origin, stanza); end stanza.attr.from, stanza.attr.to = st_from, st_to; + return true; end -function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare, core_route_stanza) +function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare) local node, host = jid_split(to_bare); local st_from, st_to = stanza.attr.from, stanza.attr.to; stanza.attr.from, stanza.attr.to = from_bare, to_bare; @@ -230,21 +220,21 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b if stanza.attr.type == "probe" then local result, err = rostermanager.is_contact_subscribed(node, host, from_bare); if result then - if 0 == send_presence_of_available_resources(node, host, st_from, origin, core_route_stanza) then - core_route_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"})); -- TODO send last activity + if 0 == send_presence_of_available_resources(node, host, st_from, origin) then + core_post_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"}), true); -- TODO send last activity end elseif not err then - core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"})); + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}), true); end elseif stanza.attr.type == "subscribe" then if rostermanager.is_contact_subscribed(node, host, from_bare) then - core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"})); -- already subscribed + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"}), true); -- already subscribed -- Sending presence is not clearly stated in the RFC, but it seems appropriate - if 0 == send_presence_of_available_resources(node, host, from_bare, origin, core_route_stanza) then - core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- TODO send last activity + if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- TODO send last activity end else - core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- acknowledging receipt + core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt if not rostermanager.is_contact_pending_in(node, host, from_bare) then if rostermanager.set_contact_pending_in(node, host, from_bare) then sessionmanager.send_to_available_resources(node, host, stanza); @@ -268,6 +258,7 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b end end -- discard any other type stanza.attr.from, stanza.attr.to = st_from, st_to; + return true; end local outbound_presence_handler = function(data) @@ -278,12 +269,12 @@ local outbound_presence_handler = function(data) if to then local t = stanza.attr.type; if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes - handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); - return true; + return handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); end local to_bare = jid_bare(to); - if not(origin.roster[to_bare] and (origin.roster[to_bare].subscription == "both" or origin.roster[to_bare].subscription == "from")) then -- directed presence + local roster = origin.roster; + if roster and not(roster[to_bare] and (roster[to_bare].subscription == "both" or roster[to_bare].subscription == "from")) then -- directed presence origin.directed = origin.directed or {}; if t then -- removing from directed presence list on sending an error or unavailable origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to? @@ -306,8 +297,7 @@ module:hook("presence/bare", function(data) local t = stanza.attr.type; if to then if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID - handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); - return true; + return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); end local user = bare_sessions[to]; @@ -319,7 +309,7 @@ module:hook("presence/bare", function(data) end end -- no resources not online, discard elseif not t or t == "unavailable" then - handle_normal_presence(origin, stanza, core_route_stanza); + handle_normal_presence(origin, stanza); end return true; end); @@ -329,8 +319,7 @@ module:hook("presence/full", function(data) local t = stanza.attr.type; if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID - handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to), core_route_stanza); - return true; + return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); end local session = full_sessions[stanza.attr.to]; @@ -347,10 +336,10 @@ module:hook("presence/host", function(data) local from_bare = jid_bare(stanza.attr.from); local t = stanza.attr.type; if t == "probe" then - core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); + core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); elseif t == "subscribe" then - core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" })); - core_route_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); + core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" })); + core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id })); end return true; end); @@ -369,7 +358,7 @@ module:hook("resource-unbind", function(event) pres:tag("status"):text("Disconnected: "..err):up(); for jid in pairs(session.directed) do pres.attr.to = jid; - core_route_stanza(session, pres); + core_post_stanza(session, pres, true); end session.directed = nil; end diff --git a/plugins/mod_privacy.lua b/plugins/mod_privacy.lua index aa953310..5355e527 100644 --- a/plugins/mod_privacy.lua +++ b/plugins/mod_privacy.lua @@ -442,7 +442,9 @@ function preCheckOutgoing(e) e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource; end end - return checkIfNeedToBeBlocked(e, session); + if session.username then -- FIXME do properly + return checkIfNeedToBeBlocked(e, session); + end end module:hook("pre-message/full", preCheckOutgoing, 500); diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua index 190d30be..61359444 100644 --- a/plugins/mod_proxy65.lua +++ b/plugins/mod_proxy65.lua @@ -14,7 +14,7 @@ if module:get_host_type() ~= "component" then error("proxy65 should be loaded as a component, please see http://prosody.im/doc/components", 0); end -local jid_split, jid_join = require "util.jid".split, require "util.jid".join; +local jid_split, jid_join, jid_compare = require "util.jid".split, require "util.jid".join, require "util.jid".compare; local st = require "util.stanza"; local componentmanager = require "core.componentmanager"; local config_get = require "core.configmanager".get; @@ -151,24 +151,11 @@ local function get_stream_host(origin, stanza) local err_reply = replies_cache.stream_host_err; local sid = stanza.tags[1].attr.sid; local allow = false; - local jid_node, jid_host, jid_resource = jid_split(stanza.attr.from); - - if stanza.attr.from == nil then - jid_node = origin.username; - jid_host = origin.host; - jid_resource = origin.resource; - end + local jid = stanza.attr.from; if proxy_acl and #proxy_acl > 0 then - if host ~= nil then -- at least a domain is needed. - for _, acl in ipairs(proxy_acl) do - local acl_node, acl_host, acl_resource = jid_split(acl); - if ((acl_node ~= nil and acl_node == jid_node) or acl_node == nil) and - ((acl_host ~= nil and acl_host == jid_host) or acl_host == nil) and - ((acl_resource ~= nil and acl_resource == jid_resource) or acl_resource == nil) then - allow = true; - end - end + for _, acl in ipairs(proxy_acl) do + if jid_compare(jid, acl) then allow = true; end end else allow = true; @@ -181,7 +168,7 @@ local function get_stream_host(origin, stanza) replies_cache.stream_host = reply; end else - module:log("warn", "Denying use of proxy for %s", tostring(jid_join(jid_node, jid_host, jid_resource))); + module:log("warn", "Denying use of proxy for %s", tostring(jid)); if err_reply == nil then err_reply = st.iq({type="error", from=host}) :query("http://jabber.org/protocol/bytestreams") diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua index 2818e336..25f47a9e 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -13,7 +13,6 @@ local datamanager = require "util.datamanager"; local usermanager_user_exists = require "core.usermanager".user_exists; local usermanager_create_user = require "core.usermanager".create_user; local usermanager_set_password = require "core.usermanager".set_password; -local datamanager_store = require "util.datamanager".store; local os_time = os.time; local nodeprep = require "util.encodings".stringprep.nodeprep; @@ -35,7 +34,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza) local username, host = session.username, session.host; --session.send(st.error_reply(stanza, "cancel", "not-allowed")); --return; - --usermanager_set_password(username, host, nil); -- Disable account + usermanager_set_password(username, nil, host); -- Disable account -- FIXME the disabling currently allows a different user to recreate the account -- we should add an in-memory account block mode when we have threading session.send(st.reply(stanza)); @@ -71,7 +70,7 @@ module:add_iq_handler("c2s", "jabber:iq:register", function (session, stanza) username = nodeprep(table.concat(username)); password = table.concat(password); if username == session.username then - if usermanager_set_password(username, session.host, password) then + if usermanager_set_password(username, password, session.host) then session.send(st.reply(stanza)); else -- TODO unable to write file, file may be locked, etc, what's the correct error? diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua index ddf02f2f..af92e24e 100644 --- a/plugins/mod_roster.lua +++ b/plugins/mod_roster.lua @@ -42,15 +42,15 @@ module:add_iq_handler("c2s", "jabber:iq:roster", if not (client_ver and server_ver) or client_ver ~= server_ver then roster:query("jabber:iq:roster"); -- Client does not support versioning, or has stale roster - for jid in pairs(session.roster) do + for jid, item in pairs(session.roster) do if jid ~= "pending" and jid then roster:tag("item", { jid = jid, - subscription = session.roster[jid].subscription, - ask = session.roster[jid].ask, - name = session.roster[jid].name, + subscription = item.subscription, + ask = item.ask, + name = item.name, }); - for group in pairs(session.roster[jid].groups) do + for group in pairs(item.groups) do roster:tag("group"):text(group):up(); end roster:up(); -- move out from item diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index d407e5da..a02c1ec4 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -14,19 +14,14 @@ local sm_make_authenticated = require "core.sessionmanager".make_authenticated; local base64 = require "util.encodings".base64; local nodeprep = require "util.encodings".stringprep.nodeprep; -local datamanager_load = require "util.datamanager".load; -local usermanager_validate_credentials = require "core.usermanager".validate_credentials; -local usermanager_get_supported_methods = require "core.usermanager".get_supported_methods; +local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; local usermanager_user_exists = require "core.usermanager".user_exists; -local usermanager_get_password = require "core.usermanager".get_password; local t_concat, t_insert = table.concat, table.insert; local tostring = tostring; -local jid_split = require "util.jid".split; -local md5 = require "util.hashes".md5; -local config = require "core.configmanager"; local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption"); local sasl_backend = module:get_option("sasl_backend") or "builtin"; +local anonymous_login = module:get_option("anonymous_login"); -- Cyrus config options local require_provisioning = module:get_option("cyrus_require_provisioning") or false; @@ -66,21 +61,6 @@ else error("Unknown SASL backend"); end -local default_authentication_profile = { - plain = function(username, realm) - local prepped_username = nodeprep(username); - if not prepped_username then - log("debug", "NODEprep failed on username: %s", username); - return "", nil; - end - local password = usermanager_get_password(prepped_username, realm); - if not password then - return "", nil; - end - return password, true; - end -}; - local anonymous_authentication_profile = { anonymous = function(username, realm) return true; -- for normal usage you should always return true here @@ -111,8 +91,8 @@ local function handle_status(session, status, ret, err_msg) local username = nodeprep(session.sasl_handler.username); if not(require_provisioning) or usermanager_user_exists(username, session.host) then - local aret, err = sm_make_authenticated(session, session.sasl_handler.username); - if aret then + local ok, err = sm_make_authenticated(session, session.sasl_handler.username); + if ok then session.sasl_handler = nil; session:reset_stream(); else @@ -132,7 +112,7 @@ end local function sasl_handler(session, stanza) if stanza.name == "auth" then -- FIXME ignoring duplicates because ejabberd does - if config.get(session.host or "*", "core", "anonymous_login") then + if anonymous_login then if stanza.attr.mechanism ~= "ANONYMOUS" then return session.send(build_reply("failure", "invalid-mechanism")); end @@ -179,18 +159,17 @@ module:hook("stream-features", function(event) if secure_auth_only and not origin.secure then return; end - local realm = module:get_option("sasl_realm") or origin.host; - if module:get_option("anonymous_login") then - origin.sasl_handler = new_sasl(realm, anonymous_authentication_profile); + if anonymous_login then + origin.sasl_handler = new_sasl(module.host, anonymous_authentication_profile); else - origin.sasl_handler = new_sasl(realm, default_authentication_profile); + origin.sasl_handler = usermanager_get_sasl_handler(module.host); if not (module:get_option("allow_unencrypted_plain_auth")) and not origin.secure then origin.sasl_handler:forbidden({"PLAIN"}); end end features:tag("mechanisms", mechanisms_attr); - for k, v in pairs(origin.sasl_handler:mechanisms()) do - features:tag("mechanism"):text(v):up(); + for k in pairs(origin.sasl_handler:mechanisms()) do + features:tag("mechanism"):text(k):up(); end features:up(); else diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index 8b96aa15..a2667ff6 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -83,7 +83,7 @@ module:hook_stanza(xmlns_starttls, "proceed", function (session, stanza) module:log("debug", "Proceeding with TLS on s2sout..."); session:reset_stream(); local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx; - session.conn:starttls(ssl_ctx, true); + session.conn:starttls(ssl_ctx); session.secure = false; return true; end); diff --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua index 24d10180..c3860af6 100644 --- a/plugins/mod_uptime.lua +++ b/plugins/mod_uptime.lua @@ -11,6 +11,7 @@ local st = require "util.stanza"; local start_time = prosody.start_time; prosody.events.add_handler("server-started", function() start_time = prosody.start_time end); +-- XEP-0012: Last activity module:add_feature("jabber:iq:last"); module:hook("iq/host/jabber:iq:last:query", function(event) @@ -20,3 +21,28 @@ module:hook("iq/host/jabber:iq:last:query", function(event) return true; end end); + +-- Ad-hoc command +local adhoc_new = module:require "adhoc".new; + +function uptime_text() + 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_text(), status = "completed" }; +end + +local descriptor = adhoc_new("Get uptime", "uptime", uptime_command_handler); + +module:add_item ("adhoc", descriptor); diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index de23aebb..a9d9336c 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -31,8 +31,11 @@ local rooms = {}; local persistent_rooms = datamanager.load(nil, muc_host, "persistent") or {}; local component; +-- Configurable options +local max_history_messages = module:get_option_number("max_history_messages"); + local function is_admin(jid) - return um_is_admin(jid) or um_is_admin(jid, module.host); + return um_is_admin(jid, module.host); end local function room_route_stanza(room, stanza) core_post_stanza(component, stanza); end @@ -58,15 +61,20 @@ end for jid in pairs(persistent_rooms) do local node = jid_split(jid); local data = datamanager.load(node, muc_host, "config") or {}; - local room = muc_new_room(jid); + local room = muc_new_room(jid, { + history_length = max_history_messages; + }); room._data = data._data; + room._data.history_length = max_history_messages; --TODO: Need to allow per-room with a global limit room._affiliations = data._affiliations; room.route_stanza = room_route_stanza; room.save = room_save; rooms[jid] = room; end -local host_room = muc_new_room(muc_host); +local host_room = muc_new_room(muc_host, { + history_length = max_history_messages; +}); host_room.route_stanza = room_route_stanza; host_room.save = room_save; @@ -78,7 +86,7 @@ end local function get_disco_items(stanza) local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items"); for jid, room in pairs(rooms) do - if not room._data.hidden then + if not room:is_hidden() then reply:tag("item", {jid=jid, name=jid}):up(); end end @@ -113,7 +121,9 @@ component = register_component(muc_host, function(origin, stanza) local room = rooms[bare]; if not room then if not(restrict_room_creation) or is_admin(stanza.attr.from) then - room = muc_new_room(bare); + room = muc_new_room(bare, { + history_length = max_history_messages; + }); room.route_stanza = room_route_stanza; room.save = room_save; rooms[bare] = room; diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index 18c80325..9cdf4d18 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -6,6 +6,9 @@ -- COPYING file in the source package for more information. -- +local select = select; +local pairs, ipairs = pairs, ipairs; + local datamanager = require "util.datamanager"; local datetime = require "util.datetime"; @@ -21,7 +24,7 @@ local base64 = require "util.encodings".base64; local md5 = require "util.hashes".md5; local muc_domain = nil; --module:get_host(); -local history_length = 20; +local default_history_length = 20; ------------ local function filter_xmlns_from_array(array, filters) @@ -88,8 +91,12 @@ room_mt.__index = room_mt; function room_mt:get_default_role(affiliation) if affiliation == "owner" or affiliation == "admin" then return "moderator"; - elseif affiliation == "member" or not affiliation then + elseif affiliation == "member" then return "participant"; + elseif not affiliation then + if not self:is_members_only() then + return self:is_moderated() and "visitor" or "participant"; + end end end @@ -122,10 +129,14 @@ function room_mt:broadcast_message(stanza, historic) local history = self._data['history']; if not history then history = {}; self._data['history'] = history; end stanza = st.clone(stanza); - stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203 + stanza.attr.to = ""; + local stamp = datetime.datetime(); + local chars = #tostring(stanza); + stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203 stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) - t_insert(history, st.preserialize(stanza)); - while #history > history_length do t_remove(history, 1) end + local entry = { stanza = stanza, stamp = stamp }; + t_insert(history, entry); + while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end end end function room_mt:broadcast_except_nick(stanza, nick) @@ -151,24 +162,65 @@ function room_mt:send_occupant_list(to) end end end -function room_mt:send_history(to) +function room_mt:send_history(to, stanza) local history = self._data['history']; -- send discussion history if history then - for _, msg in ipairs(history) do - msg = st.deserialize(msg); - msg.attr.to=to; + local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc"); + local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc"); + + local maxchars = history_tag and tonumber(history_tag.attr.maxchars); + if maxchars then maxchars = math.floor(maxchars); end + + local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history); + if not history_tag then maxstanzas = 20; end + + local seconds = history_tag and tonumber(history_tag.attr.seconds); + if seconds then seconds = datetime.datetime(os.time() - math.floor(seconds)); end + + local since = history_tag and history_tag.attr.since; + if since and not since:match("^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%dZ$") then since = nil; end -- FIXME timezone support + if seconds and (not since or since < seconds) then since = seconds; end + + local n = 0; + local charcount = 0; + local stanzacount = 0; + + for i=#history,1,-1 do + local entry = history[i]; + if maxchars then + if not entry.chars then + entry.stanza.attr.to = ""; + entry.chars = #tostring(entry.stanza); + end + charcount = charcount + entry.chars + #to; + if charcount > maxchars then break; end + end + if since and since > entry.stamp then break; end + if n + 1 > maxstanzas then break; end + n = n + 1; + end + for i=#history-n+1,#history do + local msg = history[i].stanza; + msg.attr.to = to; self:_route_stanza(msg); end end if self._data['subject'] then - self:_route_stanza(st.message({type='groupchat', from=self.jid, to=to}):tag("subject"):text(self._data['subject'])); + self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject'])); end end function room_mt:get_disco_info(stanza) return st.reply(stanza):query("http://jabber.org/protocol/disco#info") :tag("identity", {category="conference", type="text"}):up() - :tag("feature", {var="http://jabber.org/protocol/muc"}); + :tag("feature", {var="http://jabber.org/protocol/muc"}):up() + :tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up() + :tag("feature", {var=self:is_moderated() and "muc_moderated" or "muc_unmoderated"}):up() + :tag("feature", {var=self:is_members_only() and "muc_membersonly" or "muc_open"}):up() + :tag("feature", {var=self:is_persistent() and "muc_persistent" or "muc_temporary"}):up() + :tag("feature", {var=self:is_hidden() and "muc_hidden" or "muc_public"}):up() + :tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up() + ; end function room_mt:get_disco_items(stanza) local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); @@ -181,6 +233,7 @@ function room_mt:set_subject(current_nick, subject) -- TODO check nick's authority if subject == "" then subject = nil; end self._data['subject'] = subject; + self._data['subject_from'] = current_nick; if self.save then self:save(); end local msg = st.message({type='groupchat', from=current_nick}) :tag('subject'):text(subject):up(); @@ -198,6 +251,57 @@ local function build_unavailable_presence_from_error(stanza) :tag('status'):text(error_message); end +function room_mt:set_password(password) + if password == "" or type(password) ~= "string" then password = nil; end + if self._data.password ~= password then + self._data.password = password; + if self.save then self:save(true); end + end +end +function room_mt:get_password() + return self._data.password; +end +function room_mt:set_moderated(moderated) + moderated = moderated and true or nil; + if self._data.moderated ~= moderated then + self._data.moderated = moderated; + if self.save then self:save(true); end + end +end +function room_mt:is_moderated() + return self._data.moderated; +end +function room_mt:set_members_only(members_only) + members_only = members_only and true or nil; + if self._data.members_only ~= members_only then + self._data.members_only = members_only; + if self.save then self:save(true); end + end +end +function room_mt:is_members_only() + return self._data.members_only; +end +function room_mt:set_persistent(persistent) + persistent = persistent and true or nil; + if self._data.persistent ~= persistent then + self._data.persistent = persistent; + if self.save then self:save(true); end + end +end +function room_mt:is_persistent() + return self._data.persistent; +end +function room_mt:set_hidden(hidden) + hidden = hidden and true or nil; + if self._data.hidden ~= hidden then + self._data.hidden = hidden; + if self.save then self:save(true); end + end +end +function room_mt:is_hidden() + return self._data.hidden; +end + function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc local from, to = stanza.attr.from, stanza.attr.to; local room = jid_bare(to); @@ -290,7 +394,15 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc end is_merge = true; end - if not new_nick then + local password = stanza:get_child("x", "http://jabber.org/protocol/muc"); + password = password and password:get_child("password", "http://jabber.org/protocol/muc"); + password = password and password[1] ~= "" and password[1]; + if self:get_password() and self:get_password() ~= password then + log("debug", "%s couldn't join due to invalid password: %s", from, to); + local reply = st.error_reply(stanza, "auth", "not-authorized"):up(); + reply.tags[1].attr.code = "401"; + origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"})); + elseif not new_nick then log("debug", "%s couldn't join due to nick conflict: %s", from, to); local reply = st.error_reply(stanza, "cancel", "conflict"):up(); reply.tags[1].attr.code = "409"; @@ -319,12 +431,7 @@ function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up() :tag("status", {code='110'})); end - if self._data.whois == 'anyone' then -- non-anonymous? - self:_route_stanza(st.stanza("message", {from=to, to=from, type='groupchat'}) - :tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) - :tag("status", {code='100'})); - end - self:send_history(from); + self:send_history(from, stanza); else -- banned local reply = st.error_reply(stanza, "auth", "forbidden"):up(); reply.tags[1].attr.code = "403"; @@ -392,10 +499,10 @@ function room_mt:send_form(origin, stanza) :tag("instructions"):text(title):up() :tag("field", {type='hidden', var='FORM_TYPE'}):tag("value"):text("http://jabber.org/protocol/muc#roomconfig"):up():up() :tag("field", {type='boolean', label='Make Room Persistent?', var='muc#roomconfig_persistentroom'}) - :tag("value"):text(self._data.persistent and "1" or "0"):up() + :tag("value"):text(self:is_persistent() and "1" or "0"):up() :up() :tag("field", {type='boolean', label='Make Room Publicly Searchable?', var='muc#roomconfig_publicroom'}) - :tag("value"):text(self._data.hidden and "0" or "1"):up() + :tag("value"):text(self:is_hidden() and "0" or "1"):up() :up() :tag("field", {type='list-single', label='Who May Discover Real JIDs?', var='muc#roomconfig_whois'}) :tag("value"):text(self._data.whois or 'moderators'):up() @@ -406,6 +513,15 @@ function room_mt:send_form(origin, stanza) :tag("value"):text('anyone'):up() :up() :up() + :tag("field", {type='text-private', label='Password', var='muc#roomconfig_roomsecret'}) + :tag("value"):text(self:get_password() or ""):up() + :up() + :tag("field", {type='boolean', label='Make Room Moderated?', var='muc#roomconfig_moderatedroom'}) + :tag("value"):text(self:is_moderated() and "1" or "0"):up() + :up() + :tag("field", {type='boolean', label='Make Room Members-Only?', var='muc#roomconfig_membersonly'}) + :tag("value"):text(self:is_members_only() and "1" or "0"):up() + :up() ); end @@ -434,15 +550,25 @@ function room_mt:process_form(origin, stanza) local persistent = fields['muc#roomconfig_persistentroom']; if persistent == "0" or persistent == "false" then persistent = nil; elseif persistent == "1" or persistent == "true" then persistent = true; else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end - dirty = dirty or (self._data.persistent ~= persistent) - self._data.persistent = persistent; + dirty = dirty or (self:is_persistent() ~= persistent) module:log("debug", "persistent=%s", tostring(persistent)); + local moderated = fields['muc#roomconfig_moderatedroom']; + if moderated == "0" or moderated == "false" then moderated = nil; elseif moderated == "1" or moderated == "true" then moderated = true; + else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + dirty = dirty or (self:is_moderated() ~= moderated) + module:log("debug", "moderated=%s", tostring(moderated)); + + local membersonly = fields['muc#roomconfig_membersonly']; + if membersonly == "0" or membersonly == "false" then membersonly = nil; elseif membersonly == "1" or membersonly == "true" then membersonly = true; + else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end + dirty = dirty or (self:is_members_only() ~= membersonly) + module:log("debug", "membersonly=%s", tostring(membersonly)); + local public = fields['muc#roomconfig_publicroom']; if public == "0" or public == "false" then public = nil; elseif public == "1" or public == "true" then public = true; else origin.send(st.error_reply(stanza, "cancel", "bad-request")); return; end - dirty = dirty or (self._data.hidden ~= (not public and true or nil)) - self._data.hidden = not public and true or nil; + dirty = dirty or (self:is_hidden() ~= (not public and true or nil)) local whois = fields['muc#roomconfig_whois']; if not valid_whois[whois] then @@ -451,7 +577,16 @@ function room_mt:process_form(origin, stanza) end local whois_changed = self._data.whois ~= whois self._data.whois = whois - module:log('debug', 'whois=%s', tostring(whois)) + module:log('debug', 'whois=%s', whois) + + local password = fields['muc#roomconfig_roomsecret']; + if password then + self:set_password(password); + end + self:set_moderated(moderated); + self:set_members_only(membersonly); + self:set_persistent(persistent); + self:set_hidden(not public); if self.save then self:save(true); end origin.send(st.reply(stanza)); @@ -488,8 +623,7 @@ function room_mt:destroy(newjid, reason, password) end self._occupants[nick] = nil; end - self._data.persistent = nil; - if self.save then self:save(true); end + self:set_persistent(false); end function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc @@ -654,8 +788,11 @@ function room_mt:handle_to_room(origin, stanza) -- presence changes and groupcha :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}) :tag('invite', {from=_from}) :tag('reason'):text(_reason or ""):up() - :up() - :up() + :up(); + if self:get_password() then + invite:tag("password"):text(self:get_password()):up(); + end + invite:up() :tag('x', {xmlns="jabber:x:conference", jid=_to}) -- COMPAT: Some older clients expect this :text(_reason or "") :up() @@ -748,13 +885,29 @@ function room_mt:get_role(nick) local session = self._occupants[nick]; return session and session.role or nil; end +function room_mt:can_set_role(actor_jid, occupant_jid, role) + local actor = self._occupants[self._jid_nick[actor_jid]]; + local occupant = self._occupants[occupant_jid]; + + if not occupant or not actor then return nil, "modify", "not-acceptable"; end + + if actor.role == "moderator" then + if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then + if actor.affiliation == "owner" or actor.affiliation == "admin" then + return true; + elseif occupant.role ~= "moderator" and role ~= "moderator" then + return true; + end + end + end + return nil, "cancel", "not-allowed"; +end function room_mt:set_role(actor, occupant_jid, role, callback, reason) if role == "none" then role = nil; end if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end - if self:get_role(self._jid_nick[actor]) ~= "moderator" then return nil, "cancel", "not-allowed"; end + local allowed, err_type, err_condition = self:can_set_role(actor, occupant_jid, role); + if not allowed then return allowed, err_type, err_condition; end local occupant = self._occupants[occupant_jid]; - if not occupant then return nil, "modify", "not-acceptable"; end - if occupant.affiliation == "owner" or occupant.affiliation == "admin" then return nil, "cancel", "not-allowed"; end local p = st.presence({from = occupant_jid}) :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) :tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"}) @@ -804,6 +957,9 @@ function room_mt:_route_stanza(stanza) end end end + if self._data.whois == 'anyone' then + muc_child:tag('status', { code = '100' }); + end end self:route_stanza(stanza); if muc_child then @@ -817,13 +973,14 @@ end local _M = {}; -- module "muc" -function _M.new_room(jid) +function _M.new_room(jid, config) return setmetatable({ jid = jid; _jid_nick = {}; _occupants = {}; _data = { - whois = 'moderators', + whois = 'moderators'; + history_length = (config and config.history_length); }; _affiliations = {}; }, room_mt); diff --git a/plugins/storage/ejabberdstore.lib.lua b/plugins/storage/ejabberdstore.lib.lua new file mode 100644 index 00000000..7e8592a8 --- /dev/null +++ b/plugins/storage/ejabberdstore.lib.lua @@ -0,0 +1,190 @@ +
+local handlers = {};
+
+handlers.accounts = {
+ get = function(self, user)
+ local select = self:query("select password from users where username=?", user);
+ local row = select and select:fetch();
+ if row then return { password = row[1] }; end
+ end;
+ set = function(self, user, data)
+ if data and data.password then
+ return self:modify("update users set password=? where username=?", data.password, user)
+ or self:modify("insert into users (username, password) values (?, ?)", user, data.password);
+ else
+ return self:modify("delete from users where username=?", user);
+ end
+ end;
+};
+handlers.vcard = {
+ get = function(self, user)
+ local select = self:query("select vcard from vcard where username=?", user);
+ local row = select and select:fetch();
+ if row then return parse_xml(row[1]); end
+ end;
+ set = function(self, user, data)
+ if data then
+ data = unparse_xml(data);
+ return self:modify("update vcard set vcard=? where username=?", data, user)
+ or self:modify("insert into vcard (username, vcard) values (?, ?)", user, data);
+ else
+ return self:modify("delete from vcard where username=?", user);
+ end
+ end;
+};
+handlers.private = {
+ get = function(self, user)
+ local select = self:query("select namespace,data from private_storage where username=?", user);
+ if select then
+ local data = {};
+ for row in select:rows() do
+ data[row[1]] = parse_xml(row[2]);
+ end
+ return data;
+ end
+ end;
+ set = function(self, user, data)
+ if data then
+ self:modify("delete from private_storage where username=?", user);
+ for namespace,text in pairs(data) do
+ self:modify("insert into private_storage (username, namespace, data) values (?, ?, ?)", user, namespace, unparse_xml(text));
+ end
+ return true;
+ else
+ return self:modify("delete from private_storage where username=?", user);
+ end
+ end;
+ -- TODO map_set, map_get
+};
+local subscription_map = { N = "none", B = "both", F = "from", T = "to" };
+local subscription_map_reverse = { none = "N", both = "B", from = "F", to = "T" };
+handlers.roster = {
+ get = function(self, user)
+ local select = self:query("select jid,nick,subscription,ask,server,subscribe,type from rosterusers where username=?", user);
+ if select then
+ local roster = { pending = {} };
+ for row in select:rows() do
+ local jid,nick,subscription,ask,server,subscribe,typ = unpack(row);
+ local item = { groups = {} };
+ if nick == "" then nick = nil; end
+ item.nick = nick;
+ item.subscription = subscription_map[subscription];
+ if ask == "N" then ask = nil;
+ elseif ask == "O" then ask = "subscribe"
+ elseif ask == "I" then roster.pending[jid] = true; ask = nil;
+ elseif ask == "B" then roster.pending[jid] = true; ask = "subscribe";
+ else module:log("debug", "bad roster_item.ask: %s", ask); ask = nil; end
+ item.ask = ask;
+ roster[jid] = item;
+ end
+
+ select = self:query("select jid,grp from rostergroups where username=?", user);
+ if select then
+ for row in select:rows() do
+ local jid,grp = unpack(rows);
+ if roster[jid] then roster[jid].groups[grp] = true; end
+ end
+ end
+ select = self:query("select version from roster_version where username=?", user);
+ local row = select and select:fetch();
+ if row then
+ roster[false] = { version = row[1]; };
+ end
+ return roster;
+ end
+ end;
+ set = function(self, user, data)
+ if data and next(data) ~= nil then
+ self:modify("delete from rosterusers where username=?", user);
+ self:modify("delete from rostergroups where username=?", user);
+ self:modify("delete from roster_version where username=?", user);
+ local done = {};
+ local pending = data.pending or {};
+ for jid,item in pairs(data) do
+ if jid and jid ~= "pending" then
+ local subscription = subscription_map_reverse[item.subscription];
+ local ask;
+ if pending[jid] then
+ if item.ask then ask = "B"; else ask = "I"; end
+ else
+ if item.ask then ask = "O"; else ask = "N"; end
+ end
+ local r = self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?, '', '', '')", user, jid, item.nick or "", subscription, ask);
+ if not r then module:log("debug", "--- :( %s", tostring(r)); end
+ done[jid] = true;
+ for group in pairs(item.groups) do
+ self:modify("insert into rostergroups (username,jid,grp) values (?, ?, ?)", user, jid, group);
+ end
+ end
+ end
+ for jid in pairs(pending) do
+ if not done[jid] then
+ self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?. ''. ''. '')", user, jid, "", "N", "I");
+ end
+ end
+ local version = data[false] and data[false].version;
+ if version then
+ self:modify("insert into roster_version (username,version) values (?, ?)", user, version);
+ end
+ return true;
+ else
+ self:modify("delete from rosterusers where username=?", user);
+ self:modify("delete from rostergroups where username=?", user);
+ self:modify("delete from roster_version where username=?", user);
+ end
+ end;
+};
+
+-----------------------------
+local driver = {};
+driver.__index = driver;
+
+function driver:prepare(sql)
+ module:log("debug", "query: %s", sql);
+ local err;
+ if not self.sqlcache then self.sqlcache = {}; end
+ local r = self.sqlcache[sql];
+ if r then return r; end
+ r, err = self.database:prepare(sql);
+ if not r then error("Unable to prepare SQL statement: "..err); end
+ self.sqlcache[sql] = r;
+ return r;
+end
+
+function driver:query(sql, ...)
+ local stmt = self:prepare(sql);
+ if stmt:execute(...) then return stmt; end
+end
+function driver:modify(sql, ...)
+ local stmt = self:query(sql, ...);
+ if stmt and stmt:affected() > 0 then return stmt; end
+end
+
+function driver:open(host, datastore, typ)
+ local cache_key = host.." "..datastore;
+ if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
+ local instance = setmetatable({}, self);
+ instance.host = host;
+ instance.datastore = datastore;
+ local handler = handlers[datastore];
+ if not handler then return nil; end
+ for key,val in pairs(handler) do
+ instance[key] = val;
+ end
+ if instance.init then instance:init(); end
+ self.ds_cache[cache_key] = instance;
+ return instance;
+end
+
+-----------------------------
+local _M = {};
+
+function _M.new(dbtype, dbname, ...)
+ local instance = setmetatable({}, driver);
+ instance.__index = instance;
+ instance.database = get_database(dbtype, dbname, ...);
+ instance.ds_cache = {};
+ return instance;
+end
+
+return _M;
diff --git a/plugins/storage/mod_storage.lua b/plugins/storage/mod_storage.lua new file mode 100644 index 00000000..e9f5f923 --- /dev/null +++ b/plugins/storage/mod_storage.lua @@ -0,0 +1,83 @@ + +module:set_global(); + +local cache = { data = {} }; +function cache:get(key) return self.data[key]; end +function cache:set(key, val) self.data[key] = val; return val; end + +local _,DBI = pcall(require, "DBI"); +function get_database(driver, db, ...) + local uri = "dbi:"..driver..":"..db; + return cache:get(uri) or cache:set(uri, (function(...) + module:log("debug", "Opening database: %s", uri); + prosody.unlock_globals(); + local dbh = assert(DBI.Connect(...)); + prosody.lock_globals(); + dbh:autocommit(true) + return dbh; + end)(driver, db, ...)); +end + +local st = require "util.stanza"; +local _parse_xml = module:require("xmlparse"); +parse_xml_real = _parse_xml; +function parse_xml(str) + local s = _parse_xml(str); + if s and not s.gsub then + return st.preserialize(s); + end +end +function unparse_xml(s) + return tostring(st.deserialize(s)); +end + +local drivers = {}; + +--local driver = module:require("sqlbasic").new("SQLite3", "hello.sqlite"); +local option_datastore = module:get_option("datastore"); +local option_datastore_params = module:get_option("datastore_params") or {}; +if option_datastore then + local driver = module:require(option_datastore).new(unpack(option_datastore_params)); + table.insert(drivers, driver); +end + +local datamanager = require "util.datamanager"; +local olddm = {}; +local dm = {}; +for key,val in pairs(datamanager) do olddm[key] = val; end + +do -- driver based on old datamanager + local dmd = {}; + dmd.__index = dmd; + function dmd:open(host, datastore) + return setmetatable({ host = host, datastore = datastore }, dmd); + end + function dmd:get(user) return olddm.load(user, self.host, self.datastore); end + function dmd:set(user, data) return olddm.store(user, self.host, self.datastore, data); end + table.insert(drivers, dmd); +end + +local function open(...) + for _,driver in pairs(drivers) do + local ds = driver:open(...); + if ds then return ds; end + end +end + +local _data_path; +--function dm.set_data_path(path) _data_path = path; end +--function dm.add_callback(...) end +--function dm.remove_callback(...) end +--function dm.getpath(...) end +function dm.load(username, host, datastore) + local x = open(host, datastore); + return x:get(username); +end +function dm.store(username, host, datastore, data) + return open(host, datastore):set(username, data); +end +--function dm.list_append(...) return driver:list_append(...); end +--function dm.list_store(...) return driver:list_store(...); end +--function dm.list_load(...) return driver:list_load(...); end + +for key,val in pairs(dm) do datamanager[key] = val; end diff --git a/plugins/storage/sqlbasic.lib.lua b/plugins/storage/sqlbasic.lib.lua new file mode 100644 index 00000000..f1202287 --- /dev/null +++ b/plugins/storage/sqlbasic.lib.lua @@ -0,0 +1,97 @@ + +-- Basic SQL driver +-- This driver stores data as simple key-values + +local ser = require "util.serialization".serialize; +local deser = function(data) + module:log("debug", "deser: %s", tostring(data)); + if not data then return nil; end + local f = loadstring("return "..data); + if not f then return nil; end + setfenv(f, {}); + local s, d = pcall(f); + if not s then return nil; end + return d; +end; + +local driver = {}; +driver.__index = driver; + +driver.item_table = "item"; +driver.list_table = "list"; + +function driver:prepare(sql) + module:log("debug", "query: %s", sql); + local err; + if not self.sqlcache then self.sqlcache = {}; end + local r = self.sqlcache[sql]; + if r then return r; end + r, err = self.connection:prepare(sql); + if not r then error("Unable to prepare SQL statement: "..err); end + self.sqlcache[sql] = r; + return r; +end + +function driver:load(username, host, datastore) + local select = self:prepare("select data from "..self.item_table.." where username=? and host=? and datastore=?"); + select:execute(username, host, datastore); + local row = select:fetch(); + return row and deser(row[1]) or nil; +end + +function driver:store(username, host, datastore, data) + if not data or next(data) == nil then + local delete = self:prepare("delete from "..self.item_table.." where username=? and host=? and datastore=?"); + delete:execute(username, host, datastore); + return true; + else + local d = self:load(username, host, datastore); + if d then -- update + local update = self:prepare("update "..self.item_table.." set data=? where username=? and host=? and datastore=?"); + return update:execute(ser(data), username, host, datastore); + else -- insert + local insert = self:prepare("insert into "..self.item_table.." values (?, ?, ?, ?)"); + return insert:execute(username, host, datastore, ser(data)); + end + end +end + +function driver:list_append(username, host, datastore, data) + if not data then return; end + local insert = self:prepare("insert into "..self.list_table.." values (?, ?, ?, ?)"); + return insert:execute(username, host, datastore, ser(data)); +end + +function driver:list_store(username, host, datastore, data) + -- remove existing data + local delete = self:prepare("delete from "..self.list_table.." where username=? and host=? and datastore=?"); + delete:execute(username, host, datastore); + if data and next(data) ~= nil then + -- add data + for _, d in ipairs(data) do + self:list_append(username, host, datastore, ser(d)); + end + end + return true; +end + +function driver:list_load(username, host, datastore) + local select = self:prepare("select data from "..self.list_table.." where username=? and host=? and datastore=?"); + select:execute(username, host, datastore); + local r = {}; + for row in select:rows() do + table.insert(r, deser(row[1])); + end + return r; +end + +local _M = {}; +function _M.new(dbtype, dbname, ...) + local d = {}; + setmetatable(d, driver); + local dbh = get_database(dbtype, dbname, ...); + --d:set_connection(dbh); + d.connection = dbh; + return d; +end +return _M; diff --git a/plugins/storage/xep227store.lib.lua b/plugins/storage/xep227store.lib.lua new file mode 100644 index 00000000..5ef8df54 --- /dev/null +++ b/plugins/storage/xep227store.lib.lua @@ -0,0 +1,168 @@ +
+local st = require "util.stanza";
+
+local function getXml(user, host)
+ local jid = user.."@"..host;
+ local path = "data/"..jid..".xml";
+ local f = io.open(path);
+ if not f then return; end
+ local s = f:read("*a");
+ return parse_xml_real(s);
+end
+local function setXml(user, host, xml)
+ local jid = user.."@"..host;
+ local path = "data/"..jid..".xml";
+ if xml then
+ local f = io.open(path, "w");
+ if not f then return; end
+ local s = tostring(xml);
+ f:write(s);
+ f:close();
+ return true;
+ else
+ return os.remove(path);
+ end
+end
+local function getUserElement(xml)
+ if xml and xml.name == "server-data" then
+ local host = xml.tags[1];
+ if host and host.name == "host" then
+ local user = host.tags[1];
+ if user and user.name == "user" then
+ return user;
+ end
+ end
+ end
+end
+local function createOuterXml(user, host)
+ return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'})
+ :tag("host", {jid=host})
+ :tag("user", {name = user});
+end
+local function removeFromArray(array, value)
+ for i,item in ipairs(array) do
+ if item == value then
+ table.remove(array, i);
+ return;
+ end
+ end
+end
+local function removeStanzaChild(s, child)
+ removeFromArray(s.tags, child);
+ removeFromArray(s, child);
+end
+
+local handlers = {};
+
+handlers.accounts = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user and user.attr.password then
+ return { password = user.attr.password };
+ end
+ end;
+ set = function(self, user, data)
+ if data and data.password then
+ local xml = getXml(user, self.host);
+ if not xml then xml = createOuterXml(user, self.host); end
+ local usere = getUserElement(xml);
+ usere.attr.password = data.password;
+ return setXml(user, self.host, xml);
+ else
+ return setXml(user, self.host, nil);
+ end
+ end;
+};
+handlers.vcard = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user then
+ local vcard = user:get_child("vCard", 'vcard-temp');
+ if vcard then
+ return st.preserialize(vcard);
+ end
+ end
+ end;
+ set = function(self, user, data)
+ local xml = getXml(user, self.host);
+ local usere = xml and getUserElement(xml);
+ if usere then
+ local vcard = usere:get_child("vCard", 'vcard-temp');
+ if vcard then
+ removeStanzaChild(usere, vcard);
+ elseif not data then
+ return true;
+ end
+ if data then
+ vcard = st.deserialize(data);
+ usere:add_child(vcard);
+ end
+ return setXml(user, self.host, xml);
+ end
+ return true;
+ end;
+};
+handlers.private = {
+ get = function(self, user)
+ local user = getUserElement(getXml(user, self.host));
+ if user then
+ local private = user:get_child("query", "jabber:iq:private");
+ if private then
+ local r = {};
+ for _, tag in ipairs(private.tags) do
+ r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag);
+ end
+ return r;
+ end
+ end
+ end;
+ set = function(self, user, data)
+ local xml = getXml(user, self.host);
+ local usere = xml and getUserElement(xml);
+ if usere then
+ local private = usere:get_child("query", 'jabber:iq:private');
+ if private then removeStanzaChild(usere, private); end
+ if data and next(data) ~= nil then
+ private = st.stanza("query", {xmlns='jabber:iq:private'});
+ for _,tag in pairs(data) do
+ private:add_child(st.deserialize(tag));
+ end
+ usere:add_child(private);
+ end
+ return setXml(user, self.host, xml);
+ end
+ return true;
+ end;
+};
+
+-----------------------------
+local driver = {};
+driver.__index = driver;
+
+function driver:open(host, datastore, typ)
+ local cache_key = host.." "..datastore;
+ if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
+ local instance = setmetatable({}, self);
+ instance.host = host;
+ instance.datastore = datastore;
+ local handler = handlers[datastore];
+ if not handler then return nil; end
+ for key,val in pairs(handler) do
+ instance[key] = val;
+ end
+ if instance.init then instance:init(); end
+ self.ds_cache[cache_key] = instance;
+ return instance;
+end
+
+-----------------------------
+local _M = {};
+
+function _M.new()
+ local instance = setmetatable({}, driver);
+ instance.__index = instance;
+ instance.ds_cache = {};
+ return instance;
+end
+
+return _M;
diff --git a/plugins/storage/xmlparse.lib.lua b/plugins/storage/xmlparse.lib.lua new file mode 100644 index 00000000..91063995 --- /dev/null +++ b/plugins/storage/xmlparse.lib.lua @@ -0,0 +1,56 @@ +
+local st = require "util.stanza";
+
+-- XML parser
+local parse_xml = (function()
+ local entity_map = setmetatable({
+ ["amp"] = "&";
+ ["gt"] = ">";
+ ["lt"] = "<";
+ ["apos"] = "'";
+ ["quot"] = "\"";
+ }, {__index = function(_, s)
+ if s:sub(1,1) == "#" then
+ if s:sub(2,2) == "x" then
+ return string.char(tonumber(s:sub(3), 16));
+ else
+ return string.char(tonumber(s:sub(2)));
+ end
+ end
+ end
+ });
+ local function xml_unescape(str)
+ return (str:gsub("&(.-);", entity_map));
+ end
+ local function parse_tag(s)
+ local name,sattr=(s):gmatch("([^%s]+)(.*)")();
+ local attr = {};
+ for a,b in (sattr):gmatch("([^=%s]+)=['\"]([^'\"]*)['\"]") do attr[a] = xml_unescape(b); end
+ return name, attr;
+ end
+ return function(xml)
+ local stanza = st.stanza("root");
+ local regexp = "<([^>]*)>([^<]*)";
+ for elem, text in xml:gmatch(regexp) do
+ if elem:sub(1,1) == "!" or elem:sub(1,1) == "?" then -- neglect comments and processing-instructions
+ elseif elem:sub(1,1) == "/" then -- end tag
+ elem = elem:sub(2);
+ stanza:up(); -- TODO check for start-end tag name match
+ elseif elem:sub(-1,-1) == "/" then -- empty tag
+ elem = elem:sub(1,-2);
+ local name,attr = parse_tag(elem);
+ stanza:tag(name, attr):up();
+ else -- start tag
+ local name,attr = parse_tag(elem);
+ stanza:tag(name, attr);
+ end
+ if #text ~= 0 then -- text
+ stanza:text(xml_unescape(text));
+ end
+ end
+ return stanza.tags[1];
+ end
+end)();
+-- end of XML parser
+
+return parse_xml;
|