diff options
-rw-r--r-- | core/modulemanager.lua | 79 | ||||
-rw-r--r-- | core/servermanager.lua | 8 | ||||
-rw-r--r-- | core/sessionmanager.lua | 20 | ||||
-rw-r--r-- | core/stanza_router.lua | 292 | ||||
-rw-r--r-- | main.lua | 15 | ||||
-rw-r--r-- | plugins/mod_legacyauth.lua | 46 | ||||
-rw-r--r-- | plugins/mod_roster.lua | 24 | ||||
-rw-r--r-- | util/logger.lua | 23 | ||||
-rw-r--r-- | util/stanza.lua | 1 |
9 files changed, 499 insertions, 9 deletions
diff --git a/core/modulemanager.lua b/core/modulemanager.lua new file mode 100644 index 00000000..ed70b75b --- /dev/null +++ b/core/modulemanager.lua @@ -0,0 +1,79 @@ + +local log = require "util.logger".init("modulemanager") + +local loadfile, pcall = loadfile, pcall; +local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv; +local pairs, ipairs = pairs, ipairs; +local type = type; + +local tostring, print = tostring, print; + +local _G = _G; + +module "modulemanager" + +local handler_info = {}; +local handlers = {}; + +local modulehelpers = setmetatable({}, { __index = _G }); + +function modulehelpers.add_iq_handler(origin_type, xmlns, handler) + handlers[origin_type] = handlers[origin_type] or {}; + handlers[origin_type].iq = handlers[origin_type].iq or {}; + if not handlers[origin_type].iq[xmlns] then + handlers[origin_type].iq[xmlns]= handler; + handler_info[handler] = getfenv(2).module; + log("debug", "mod_%s now handles iq,%s", getfenv(2).module.name, xmlns); + else + log("warning", "mod_%s wants to handle iq,%s but mod_%s already handles that", getfenv(2).module.name, xmlns, handler_info[handlers[origin_type].iq[xmlns]].module.name); + end +end + +function modulehelpers.add_presence_handler(origin_type, handler) +end + +function modulehelpers.add_message_handler(origin_type, handler) +end + +function loadall() + load("legacyauth"); + load("roster"); +end + +function load(name) + local mod, err = loadfile("plugins/mod_"..name..".lua"); + if not mod then + log("error", "Unable to load module '%s': %s", name or "nil", err or "nil"); + return; + end + + local pluginenv = setmetatable({ module = { name = name } }, { __index = modulehelpers }); + + setfenv(mod, pluginenv); + local success, ret = pcall(mod); + if not success then + log("error", "Error initialising module '%s': %s", name or "nil", ret or "nil"); + return; + end +end + +function handle_stanza(origin, stanza) + local name, origin_type = stanza.name, origin.type; + + if name == "iq" then + log("debug", "Stanza is an <iq/>"); + local child = stanza.tags[1]; + if child then + local xmlns = child.attr.xmlns; + log("debug", "Stanza has xmlns: %s", xmlns); + local handler = handlers[origin_type][name][xmlns]; + if handler then + log("debug", "Passing stanza to mod_%s", handler_info[handler].name); + return handler(origin, stanza) or true; + end + + end + end + log("debug", "Stanza unhandled by any modules"); + return false; -- we didn't handle it +end diff --git a/core/servermanager.lua b/core/servermanager.lua new file mode 100644 index 00000000..02ccab53 --- /dev/null +++ b/core/servermanager.lua @@ -0,0 +1,8 @@ + +require "modulemanager" + +-- Handle stanzas that were addressed to the server (whether they came from c2s, s2s, etc.) +function handle_stanza(origin, stanza) + -- Use plugins + return modulemanager.handle_stanza(origin, stanza); +end diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua new file mode 100644 index 00000000..47f47ba9 --- /dev/null +++ b/core/sessionmanager.lua @@ -0,0 +1,20 @@ + +local tostring = tostring; + +local log = require "util.logger".init("sessionmanager"); + +module "sessionmanager" + +function new_session(conn) + local session = { conn = conn, notopen = true, priority = 0, type = "c2s_unauthed" }; + local w = conn.write; + session.send = function (t) w(tostring(t)); end + return session; +end + +function send_to_session(session, data) + log("debug", "Sending...", tostring(data)); + session.conn.write(tostring(data)); +end + +return _M;
\ No newline at end of file diff --git a/core/stanza_router.lua b/core/stanza_router.lua new file mode 100644 index 00000000..0ca534ce --- /dev/null +++ b/core/stanza_router.lua @@ -0,0 +1,292 @@ + +-- The code in this file should be self-explanatory, though the logic is horrible +-- for more info on that, see doc/stanza_routing.txt, which attempts to condense +-- the rules from the RFCs (mainly 3921) + +require "core.servermanager" + +local log = require "util.logger".init("stanzarouter") + +require "util.jid" +local jid_split = jid.split; + +function core_process_stanza(origin, stanza) + local to = stanza.attr.to; + + if not to or (hosts[to] and hosts[to].type == "local") then + core_handle_stanza(origin, stanza); + elseif origin.type == "c2s" then + core_route_stanza(origin, stanza); + end + +end + +function core_handle_stanza(origin, stanza) + -- Handlers + if origin.type == "c2s" or origin.type == "c2s_unauthed" then + local session = origin; + stanza.attr.from = session.full_jid; + + log("debug", "Routing stanza"); + -- Stanza has no to attribute + --local to_node, to_host, to_resource = jid_split(stanza.attr.to); + --if not to_host then error("Invalid destination JID: "..string.format("{ %q, %q, %q } == %q", to_node or "", to_host or "", to_resource or "", stanza.attr.to or "nil")); end + + -- Stanza is to this server, or a user on this server + log("debug", "Routing stanza to local"); + print(type(origin), type(stanza)); + handle_stanza(session, stanza); + end +end + +function core_route_stanza(origin, stanza) + -- Hooks + -- Deliver +end + +function handle_stanza_nodest(stanza) + if stanza.name == "iq" then + handle_stanza_iq_no_to(session, stanza); + elseif stanza.name == "presence" then + -- Broadcast to this user's contacts + handle_stanza_presence_broadcast(session, stanza); + -- also, if it is initial presence, send out presence probes + if not session.last_presence then + handle_stanza_presence_probe_broadcast(session, stanza); + end + session.last_presence = stanza; + elseif stanza.name == "message" then + -- Treat as if message was sent to bare JID of the sender + handle_stanza_to_local_user(stanza); + end +end + +function handle_stanza_tolocal(stanza) + local node, host, resource = jid.split(stanza.attr.to); + if host and hosts[host] and hosts[host].type == "local" then + -- Is a local host, handle internally + if node then + -- Is a local user, send to their session + log("debug", "Routing stanza to %s@%s", node, host); + if not session.username then return; end --FIXME: Correct response when trying to use unauthed stream is what? + handle_stanza_to_local_user(stanza); + else + -- Is sent to this server, let's handle it... + log("debug", "Routing stanza to %s", host); + handle_stanza_to_server(stanza, session); + end + end +end + +function handle_stanza_toremote(stanza) + log("error", "Stanza bound for remote host, but s2s is not implemented"); +end + + +--[[ +local function route_c2s_stanza(session, stanza) + stanza.attr.from = session.full_jid; + if not stanza.attr.to and session.username then + -- Has no 'to' attribute, handle internally + if stanza.name == "iq" then + handle_stanza_iq_no_to(session, stanza); + elseif stanza.name == "presence" then + -- Broadcast to this user's contacts + handle_stanza_presence_broadcast(session, stanza); + -- also, if it is initial presence, send out presence probes + if not session.last_presence then + handle_stanza_presence_probe_broadcast(session, stanza); + end + session.last_presence = stanza; + elseif stanza.name == "message" then + -- Treat as if message was sent to bare JID of the sender + handle_stanza_to_local_user(stanza); + end + end + local node, host, resource = jid.split(stanza.attr.to); + if host and hosts[host] and hosts[host].type == "local" then + -- Is a local host, handle internally + if node then + -- Is a local user, send to their session + if not session.username then return; end --FIXME: Correct response when trying to use unauthed stream is what? + handle_stanza_to_local_user(stanza); + else + -- Is sent to this server, let's handle it... + handle_stanza_to_server(stanza, session); + end + else + -- Is not for us or a local user, route accordingly + route_s2s_stanza(stanza); + end +end + +function handle_stanza_no_to(session, stanza) + if not stanza.attr.id then log("warn", "<iq> without id attribute is invalid"); end + local xmlns = (stanza.tags[1].attr and stanza.tags[1].attr.xmlns); + if stanza.attr.type == "get" or stanza.attr.type == "set" then + if iq_handlers[xmlns] then + if iq_handlers[xmlns](stanza) then return; end; -- If handler returns true, it handled it + end + -- Oh, handler didn't handle it. Need to send service-unavailable now. + log("warn", "Unhandled namespace: "..xmlns); + session:send(format("<iq type='error' id='%s'><error type='cancel'><service-unavailable/></error></iq>", stanza.attr.id)); + return; -- All done! + end +end + +function handle_stanza_to_local_user(stanza) + if stanza.name == "message" then + handle_stanza_message_to_local_user(stanza); + elseif stanza.name == "presence" then + handle_stanza_presence_to_local_user(stanza); + elseif stanza.name == "iq" then + handle_stanza_iq_to_local_user(stanza); + end +end + +function handle_stanza_message_to_local_user(stanza) + local node, host, resource = stanza.to.node, stanza.to.host, stanza.to.resource; + local destuser = hosts[host].sessions[node]; + if destuser then + if resource and destuser[resource] then + destuser[resource]:send(stanza); + else + -- Bare JID, or resource offline + local best_session; + for resource, session in pairs(destuser.sessions) do + if not best_session then best_session = session; + elseif session.priority >= best_session.priority and session.priority >= 0 then + best_session = session; + end + end + if not best_session then + offlinemessage.new(node, host, stanza); + else + print("resource '"..resource.."' was not online, have chosen to send to '"..best_session.username.."@"..best_session.host.."/"..best_session.resource.."'"); + destuser[best_session]:send(stanza); + end + end + else + -- User is offline + offlinemessage.new(node, host, stanza); + end +end + +function handle_stanza_presence_to_local_user(stanza) + local node, host, resource = stanza.to.node, stanza.to.host, stanza.to.resource; + local destuser = hosts[host].sessions[node]; + if destuser then + if resource then + if destuser[resource] then + destuser[resource]:send(stanza); + else + return; + end + else + -- Broadcast to all user's resources + for resource, session in pairs(destuser.sessions) do + session:send(stanza); + end + end + end +end + +function handle_stanza_iq_to_local_user(stanza) + +end + +function foo() + local node, host, resource = stanza.to.node, stanza.to.host, stanza.to.resource; + local destuser = hosts[host].sessions[node]; + if destuser and destuser.sessions then + -- User online + if resource and destuser.sessions[resource] then + stanza.to:send(stanza); + else + --User is online, but specified resource isn't (or no resource specified) + local best_session; + for resource, session in pairs(destuser.sessions) do + if not best_session then best_session = session; + elseif session.priority >= best_session.priority and session.priority >= 0 then + best_session = session; + end + end + if not best_session then + offlinemessage.new(node, host, stanza); + else + print("resource '"..resource.."' was not online, have chosen to send to '"..best_session.username.."@"..best_session.host.."/"..best_session.resource.."'"); + resource = best_session.resource; + end + end + if destuser.sessions[resource] == session then + log("warn", "core", "Attempt to send stanza to self, dropping..."); + else + print("...sending...", tostring(stanza)); + --destuser.sessions[resource].conn.write(tostring(data)); + print(" to conn ", destuser.sessions[resource].conn); + destuser.sessions[resource].conn.write(tostring(stanza)); + print("...sent") + end + elseif stanza.name == "message" then + print(" ...will be stored offline"); + offlinemessage.new(node, host, stanza); + elseif stanza.name == "iq" then + print(" ...is an iq"); + stanza.from:send(st.reply(stanza) + :tag("error", { type = "cancel" }) + :tag("service-unavailable", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" })); + end +end + +-- Broadcast a presence stanza to all of a user's contacts +function handle_stanza_presence_broadcast(session, stanza) + if session.roster then + local initial_presence = not session.last_presence; + session.last_presence = stanza; + + -- Broadcast presence and probes + local broadcast = st.presence({ from = session.full_jid, type = stanza.attr.type }); + + for child in stanza:childtags() do + broadcast:add_child(child); + end + for contact_jid in pairs(session.roster) do + broadcast.attr.to = contact_jid; + send_to(contact_jid, broadcast); + if initial_presence then + local node, host = jid.split(contact_jid); + if hosts[host] and hosts[host].type == "local" then + local contact = hosts[host].sessions[node] + if contact then + local pres = st.presence { to = session.full_jid }; + for resource, contact_session in pairs(contact.sessions) do + if contact_session.last_presence then + pres.tags = contact_session.last_presence.tags; + pres.attr.from = contact_session.full_jid; + send(pres); + end + end + end + --FIXME: Do we send unavailable if they are offline? + else + probe.attr.to = contact; + send_to(contact, probe); + end + end + end + + -- Probe for our contacts' presence + end +end + +-- Broadcast presence probes to all of a user's contacts +function handle_stanza_presence_probe_broadcast(session, stanza) +end + +-- +function handle_stanza_to_server(stanza) +end + +function handle_stanza_iq_no_to(session, stanza) +end +]]
\ No newline at end of file @@ -13,7 +13,10 @@ require "core.stanza_dispatch" require "core.xmlhandlers" require "core.rostermanager" require "core.offlinemessage" +require "core.modulemanager" require "core.usermanager" +require "core.sessionmanager" +require "core.stanza_router" require "util.stanza" require "util.jid" @@ -113,7 +116,7 @@ function handler(conn, data, err) local session = sessions[conn]; if not session then - sessions[conn] = { conn = conn, notopen = true, priority = 0 }; + sessions[conn] = sessionmanager.new_session(conn); session = sessions[conn]; -- Logging functions -- @@ -127,16 +130,9 @@ function handler(conn, data, err) local print = function (...) log("info", "core", t_concatall({...}, "\t")); end session.log = log; - -- -- -- - - -- Send buffers -- - - local send = function (data) print("Sending...", tostring(data)); conn.write(tostring(data)); end; - session.send, session.send_to = send, send_to; - print("Client connected"); - session.stanza_dispatch = init_stanza_dispatcher(session); + session.stanza_dispatch = function (stanza) return core_process_stanza(session, stanza); end session.xml_handlers = init_xmlhandlers(session); session.parser = lxp.new(session.xml_handlers, ":"); @@ -168,6 +164,7 @@ end setmetatable(_G, { __index = function (t, k) print("WARNING: ATTEMPT TO READ A NIL GLOBAL!!!", k); error("Attempt to read a non-existent global. Naughty boy.", 2); end, __newindex = function (t, k, v) print("ATTEMPT TO SET A GLOBAL!!!!", tostring(k).." = "..tostring(v)); error("Attempt to set a global. Naughty boy.", 2); end }) --]][][[]][]; +modulemanager.loadall(); local protected_handler = function (conn, data, err) local success, ret = pcall(handler, conn, data, err); if not success then print("ERROR on "..tostring(conn)..": "..ret); conn:close(); end end; local protected_disconnect = function (conn, err) local success, ret = pcall(disconnect, conn, err); if not success then print("ERROR on "..tostring(conn).." disconnect: "..ret); conn:close(); end end; diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua new file mode 100644 index 00000000..276842b1 --- /dev/null +++ b/plugins/mod_legacyauth.lua @@ -0,0 +1,46 @@ + +local st = require "util.stanza"; +local send = require "core.sessionmanager".send_to_session; +local t_concat = table.concat; + +add_iq_handler("c2s_unauthed", "jabber:iq:auth", + function (session, stanza) + local username = stanza.tags[1]:child_with_name("username"); + local password = stanza.tags[1]:child_with_name("password"); + local resource = stanza.tags[1]:child_with_name("resource"); + if not (username and password and resource) then + local reply = st.reply(stanza); + send(session, reply:query("jabber:iq:auth") + :tag("username"):up() + :tag("password"):up() + :tag("resource"):up()); + return true; + else + username, password, resource = t_concat(username), t_concat(password), t_concat(resource); + local reply = st.reply(stanza); + require "core.usermanager" + if usermanager.validate_credentials(session.host, username, password) then + -- Authentication successful! + session.username = username; + session.resource = resource; + session.full_jid = username.."@"..session.host.."/"..session.resource; + if session.type == "c2s_unauthed" then + session.type = "c2s"; + end + if not hosts[session.host].sessions[username] then + hosts[session.host].sessions[username] = { sessions = {} }; + end + hosts[session.host].sessions[username].sessions[resource] = session; + send(session, st.reply(stanza)); + return true; + else + local reply = st.reply(stanza); + reply.attr.type = "error"; + reply:tag("error", { code = "401", type = "auth" }) + :tag("not-authorized", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" }); + dispatch_stanza(reply); + return true; + end + end + + end);
\ No newline at end of file diff --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua new file mode 100644 index 00000000..8b4a3a0a --- /dev/null +++ b/plugins/mod_roster.lua @@ -0,0 +1,24 @@ + +local st = require "util.stanza" +local send = require "core.sessionmanager".send_to_session + +add_iq_handler("c2s", "jabber:iq:roster", + function (session, stanza) + if stanza.attr.type == "get" then + session.roster = session.roster or rostermanager.getroster(session.username, session.host); + if session.roster == false then + send(session, st.reply(stanza) + :tag("error", { type = "wait" }) + :tag("internal-server-error", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"})); + return true; + else session.roster = session.roster or {}; + end + local roster = st.reply(stanza) + :query("jabber:iq:roster"); + for jid in pairs(session.roster) do + roster:tag("item", { jid = jid, subscription = "none" }):up(); + end + send(session, roster); + return true; + end + end);
\ No newline at end of file diff --git a/util/logger.lua b/util/logger.lua new file mode 100644 index 00000000..3d672e94 --- /dev/null +++ b/util/logger.lua @@ -0,0 +1,23 @@ + +local format = string.format; +local print = print; +local debug = debug; +local tostring = tostring; +module "logger" + +function init(name) + name = nil; -- While this line is not commented, will automatically fill in file/line number info + return function (level, message, ...) + if not name then + local inf = debug.getinfo(2, 'Snl'); + level = level .. ","..tostring(inf.short_src):match("[^/]*$")..":"..inf.currentline; + end + if ... then + print(level, format(message, ...)); + else + print(level, message); + end + end +end + +return _M;
\ No newline at end of file diff --git a/util/stanza.lua b/util/stanza.lua index 3a17fc20..553bba4d 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -136,3 +136,4 @@ function presence(attr) return stanza("presence", attr); end +return _M;
\ No newline at end of file |