From 6db3d039b3d8d55c9e03ebdc776cf1a23dd826c2 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 2 Oct 2008 01:08:58 +0100 Subject: SASL! (but before you get too excited, no resource binding yet. And yes, there are still plenty of rough edges to the code...) ((eg. must move out of xmlhandlers.lua o_O )) --- core/modulemanager.lua | 29 ++++++++++++++++++------- core/sessionmanager.lua | 39 +++++++++++++++++++++++++++++++++- core/usermanager.lua | 2 ++ core/xmlhandlers.lua | 20 +++++++++++------ main.lua | 8 +++---- net/connhandlers.lua | 16 ++++++++++++++ plugins/mod_legacyauth.lua | 29 +++++++++++++++++-------- plugins/mod_saslauth.lua | 53 ++++++++++++++++++++++++++++++++++++++++++++++ util/sasl.lua | 43 ++++++++++++++++++++++--------------- 9 files changed, 194 insertions(+), 45 deletions(-) create mode 100644 net/connhandlers.lua create mode 100644 plugins/mod_saslauth.lua diff --git a/core/modulemanager.lua b/core/modulemanager.lua index ed70b75b..ad92b41b 100644 --- a/core/modulemanager.lua +++ b/core/modulemanager.lua @@ -23,19 +23,25 @@ function modulehelpers.add_iq_handler(origin_type, xmlns, handler) 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); + log("debug", "mod_%s now handles tag 'iq' with query namespace '%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); + log("warning", "mod_%s wants to handle tag 'iq' with query namespace '%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) +function modulehelpers.add_handler(origin_type, tag, handler) + handlers[origin_type] = handlers[origin_type] or {}; + if not handlers[origin_type][tag] then + handlers[origin_type][tag]= handler; + handler_info[handler] = getfenv(2).module; + log("debug", "mod_%s now handles tag '%s'", getfenv(2).module.name, tag); + elseif handler_info[handlers[origin_type][tag]] then + log("warning", "mod_%s wants to handle tag '%s' but mod_%s already handles that", getfenv(2).module.name, tag, handler_info[handlers[origin_type][tag]].module.name); + end end function loadall() + load("saslauth"); load("legacyauth"); load("roster"); end @@ -58,9 +64,9 @@ function load(name) end function handle_stanza(origin, stanza) - local name, origin_type = stanza.name, origin.type; + local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns, origin.type; - if name == "iq" then + if name == "iq" and xmlns == "jabber:client" and handlers[origin_type] then log("debug", "Stanza is an "); local child = stanza.tags[1]; if child then @@ -73,6 +79,13 @@ function handle_stanza(origin, stanza) end end + --FIXME: All iq's must be replied to, here we should return service-unavailable I think + elseif handlers[origin_type] then + local handler = handlers[origin_type][name]; + if handler then + log("debug", "Passing stanza to mod_%s", handler_info[handler].name); + return handler(origin, stanza) or true; + end end log("debug", "Stanza unhandled by any modules"); return false; -- we didn't handle it diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 47f47ba9..e89de262 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -1,6 +1,10 @@ local tostring = tostring; +local print = print; + +local hosts = hosts; + local log = require "util.logger".init("sessionmanager"); module "sessionmanager" @@ -12,9 +16,42 @@ function new_session(conn) return session; end +function destroy_session(session) +end + function send_to_session(session, data) - log("debug", "Sending...", tostring(data)); + log("debug", "Sending: %s", tostring(data)); session.conn.write(tostring(data)); end +function make_authenticated(session, username) + session.username = username; + session.resource = resource; + if session.type == "c2s_unauthed" then + session.type = "c2s"; + end +end + +function bind_resource(session, resource) + if not session.username then return false, "auth"; end + if session.resource then return false, "constraint"; end -- We don't support binding multiple resources + resource = resource or math.random(100000, 99999999); -- FIXME: Clearly we have issues :) + --FIXME: Randomly-generated resources must be unique per-user, and never conflict with existing + + if not hosts[session.host].sessions[session.username] then + hosts[session.host].sessions[session.username] = { sessions = {} }; + else + if hosts[session.host].sessions[session.username].sessions[resource] then + -- Resource conflict + return false, "conflict"; + end + end + + session.resource = resource; + session.full_jid = session.username .. '@' .. session.host .. '/' .. resource; + hosts[session.host].sessions[session.username].sessions[resource] = session; + + return true; +end + return _M; \ No newline at end of file diff --git a/core/usermanager.lua b/core/usermanager.lua index c98a1918..a67ad368 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -9,3 +9,5 @@ function validate_credentials(host, username, password) if password == credentials.password then return true; end return false; end + +return _M; \ No newline at end of file diff --git a/core/xmlhandlers.lua b/core/xmlhandlers.lua index 96e3f3ac..b6050c5a 100644 --- a/core/xmlhandlers.lua +++ b/core/xmlhandlers.lua @@ -27,6 +27,7 @@ function init_xmlhandlers(session) local stanza function xml_handlers:StartElement(name, attr) + log("info", "xmlhandlers", "Start element: " .. name); if stanza and #chardata > 0 then -- We have some character data in the buffer stanza:text(t_concat(chardata)); @@ -41,21 +42,28 @@ function init_xmlhandlers(session) session.streamid = m_random(1000000, 99999999); print(session, session.host, "Client opened stream"); send(""); - send(format("", session.streamid, session.host)); - --send(""); - --send("PLAIN"); + send(format("", session.streamid, session.host)); + send(""); + if not session.username then + send(""); + send("PLAIN"); + send(""); + else + send(""); + end --send [[ ]] - --send(""); + send(""); log("info", "core", "Stream opened successfully"); session.notopen = nil; return; end error("Client failed to open stream successfully"); end - if name ~= "iq" and name ~= "presence" and name ~= "message" then + if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then error("Client sent invalid top-level stanza"); end - stanza = st.stanza(name, { to = attr.to, type = attr.type, id = attr.id, xmlns = curr_ns }); + attr.xmlns = curr_ns; + stanza = st.stanza(name, attr); --{ to = attr.to, type = attr.type, id = attr.id, xmlns = curr_ns }); curr_tag = stanza; else attr.xmlns = curr_ns; diff --git a/main.lua b/main.lua index b8a15adf..6712694d 100644 --- a/main.lua +++ b/main.lua @@ -21,6 +21,7 @@ require "core.modulemanager" require "core.usermanager" require "core.sessionmanager" require "core.stanza_router" +require "net.connhandlers" require "util.stanza" require "util.jid" @@ -31,7 +32,6 @@ local t_concatall = function (t, sep) local tt = {}; for _, s in ipairs(t) do t_ local m_random = math.random; local format = string.format; local st = stanza; -local init_xmlhandlers = xmlhandlers.init_xmlhandlers; ------------------------------ @@ -63,8 +63,8 @@ function handler(conn, data, err) print("Client connected"); 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, ":"); + + session.connhandler = connhandlers.new("xmpp-client", session); function session.disconnect(err) if session.last_presence and session.last_presence.attr.type ~= "unavailable" then @@ -82,7 +82,7 @@ function handler(conn, data, err) end end if data then - session.parser:parse(data); + session.connhandler:data(data); end --log("info", "core", "Client disconnected, connection closed"); diff --git a/net/connhandlers.lua b/net/connhandlers.lua new file mode 100644 index 00000000..493f1946 --- /dev/null +++ b/net/connhandlers.lua @@ -0,0 +1,16 @@ + +local lxp = require "lxp" +local init_xmlhandlers = require "core.xmlhandlers" + +module "connhandlers" + + +function new(name, session) + if name == "xmpp-client" then + local parser = lxp.new(init_xmlhandlers(session), ":"); + local parse = parser.parse; + return { data = function (self, data) return parse(parser, data); end, parser = parser } + end +end + +return _M; \ No newline at end of file diff --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index 276842b1..7a205e5b 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -21,16 +21,27 @@ add_iq_handler("c2s_unauthed", "jabber:iq:auth", 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"; + local success, err = sessionmanager.make_authenticated(session, username); + if success then + success, err = sessionmanager.bind_resource(session, resource); + --FIXME: Reply with error + if not success then + local reply = st.reply(stanza); + reply.attr.type = "error"; + if err == "conflict" then + reply:tag("error", { code = "409", type = "cancel" }) + :tag("conflict", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" }); + elseif err == "constraint" then + reply:tag("error", { code = "409", type = "cancel" }) + :tag("already-bound", { xmlns = "x-lxmppd:extensions:legacyauth" }); + elseif err == "auth" then + reply:tag("error", { code = "401", type = "auth" }) + :tag("not-authorized", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" }); + end + dispatch_stanza(reply); + return true; + end 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 diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua new file mode 100644 index 00000000..dc6f3645 --- /dev/null +++ b/plugins/mod_saslauth.lua @@ -0,0 +1,53 @@ + +local st = require "util.stanza"; +local send = require "core.sessionmanager".send_to_session; + +local usermanager_validate_credentials = require "core.usermanager".validate_credentials; +local t_concat = table.concat; +local tostring = tostring; + +local log = require "util.logger".init("mod_saslauth"); + +local xmlns_sasl ='urn:ietf:params:xml:ns:xmpp-sasl'; + +local new_connhandler = require "net.connhandlers".new; +local new_sasl = require "util.sasl".new; + +add_handler("c2s_unauthed", "auth", + function (session, stanza) + if not session.sasl_handler then + session.sasl_handler = new_sasl(stanza.attr.mechanism, + function (username, password) + -- onAuth + require "core.usermanager" + if usermanager_validate_credentials(session.host, username, password) then + return true; + end + return false; + end, + function (username) + -- onSuccess + local success, err = sessionmanager.make_authenticated(session, username); + if not success then + sessionmanager.destroy_session(session); + end + session.sasl_handler = nil; + session.connhandler = new_connhandler("xmpp-client", session); + session.notopen = true; + end, + function (reason) + -- onFail + log("debug", "SASL failure, reason: %s", reason); + end, + function (stanza) + -- onWrite + log("debug", "SASL writes: %s", tostring(stanza)); + send(session, stanza); + end + ); + session.sasl_handler:feed(stanza); + else + error("Client tried to negotiate SASL again", 0); + end + + end); \ No newline at end of file diff --git a/util/sasl.lua b/util/sasl.lua index 0d7740c8..515bcf8a 100644 --- a/util/sasl.lua +++ b/util/sasl.lua @@ -1,34 +1,43 @@ -require "base64" -sasl = {} -function sasl:new_plain(onAuth, onSuccess, onFail, onWrite) +local base64 = require "base64" +local log = require "util.logger".init("sasl"); +local tostring = tostring; +local st = require "util.stanza"; +local s_match = string.match; +module "sasl" + + +local function new_plain(onAuth, onSuccess, onFail, onWrite) local object = { mechanism = "PLAIN", onAuth = onAuth, onSuccess = onSuccess, onFail = onFail, onWrite = onWrite} - local challenge = base64.encode(""); - onWrite(stanza.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(challenge)) + --local challenge = base64.encode(""); + --onWrite(st.stanza("challenge", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):text(challenge)) object.feed = function(self, stanza) - if (stanza.name ~= "response") then self.onFail() end - if (stanza.attr.xmlns ~= "urn:ietf:params:xml:ns:xmpp-sasl") then self.onFail() end - local response = base64.decode(stanza.tag[1]) - local authorization = string.match(response, "([^&\0]+)") - local authentication = string.match(response, "\0([^&\0]+)\0") - local password = string.match(response, "\0[^&\0]+\0([^&\0]+)") + if stanza.name ~= "response" and stanza.name ~= "auth" then self.onFail("invalid-stanza-tag") end + if stanza.attr.xmlns ~= "urn:ietf:params:xml:ns:xmpp-sasl" then self.onFail("invalid-stanza-namespace") end + local response = base64.decode(stanza[1]) + local authorization = s_match(response, "([^&%z]+)") + local authentication = s_match(response, "%z([^&%z]+)%z") + local password = s_match(response, "%z[^&%z]+%z([^&%z]+)") if self.onAuth(authorization, password) == true then - self.onWrite(stanza.stanza("success", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"})) - self.onSuccess() + self.onWrite(st.stanza("success", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"})) + self.onSuccess(authentication) else - self.onWrite(stanza.stanza("failure", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):tag("temporary-auth-failure")); + self.onWrite(st.stanza("failure", {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"}):tag("temporary-auth-failure")); end end return object end -function sasl:new(mechanism, onAuth, onSuccess, onFail, onWrite) + +function new(mechanism, onAuth, onSuccess, onFail, onWrite) local object if mechanism == "PLAIN" then object = new_plain(onAuth, onSuccess, onFail, onWrite) - else onFail() + else + log("debug", "Unsupported SASL mechanism: "..tostring(mechanism)); + onFail("unsupported-mechanism") end return object end -module "sasl" +return _M; \ No newline at end of file -- cgit v1.2.3