SASL!

Thu, 02 Oct 2008 01:08:58 +0100

author
Matthew Wild <mwild1@gmail.com>
date
Thu, 02 Oct 2008 01:08:58 +0100
changeset 38
3fdfd6e0cb4e
parent 37
06eadafafefa
child 39
89877d61ac51
child 51
5253c891a360

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 <stream:features> out of xmlhandlers.lua o_O ))

core/modulemanager.lua file | annotate | diff | comparison | revisions
core/sessionmanager.lua file | annotate | diff | comparison | revisions
core/usermanager.lua file | annotate | diff | comparison | revisions
core/xmlhandlers.lua file | annotate | diff | comparison | revisions
main.lua file | annotate | diff | comparison | revisions
net/connhandlers.lua file | annotate | diff | comparison | revisions
plugins/mod_legacyauth.lua file | annotate | diff | comparison | revisions
plugins/mod_saslauth.lua file | annotate | diff | comparison | revisions
util/sasl.lua file | annotate | diff | comparison | revisions
--- a/core/modulemanager.lua	Thu Oct 02 00:00:35 2008 +0100
+++ b/core/modulemanager.lua	Thu Oct 02 01:08:58 2008 +0100
@@ -23,19 +23,25 @@
 	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 @@
 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 <iq/>");
 		local child = stanza.tags[1];
 		if child then
@@ -73,6 +79,13 @@
 			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
--- a/core/sessionmanager.lua	Thu Oct 02 00:00:35 2008 +0100
+++ b/core/sessionmanager.lua	Thu Oct 02 01:08:58 2008 +0100
@@ -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 @@
 	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
--- a/core/usermanager.lua	Thu Oct 02 00:00:35 2008 +0100
+++ b/core/usermanager.lua	Thu Oct 02 01:08:58 2008 +0100
@@ -9,3 +9,5 @@
 	if password == credentials.password then return true; end
 	return false;
 end
+
+return _M;
\ No newline at end of file
--- a/core/xmlhandlers.lua	Thu Oct 02 00:00:35 2008 +0100
+++ b/core/xmlhandlers.lua	Thu Oct 02 01:08:58 2008 +0100
@@ -27,6 +27,7 @@
 		
 		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 @@
 			                        session.streamid = m_random(1000000, 99999999);
 			                        print(session, session.host, "Client opened stream");
 			                        send("<?xml version='1.0'?>");
-			                        send(format("<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='%s' from='%s'>", session.streamid, session.host));
-			                        --send("<stream:features>");
-			                        --send("<mechanism>PLAIN</mechanism>");
+			                        send(format("<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='%s' from='%s' version='1.0'>", session.streamid, session.host));
+						send("<stream:features>");
+						if not session.username then
+							send("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>");
+								send("<mechanism>PLAIN</mechanism>");
+							send("</mechanisms>");
+						else
+							send("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><required/></bind>");
+						end
         			                --send [[<register xmlns="http://jabber.org/features/iq-register"/> ]]
-        			                --send("</stream:features>");
+        			                send("</stream:features>");
 						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;
--- a/main.lua	Thu Oct 02 00:00:35 2008 +0100
+++ b/main.lua	Thu Oct 02 01:08:58 2008 +0100
@@ -21,6 +21,7 @@
 require "core.usermanager"
 require "core.sessionmanager"
 require "core.stanza_router"
+require "net.connhandlers"
 require "util.stanza"
 require "util.jid"
  
@@ -31,7 +32,6 @@
 local m_random = math.random;
 local format = string.format;
 local st = stanza;
-local init_xmlhandlers = xmlhandlers.init_xmlhandlers;
 ------------------------------
 
 
@@ -63,8 +63,8 @@
 		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 @@
 		end
 	end
 	if data then
-		session.parser:parse(data);
+		session.connhandler:data(data);
 	end
 	
 	--log("info", "core", "Client disconnected, connection closed");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/connhandlers.lua	Thu Oct 02 01:08:58 2008 +0100
@@ -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
--- a/plugins/mod_legacyauth.lua	Thu Oct 02 00:00:35 2008 +0100
+++ b/plugins/mod_legacyauth.lua	Thu Oct 02 01:08:58 2008 +0100
@@ -21,16 +21,27 @@
 				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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_saslauth.lua	Thu Oct 02 01:08:58 2008 +0100
@@ -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
--- a/util/sasl.lua	Thu Oct 02 00:00:35 2008 +0100
+++ b/util/sasl.lua	Thu Oct 02 01:08:58 2008 +0100
@@ -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

mercurial