Changeset

38:3fdfd6e0cb4e

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 ))
author Matthew Wild <mwild1@gmail.com>
date Thu, 02 Oct 2008 01:08:58 +0100
parents 37:06eadafafefa
children 39:89877d61ac51 51:5253c891a360
files core/modulemanager.lua core/sessionmanager.lua core/usermanager.lua core/xmlhandlers.lua main.lua net/connhandlers.lua plugins/mod_legacyauth.lua plugins/mod_saslauth.lua util/sasl.lua
diffstat 9 files changed, 194 insertions(+), 45 deletions(-) [+]
line wrap: on
line diff
--- 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