diff options
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/mod_auth_internal.lua | 96 | ||||
-rw-r--r-- | plugins/mod_auth_internal_hashed.lua | 128 | ||||
-rw-r--r-- | plugins/mod_compression.lua | 62 | ||||
-rw-r--r-- | plugins/mod_groups.lua | 10 | ||||
-rw-r--r-- | plugins/mod_iq.lua | 11 | ||||
-rw-r--r-- | plugins/mod_motd.lua | 25 | ||||
-rw-r--r-- | plugins/mod_offline.lua | 56 | ||||
-rw-r--r-- | plugins/mod_pep.lua | 44 | ||||
-rw-r--r-- | plugins/mod_posix.lua | 22 | ||||
-rw-r--r-- | plugins/mod_presence.lua | 36 | ||||
-rw-r--r-- | plugins/mod_register.lua | 4 | ||||
-rw-r--r-- | plugins/mod_saslauth.lua | 24 | ||||
-rw-r--r-- | plugins/muc/muc.lib.lua | 62 | ||||
-rw-r--r-- | plugins/storage/ejabberdstore.lib.lua | 190 | ||||
-rw-r--r-- | plugins/storage/mod_storage.lua | 83 | ||||
-rw-r--r-- | plugins/storage/sqlbasic.lib.lua | 97 | ||||
-rw-r--r-- | plugins/storage/xep227store.lib.lua | 168 | ||||
-rw-r--r-- | plugins/storage/xmlparse.lib.lua | 56 |
18 files changed, 1062 insertions, 112 deletions
diff --git a/plugins/mod_auth_internal.lua b/plugins/mod_auth_internal.lua new file mode 100644 index 00000000..78a75a1d --- /dev/null +++ b/plugins/mod_auth_internal.lua @@ -0,0 +1,96 @@ +-- 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("usermanager"); +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 hosts = hosts; + +local prosody = _G.prosody; + +local is_cyrus = usermanager.is_cyrus; + +function new_default_provider(host) + local provider = { name = "internal" }; + 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 + if account.password == nil or string.len(account.password) == 0 then + log("debug", "account password not set or zero-length for username '%s' at host '%s'", username, module.host); + return nil, "Auth failed. Password invalid."; + 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_supported_methods() + return {["PLAIN"] = true, ["DIGEST-MD5"] = true}; -- TODO this should be taken from the config + end + + function provider.is_admin(jid) + local admins = config.get(host, "core", "admins"); + if admins ~= config.get("*", "core", "admins") and type(admins) == "table" then + jid = jid_bare(jid); + for _,admin in ipairs(admins) do + if admin == jid then return true; end + end + elseif admins then + log("error", "Option 'admins' for host '%s' is not a table", host); + end + return is_admin(jid); -- Test whether it's a global admin instead + 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..e2c423f2 --- /dev/null +++ b/plugins/mod_auth_internal_hashed.lua @@ -0,0 +1,128 @@ +-- 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("usermanager"); +local type = type; +local error = error; +local ipairs = ipairs; +local hashes = require "util.hashes"; +local jid_bare = require "util.jid".bare; +local saltedPasswordSHA1 = require "util.sasl.scram".saltedPasswordSHA1; +local config = require "core.configmanager"; +local usermanager = require "core.usermanager"; +local generate_uuid = require "util.uuid".generate; +local hosts = hosts; + +local prosody = _G.prosody; + +local is_cyrus = usermanager.is_cyrus; + +-- 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) + if is_cyrus(host) then return nil, "Legacy auth not supported with Cyrus SASL."; end + 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 + + local valid, binpass = saltedPasswordSHA1(password, credentials.salt, credentials.iteration_count); + local hexpass = binpass:gsub(".", function (c) return ("%02x"):format(c:byte()); end); + + if valid and hexpass == credentials.hashpass then + return true; + else + return nil, "Auth failed. Invalid username, password, or password hash information."; + end + 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 + if account.iteration_count == nil then + account.iteration_count = iteration_count; + end + + if account.salt == nil then + account.salt = generate_uuid(); + end + + local valid, binpass = saltedPasswordSHA1(password, account.salt, account.iteration_count); + local hexpass = binpass:gsub(".", function (c) return ("%02x"):format(c:byte()); end); + account.hashpass = hexpass; + + account.password = nil; + 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 + if (account.hashpass == nil or string.len(account.hashpass) == 0) and (account.password == nil or string.len(account.password) == 0) then + log("debug", "account password not set or zero-length for username '%s' at host '%s'", username, module.host); + return nil, "Auth failed. Password invalid."; + 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 + local salt = generate_uuid(); + local valid, binpass = saltedPasswordSHA1(password, salt, iteration_count); + local hexpass = binpass:gsub(".", function (c) return ("%02x"):format(c:byte()); end); + return datamanager.store(username, host, "accounts", {hashpass = hexpass, salt = salt, iteration_count = iteration_count}); + end + + function provider.get_supported_methods() + return {["PLAIN"] = true}; -- TODO this should be taken from the config + end + + function provider.is_admin(jid) + local admins = config.get(host, "core", "admins"); + if admins ~= config.get("*", "core", "admins") and type(admins) == "table" then + jid = jid_bare(jid); + for _,admin in ipairs(admins) do + if admin == jid then return true; end + end + elseif admins then + log("error", "Option 'admins' for host '%s' is not a table", host); + end + return is_admin(jid); -- Test whether it's a global admin instead + end + return provider; +end + +module:add_item("auth-provider", new_hashpass_provider(module.host)); + diff --git a/plugins/mod_compression.lua b/plugins/mod_compression.lua index c2e84f2b..0e1aab8c 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,44 +95,37 @@ end -- setup compression for a stream local function setup_compression(session, deflate_stream) - local old_send = (session.sends2s or session.send); - - local new_send = function(t) - --TODO: Better code injection in the sending process - session.log(t) - local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync'); - if status == false then - session:close({ - condition = "undefined-condition"; - text = compressed; - extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); - }); - module:log("warn", "%s", tostring(compressed)); - return; - end - session.conn:write(compressed); - end; - - if session.sends2s then session.sends2s = new_send - elseif session.send then session.send = new_send end + add_filter(session, "bytes/out", function(t) + session.log(t) + local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync'); + if status == false then + session:close({ + condition = "undefined-condition"; + text = compressed; + extra = st.stanza("failure", {xmlns="http://jabber.org/protocol/compress"}):tag("processing-failed"); + }); + module:log("warn", "%s", tostring(compressed)); + return; + end + 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 + 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 + return decompressed; + end); end module:add_handler({"s2sout_unauthed", "s2sout"}, "compressed", xmlns_compression_protocol, 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_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_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..31546dff 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -16,7 +16,6 @@ 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; @@ -40,8 +39,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 +117,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); diff --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua index c38f7eba..52b1e0e6 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; @@ -141,7 +141,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..6da0f863 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -31,7 +31,7 @@ function core_route_stanza(origin, stanza) 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); + handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to)); return; end end @@ -64,7 +64,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 @@ -97,7 +97,7 @@ 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 @@ -159,7 +159,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 @@ -181,13 +181,16 @@ 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 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 @@ -209,7 +212,7 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_ 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); + 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) @@ -219,9 +222,10 @@ function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_ core_route_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,7 +234,7 @@ 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 + if 0 == send_presence_of_available_resources(node, host, st_from, origin) then core_route_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"})); -- TODO send last activity end elseif not err then @@ -240,7 +244,7 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b 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 -- 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 + if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then core_route_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"})); -- TODO send last activity end else @@ -268,6 +272,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,8 +283,7 @@ 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); @@ -306,8 +310,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 +322,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 +332,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]; diff --git a/plugins/mod_register.lua b/plugins/mod_register.lua index b8d142f7..7e150ac7 100644 --- a/plugins/mod_register.lua +++ b/plugins/mod_register.lua @@ -35,7 +35,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)); @@ -70,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_saslauth.lua b/plugins/mod_saslauth.lua index 9f940c37..c16a14da 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -15,10 +15,11 @@ 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_provider = require "core.usermanager".get_provider; local usermanager_get_supported_methods = require "core.usermanager".get_supported_methods; local usermanager_user_exists = require "core.usermanager".user_exists; local usermanager_get_password = require "core.usermanager".get_password; +local usermanager_test_password = require "core.usermanager".test_password; local t_concat, t_insert = table.concat, table.insert; local tostring = tostring; local jid_split = require "util.jid".split; @@ -66,7 +67,7 @@ else error("Unknown SASL backend"); end -local default_authentication_profile = { +local getpass_authentication_profile = { plain = function(username, realm) local prepped_username = nodeprep(username); if not prepped_username then @@ -81,6 +82,17 @@ local default_authentication_profile = { end }; +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, password, realm), true; + end +}; + local anonymous_authentication_profile = { anonymous = function(username, realm) return true; -- for normal usage you should always return true here @@ -183,7 +195,13 @@ module:hook("stream-features", function(event) if module:get_option("anonymous_login") then origin.sasl_handler = new_sasl(realm, anonymous_authentication_profile); else - origin.sasl_handler = new_sasl(realm, default_authentication_profile); + if usermanager_get_provider(realm).get_password then + origin.sasl_handler = new_sasl(realm, getpass_authentication_profile); + elseif usermanager_get_provider(realm).test_password then + origin.sasl_handler = new_sasl(realm, testpass_authentication_profile); + else + log("warn", "AUTH: Could not load an authentication profile for the given provider."); + end if not (module:get_option("allow_unencrypted_plain_auth")) and not origin.secure then origin.sasl_handler:forbidden({"PLAIN"}); end diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index 18c80325..273e21ce 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -122,9 +122,13 @@ 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)); + local entry = { stanza = stanza, stamp = stamp }; + t_insert(history, entry); while #history > history_length do t_remove(history, 1) end end end @@ -151,12 +155,46 @@ 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 @@ -319,12 +357,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"; @@ -751,7 +784,7 @@ 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 + if self:get_affiliation(actor) ~= "owner" then return nil, "cancel", "not-allowed"; 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 @@ -804,6 +837,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 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..e22de82c --- /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 = 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;
|