# HG changeset patch # User Waqas Hussain # Date 1273148051 -18000 # Node ID 4164023a2258a4898962fa26bf3890751334efee # Parent b6046df19898209aaf40c6b704d46039b28be8dc# Parent 6b2e4b5817170a7cb01480084991a813af3fdb1c Merge 0.7->trunk diff -r 6b2e4b581717 -r 4164023a2258 core/configmanager.lua --- a/core/configmanager.lua Thu May 06 16:25:40 2010 +0500 +++ b/core/configmanager.lua Thu May 06 17:14:11 2010 +0500 @@ -30,10 +30,11 @@ -- When key not found in section, check key in global's section function section_mt(section_name) return { __index = function (t, k) - local section = rawget(global_config, section_name); - if not section then return nil; end - return section[k]; - end }; + local section = rawget(global_config, section_name); + if not section then return nil; end + return section[k]; + end + }; end function getconfig() @@ -112,16 +113,20 @@ function parsers.lua.load(data, filename) local env; -- The ' = true' are needed so as not to set off __newindex when we assign the functions below - env = setmetatable({ Host = true, host = true, VirtualHost = true, Component = true, component = true, - Include = true, include = true, RunScript = dofile }, { __index = function (t, k) - return rawget(_G, k) or - function (settings_table) - config[__currenthost or "*"][k] = settings_table; - end; - end, - __newindex = function (t, k, v) - set(env.__currenthost or "*", "core", k, v); - end}); + env = setmetatable({ + Host = true, host = true, VirtualHost = true, + Component = true, component = true, + Include = true, include = true, RunScript = dofile }, { + __index = function (t, k) + return rawget(_G, k) or + function (settings_table) + config[__currenthost or "*"][k] = settings_table; + end; + end, + __newindex = function (t, k, v) + set(env.__currenthost or "*", "core", k, v); + end + }); rawset(env, "__currenthost", "*") -- Default is global function env.VirtualHost(name) diff -r 6b2e4b581717 -r 4164023a2258 core/eventmanager.lua --- a/core/eventmanager.lua Thu May 06 16:25:40 2010 +0500 +++ b/core/eventmanager.lua Thu May 06 17:14:11 2010 +0500 @@ -10,24 +10,18 @@ local t_insert = table.insert; local ipairs = ipairs; +local events = _G.prosody.events; + module "eventmanager" local event_handlers = {}; function add_event_hook(name, handler) - if not event_handlers[name] then - event_handlers[name] = {}; - end - t_insert(event_handlers[name] , handler); + return events.add_handler(name, handler); end function fire_event(name, ...) - local event_handlers = event_handlers[name]; - if event_handlers then - for name, handler in ipairs(event_handlers) do - handler(...); - end - end + return events.fire_event(name, ...); end -return _M; \ No newline at end of file +return _M; diff -r 6b2e4b581717 -r 4164023a2258 core/loggingmanager.lua diff -r 6b2e4b581717 -r 4164023a2258 core/usermanager.lua --- a/core/usermanager.lua Thu May 06 16:25:40 2010 +0500 +++ b/core/usermanager.lua Thu May 06 17:14:11 2010 +0500 @@ -16,82 +16,117 @@ local config = require "core.configmanager"; local hosts = hosts; +local prosody = _G.prosody; + module "usermanager" +local new_default_provider; + +prosody.events.add_handler("host-activated", function (host) + local host_session = hosts[host]; + host_session.events.add_handler("item-added/auth-provider", function (provider) + if config.get(host, "core", "authentication") == provider.name then + host_session.users = provider; + end + end); + host_session.events.add_handler("item-removed/auth-provider", function (provider) + if host_session.users == provider then + host_session.users = new_default_provider(host); + end + end); + host_session.users = new_default_provider(host); -- Start with the default usermanager provider +end); + local function is_cyrus(host) return config.get(host, "core", "sasl_backend") == "cyrus"; end -function validate_credentials(host, username, password, method) - log("debug", "User '%s' is being validated", username); - if is_cyrus(host) then return nil, "Legacy auth not supported with Cyrus SASL."; end - local credentials = datamanager.load(username, host, "accounts") or {}; - - if method == nil then method = "PLAIN"; end - if method == "PLAIN" and credentials.password then -- PLAIN, do directly +function new_default_provider(host) + local provider = { name = "default" }; + + 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 password == credentials.password then return true; else return nil, "Auth failed. Invalid username or password."; end - end - -- must do md5 - -- make credentials md5 - local pwd = credentials.password; - if not pwd then pwd = credentials.md5; else pwd = hashes.md5(pwd, true); end - -- make password md5 - if method == "PLAIN" then - password = hashes.md5(password or "", true); - elseif method ~= "DIGEST-MD5" then - return nil, "Unsupported auth method"; + end + + function provider.get_password(username) + 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 + return datamanager.load(username, host, "accounts") ~= nil; -- FIXME also check for empty credentials + 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 - -- compare - if password == pwd then - return true; - else - return nil, "Auth failed. Invalid username or password."; + + 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) + host = host or "*"; + local admins = config.get(host, "core", "admins"); + if host ~= "*" and admins == config.get("*", "core", "admins") then + return nil; + end + if 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("warn", "Option 'admins' for host '%s' is not a table", host); + end + return nil; + end + return provider; +end + +function validate_credentials(host, username, password, method) + return hosts[host].users.test_password(username, password); end function get_password(username, host) - if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end - return (datamanager.load(username, host, "accounts") or {}).password + return hosts[host].users.get_password(username); end + function set_password(username, host, 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."; + return hosts[host].users.set_password(username, password); end function user_exists(username, host) - if is_cyrus(host) then return true; end - return datamanager.load(username, host, "accounts") ~= nil; -- FIXME also check for empty credentials + return hosts[host].users.user_exists(username); end function create_user(username, password, host) - if is_cyrus(host) then return nil, "Account creation/modification not available with Cyrus SASL."; end - return datamanager.store(username, host, "accounts", {password = password}); + return hosts[host].users.create_user(username, password); end function get_supported_methods(host) - return {["PLAIN"] = true, ["DIGEST-MD5"] = true}; -- TODO this should be taken from the config + return hosts[host].users.get_supported_methods(); end function is_admin(jid, host) - host = host or "*"; - local admins = config.get(host, "core", "admins"); - if host ~= "*" and admins == config.get("*", "core", "admins") then - return nil; - end - if 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("warn", "Option 'admins' for host '%s' is not a table", host); end - return nil; + return hosts[host].users.is_admin(jid); end return _M; diff -r 6b2e4b581717 -r 4164023a2258 net/server_select.lua --- a/net/server_select.lua Thu May 06 16:25:40 2010 +0500 +++ b/net/server_select.lua Thu May 06 17:14:11 2010 +0500 @@ -520,6 +520,9 @@ out_put( "server.lua: ssl handshake done" ) handler.readbuffer = _readbuffer -- when handshake is done, replace the handshake function with regular functions handler.sendbuffer = _sendbuffer + out_put ("server.lua: compression used: "..tostring(client:compression())) + out_put ("server.lua: finished: "..tostring(client:getfinished()):sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_")) + out_put ("server.lua: peer finished: "..tostring(client:getpeerfinished()):sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_")) _ = status and status( handler, "ssl-handshake-complete" ) _readlistlen = addsocket(_readlist, client, _readlistlen) return true diff -r 6b2e4b581717 -r 4164023a2258 plugins/mod_offline.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/mod_offline.lua Thu May 06 17:14:11 2010 +0500 @@ -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 -r 6b2e4b581717 -r 4164023a2258 plugins/muc/muc.lib.lua --- a/plugins/muc/muc.lib.lua Thu May 06 16:25:40 2010 +0500 +++ b/plugins/muc/muc.lib.lua Thu May 06 17:14:11 2010 +0500 @@ -122,9 +122,13 @@ 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 @@ 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 @@ :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 @@ 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 @@ 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 -r 6b2e4b581717 -r 4164023a2258 plugins/storage/ejabberdstore.lib.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/storage/ejabberdstore.lib.lua Thu May 06 17:14:11 2010 +0500 @@ -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 -r 6b2e4b581717 -r 4164023a2258 plugins/storage/mod_storage.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/storage/mod_storage.lua Thu May 06 17:14:11 2010 +0500 @@ -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 -r 6b2e4b581717 -r 4164023a2258 plugins/storage/sqlbasic.lib.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/storage/sqlbasic.lib.lua Thu May 06 17:14:11 2010 +0500 @@ -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 -r 6b2e4b581717 -r 4164023a2258 plugins/storage/xep227store.lib.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/storage/xep227store.lib.lua Thu May 06 17:14:11 2010 +0500 @@ -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 -r 6b2e4b581717 -r 4164023a2258 plugins/storage/xmlparse.lib.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/storage/xmlparse.lib.lua Thu May 06 17:14:11 2010 +0500 @@ -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; diff -r 6b2e4b581717 -r 4164023a2258 prosody --- a/prosody Thu May 06 16:25:40 2010 +0500 +++ b/prosody Thu May 06 17:14:11 2010 +0500 @@ -22,9 +22,6 @@ package.cpath = CFG_SOURCEDIR.."/?.so;"..package.cpath; end -package.path = package.path..";"..(CFG_SOURCEDIR or ".").."/fallbacks/?.lua"; -package.cpath = package.cpath..";"..(CFG_SOURCEDIR or ".").."/fallbacks/?.so"; - -- Substitute ~ with path to home directory in data path if CFG_DATADIR then if os.getenv("HOME") then @@ -32,6 +29,10 @@ end end +-- Global 'prosody' object +prosody = { events = require "util.events".new(); }; +local prosody = prosody; + -- Load the config-parsing module config = require "core.configmanager" @@ -151,10 +152,6 @@ full_sessions = {}; hosts = {}; - -- Global 'prosody' object - prosody = {}; - local prosody = prosody; - prosody.bare_sessions = bare_sessions; prosody.full_sessions = full_sessions; prosody.hosts = hosts; @@ -164,8 +161,6 @@ prosody.arg = _G.arg; - prosody.events = require "util.events".new(); - prosody.platform = "unknown"; if os.getenv("WINDIR") then prosody.platform = "windows"; @@ -196,7 +191,6 @@ -- Function to reopen logfiles function prosody.reopen_logfiles() log("info", "Re-opening log files"); - eventmanager.fire_event("reopen-log-files"); -- Handled by appropriate log sinks prosody.events.fire_event("reopen-log-files"); end @@ -289,7 +283,6 @@ require "util.import" require "core.xmlhandlers" require "core.rostermanager" - require "core.eventmanager" require "core.hostmanager" require "core.modulemanager" require "core.usermanager" @@ -333,7 +326,6 @@ function prepare_to_start() log("info", "Prosody is using the %s backend for connection handling", server.get_backend()); -- Signal to modules that we are ready to start - eventmanager.fire_event("server-starting"); prosody.events.fire_event("server-starting"); -- start listening on sockets @@ -451,14 +443,12 @@ init_global_protection(); prepare_to_start(); -eventmanager.fire_event("server-started"); prosody.events.fire_event("server-started"); loop(); log("info", "Shutting down..."); cleanup(); -eventmanager.fire_event("server-stopped"); prosody.events.fire_event("server-stopped"); log("info", "Shutdown complete"); diff -r 6b2e4b581717 -r 4164023a2258 prosodyctl --- a/prosodyctl Thu May 06 16:25:40 2010 +0500 +++ b/prosodyctl Thu May 06 17:14:11 2010 +0500 @@ -29,6 +29,14 @@ end end +-- Global 'prosody' object +prosody = { + hosts = {}, + events = require "util.events".new(), + platform = "posix" +}; +local prosody = prosody; + config = require "core.configmanager" do @@ -63,8 +71,6 @@ os.exit(1); end -prosody = { hosts = {}, events = events, platform = "posix" }; - local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data"; require "util.datamanager".set_data_path(data_path); @@ -114,12 +120,10 @@ ["not-running"] = "Prosody is not running"; }, { __index = function (t,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end }); -local events = require "util.events".new(); - hosts = prosody.hosts; for hostname, config in pairs(config.getconfig()) do - hosts[hostname] = { events = events }; + hosts[hostname] = { events = prosody.events }; end require "core.modulemanager" diff -r 6b2e4b581717 -r 4164023a2258 util/sasl.lua --- a/util/sasl.lua Thu May 06 16:25:40 2010 +0500 +++ b/util/sasl.lua Thu May 06 17:14:11 2010 +0500 @@ -41,27 +41,6 @@ state = false : disabled state = true : enabled state = nil : non-existant - -plain: - function(username, realm) - return password, state; - end - -plain-test: - function(username, realm, password) - return true or false, state; - end - -digest-md5: - function(username, domain, realm, encoding) -- domain and realm are usually the same; for some broken - -- implementations it's not - return digesthash, state; - end - -digest-md5-test: - function(username, domain, realm, encoding, digesthash) - return true or false, state; - end ]] local method = {}; diff -r 6b2e4b581717 -r 4164023a2258 util/sasl/anonymous.lua --- a/util/sasl/anonymous.lua Thu May 06 16:25:40 2010 +0500 +++ b/util/sasl/anonymous.lua Thu May 06 17:14:11 2010 +0500 @@ -1,5 +1,5 @@ -- sasl.lua v0.4 --- Copyright (C) 2008-2009 Tobias Markmann +-- Copyright (C) 2008-2010 Tobias Markmann -- -- All rights reserved. -- @@ -20,6 +20,16 @@ --========================= --SASL ANONYMOUS according to RFC 4505 + +--[[ +Supported Authentication Backends + +anonymous: + function(username, realm) + return true; --for normal usage just return true; if you don't like the supplied username you can return false. + end +]] + local function anonymous(self, message) local username; repeat diff -r 6b2e4b581717 -r 4164023a2258 util/sasl/digest-md5.lua --- a/util/sasl/digest-md5.lua Thu May 06 16:25:40 2010 +0500 +++ b/util/sasl/digest-md5.lua Thu May 06 17:14:11 2010 +0500 @@ -1,5 +1,5 @@ -- sasl.lua v0.4 --- Copyright (C) 2008-2009 Tobias Markmann +-- Copyright (C) 2008-2010 Tobias Markmann -- -- All rights reserved. -- @@ -29,6 +29,21 @@ --========================= --SASL DIGEST-MD5 according to RFC 2831 +--[[ +Supported Authentication Backends + +digest-md5: + function(username, domain, realm, encoding) -- domain and realm are usually the same; for some broken + -- implementations it's not + return digesthash, state; + end + +digest-md5-test: + function(username, domain, realm, encoding, digesthash) + return true or false, state; + end +]] + local function digest(self, message) --TODO complete support for authzid diff -r 6b2e4b581717 -r 4164023a2258 util/sasl/plain.lua --- a/util/sasl/plain.lua Thu May 06 16:25:40 2010 +0500 +++ b/util/sasl/plain.lua Thu May 06 17:14:11 2010 +0500 @@ -1,5 +1,5 @@ -- sasl.lua v0.4 --- Copyright (C) 2008-2009 Tobias Markmann +-- Copyright (C) 2008-2010 Tobias Markmann -- -- All rights reserved. -- @@ -19,6 +19,26 @@ -- ================================ -- SASL PLAIN according to RFC 4616 + +--[[ +Supported Authentication Backends + +plain: + function(username, realm) + return password, state; + end + +plain-test: + function(username, realm, password) + return true or false, state; + end + +plain-hashed: + function(username, realm) + return hashed_password, hash_function, state; + end +]] + local function plain(self, message) if not message then return "failure", "malformed-request"; @@ -46,6 +66,10 @@ if correct_password == password then correct = true; else correct = false; end elseif self.profile.plain_test then correct, state = self.profile.plain_test(authentication, self.realm, password); + elseif self.profile.plain_hashed then + local hashed_password, hash_f; + hashed_password, hash_f, state = self.profile.plain_hashed(authentication, self.realm); + if hashed_password == hash_f(password) then correct = true; else correct = false; end end self.username = authentication @@ -61,7 +85,7 @@ end function init(registerMechanism) - registerMechanism("PLAIN", {"plain", "plain_test"}, plain); + registerMechanism("PLAIN", {"plain", "plain_test", "plain_hashed"}, plain); end return _M; diff -r 6b2e4b581717 -r 4164023a2258 util/sasl/scram.lua --- a/util/sasl/scram.lua Thu May 06 16:25:40 2010 +0500 +++ b/util/sasl/scram.lua Thu May 06 17:14:11 2010 +0500 @@ -1,5 +1,5 @@ -- sasl.lua v0.4 --- Copyright (C) 2008-2009 Tobias Markmann +-- Copyright (C) 2008-2010 Tobias Markmann -- -- All rights reserved. -- @@ -28,6 +28,16 @@ --========================= --SASL SCRAM-SHA-1 according to draft-ietf-sasl-scram-10 + +--[[ +Supported Authentication Backends + +scram-{MECH}: + function(username, realm) + return salted_password, iteration_count, salt, state; + end +]] + local default_i = 4096 local function bp( b ) @@ -82,77 +92,95 @@ return username; end -local function scram_sha_1(self, message) - if not self.state then self["state"] = {} end +local function scram_gen(hash_name, H_f, HMAC_f) + local function scram_hash(self, message) + if not self.state then self["state"] = {} end - if not self.state.name then - -- we are processing client_first_message - local client_first_message = message; - self.state["client_first_message"] = client_first_message; - self.state["name"] = client_first_message:match("n=(.+),r=") - self.state["clientnonce"] = client_first_message:match("r=([^,]+)") + if not self.state.name then + -- we are processing client_first_message + local client_first_message = message; + self.state["client_first_message"] = client_first_message; + self.state["name"] = client_first_message:match("n=(.+),r=") + self.state["clientnonce"] = client_first_message:match("r=([^,]+)") - if not self.state.name or not self.state.clientnonce then - return "failure", "malformed-request"; - end + if not self.state.name or not self.state.clientnonce then + return "failure", "malformed-request"; + end - self.state.name = validate_username(self.state.name); - if not self.state.name then - log("debug", "Username violates either SASLprep or contains forbidden character sequences.") - return "failure", "malformed-request", "Invalid username."; - end - - self.state["servernonce"] = generate_uuid(); - self.state["salt"] = generate_uuid(); + self.state.name = validate_username(self.state.name); + if not self.state.name then + log("debug", "Username violates either SASLprep or contains forbidden character sequences.") + return "failure", "malformed-request", "Invalid username."; + end - local server_first_message = "r="..self.state.clientnonce..self.state.servernonce..",s="..base64.encode(self.state.salt)..",i="..default_i; - self.state["server_first_message"] = server_first_message; - return "challenge", server_first_message - else - if type(message) ~= "string" then return "failure", "malformed-request" end - -- we are processing client_final_message - local client_final_message = message; + self.state["servernonce"] = generate_uuid(); + + -- retreive credentials + if self.profile.plain then + local password, state = self.profile.plain(self.state.name, self.realm) + if state == nil then return "failure", "not-authorized" + elseif state == false then return "failure", "account-disabled" end + + password = saslprep(password); + if not password then + log("debug", "Password violates SASLprep."); + return "failure", "not-authorized", "Invalid password." + end + self.state.salt = generate_uuid(); + self.state.iteration_count = default_i; + self.state.salted_password = Hi(HMAC_f, password, self.state.salt, default_i); + elseif self.profile["scram_"..hash_name] then + local salted_password, iteration_count, salt, state = self.profile["scram-"..hash_name](self.state.name, self.realm); + if state == nil then return "failure", "not-authorized" + elseif state == false then return "failure", "account-disabled" end + + self.state.salted_password = salted_password; + self.state.iteration_count = iteration_count; + self.state.salt = salt + end - self.state["proof"] = client_final_message:match("p=(.+)"); - self.state["nonce"] = client_final_message:match("r=(.+),p="); - self.state["channelbinding"] = client_final_message:match("c=(.+),r="); - if not self.state.proof or not self.state.nonce or not self.state.channelbinding then - return "failure", "malformed-request", "Missing an attribute(p, r or c) in SASL message."; - end + local server_first_message = "r="..self.state.clientnonce..self.state.servernonce..",s="..base64.encode(self.state.salt)..",i="..self.state.iteration_count; + self.state["server_first_message"] = server_first_message; + return "challenge", server_first_message + else + if type(message) ~= "string" then return "failure", "malformed-request" end + -- we are processing client_final_message + local client_final_message = message; + + self.state["proof"] = client_final_message:match("p=(.+)"); + self.state["nonce"] = client_final_message:match("r=(.+),p="); + self.state["channelbinding"] = client_final_message:match("c=(.+),r="); + if not self.state.proof or not self.state.nonce or not self.state.channelbinding then + return "failure", "malformed-request", "Missing an attribute(p, r or c) in SASL message."; + end - local password, state; - if self.profile.plain then - password, state = self.profile.plain(self.state.name, self.realm) - if state == nil then return "failure", "not-authorized" - elseif state == false then return "failure", "account-disabled" end - password = saslprep(password); - if not password then - log("debug", "Password violates SASLprep."); - return "failure", "not-authorized", "Invalid password." + local SaltedPassword = self.state.salted_password; + local ClientKey = HMAC_f(SaltedPassword, "Client Key") + local ServerKey = HMAC_f(SaltedPassword, "Server Key") + local StoredKey = H_f(ClientKey) + local AuthMessage = "n=" .. s_match(self.state.client_first_message,"n=(.+)") .. "," .. self.state.server_first_message .. "," .. s_match(client_final_message, "(.+),p=.+") + local ClientSignature = HMAC_f(StoredKey, AuthMessage) + local ClientProof = binaryXOR(ClientKey, ClientSignature) + local ServerSignature = HMAC_f(ServerKey, AuthMessage) + + if base64.encode(ClientProof) == self.state.proof then + local server_final_message = "v="..base64.encode(ServerSignature); + self["username"] = self.state.name; + return "success", server_final_message; + else + return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."; end end - - local SaltedPassword = Hi(hmac_sha1, password, self.state.salt, default_i) - local ClientKey = hmac_sha1(SaltedPassword, "Client Key") - local ServerKey = hmac_sha1(SaltedPassword, "Server Key") - local StoredKey = sha1(ClientKey) - local AuthMessage = "n=" .. s_match(self.state.client_first_message,"n=(.+)") .. "," .. self.state.server_first_message .. "," .. s_match(client_final_message, "(.+),p=.+") - local ClientSignature = hmac_sha1(StoredKey, AuthMessage) - local ClientProof = binaryXOR(ClientKey, ClientSignature) - local ServerSignature = hmac_sha1(ServerKey, AuthMessage) - - if base64.encode(ClientProof) == self.state.proof then - local server_final_message = "v="..base64.encode(ServerSignature); - self["username"] = self.state.name; - return "success", server_final_message; - else - return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."; - end end + return scram_hash; end function init(registerMechanism) - registerMechanism("SCRAM-SHA-1", {"plain"}, scram_sha_1); + local function registerSCRAMMechanism(hash_name, hash, hmac_hash) + registerMechanism("SCRAM-"..hash_name, {"plain", "scram_"..(hash_name:lower())}, scram_gen(hash_name:lower(), hash, hmac_hash)); + end + + registerSCRAMMechanism("SHA-1", sha1, hmac_sha1); end return _M; \ No newline at end of file diff -r 6b2e4b581717 -r 4164023a2258 util/xmppstream.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util/xmppstream.lua Thu May 06 17:14:11 2010 +0500 @@ -0,0 +1,150 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + + +local lxp = require "lxp"; +local st = require "util.stanza"; + +local tostring = tostring; +local t_insert = table.insert; +local t_concat = table.concat; + +local default_log = require "util.logger".init("xmlhandlers"); + +local error = error; + +module "xmppstream" + +local new_parser = lxp.new; + +local ns_prefixes = { + ["http://www.w3.org/XML/1998/namespace"] = "xml"; +}; + +local xmlns_streams = "http://etherx.jabber.org/streams"; + +local ns_separator = "\1"; +local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$"; + +function new_sax_handlers(session, stream_callbacks) + local chardata = {}; + local xml_handlers = {}; + local log = session.log or default_log; + + local cb_streamopened = stream_callbacks.streamopened; + local cb_streamclosed = stream_callbacks.streamclosed; + local cb_error = stream_callbacks.error or function(session, e) error("XML stream error: "..tostring(e)); end; + local cb_handlestanza = stream_callbacks.handlestanza; + + local stream_ns = stream_callbacks.stream_ns or xmlns_streams; + local stream_tag = stream_ns..ns_separator..(stream_callbacks.stream_tag or "stream"); + local stream_error_tag = stream_ns..ns_separator..(stream_callbacks.error_tag or "error"); + + local stream_default_ns = stream_callbacks.default_ns; + + local stanza; + function xml_handlers:StartElement(tagname, attr) + if stanza and #chardata > 0 then + -- We have some character data in the buffer + stanza:text(t_concat(chardata)); + chardata = {}; + end + local curr_ns,name = tagname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end + + if curr_ns ~= stream_default_ns then + attr.xmlns = curr_ns; + end + + -- FIXME !!!!! + for i=1,#attr do + local k = attr[i]; + attr[i] = nil; + local ns, nm = k:match(ns_pattern); + if nm ~= "" then + ns = ns_prefixes[ns]; + if ns then + attr[ns..":"..nm] = attr[k]; + attr[k] = nil; + end + end + end + + if not stanza then --if we are not currently inside a stanza + if session.notopen then + if tagname == stream_tag then + if cb_streamopened then + cb_streamopened(session, attr); + end + else + -- Garbage before stream? + cb_error(session, "no-stream"); + end + return; + end + if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then + cb_error(session, "invalid-top-level-element"); + end + + stanza = st.stanza(name, attr); + else -- we are inside a stanza, so add a tag + attr.xmlns = nil; + if curr_ns ~= stream_default_ns then + attr.xmlns = curr_ns; + end + stanza:tag(name, attr); + end + end + function xml_handlers:CharacterData(data) + if stanza then + t_insert(chardata, data); + end + end + function xml_handlers:EndElement(tagname) + if stanza then + if #chardata > 0 then + -- We have some character data in the buffer + stanza:text(t_concat(chardata)); + chardata = {}; + end + -- Complete stanza + if #stanza.last_add == 0 then + if tagname ~= stream_error_tag then + cb_handlestanza(session, stanza); + else + cb_error(session, "stream-error", stanza); + end + stanza = nil; + else + stanza:up(); + end + else + if tagname == stream_tag then + if cb_streamclosed then + cb_streamclosed(session); + end + else + local curr_ns,name = tagname:match(ns_pattern); + if name == "" then + curr_ns, name = "", curr_ns; + end + cb_error(session, "parse-error", "unexpected-element-close", name); + end + stanza, chardata = nil, {}; + end + end + return xml_handlers; +end + +function new(session, stream_callbacks) + return new_parser(new_sax_handlers(session, stream_callbacks), ns_separator); +end + +return _M;