Changeset

30:bcf539295f2d

Huge commit to: * Break stanza routing (to be restored in a future commit) * Remove the old stanza_dispatcher code, which was never going to be maintainable nor extendable :) * Bring us plugins, starting with mod_legacyauth and mod_roster * Sessions are now created/destroyed using a standard sessionmanager interface
author Matthew Wild <mwild1@gmail.com>
date Tue, 30 Sep 2008 19:52:00 +0100
parents 29:b847875801e5
children 31:aaccbf07849b
files core/modulemanager.lua core/servermanager.lua core/sessionmanager.lua core/stanza_router.lua main.lua plugins/mod_legacyauth.lua plugins/mod_roster.lua util/logger.lua util/stanza.lua
diffstat 9 files changed, 499 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/modulemanager.lua	Tue Sep 30 19:52:00 2008 +0100
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/servermanager.lua	Tue Sep 30 19:52:00 2008 +0100
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/sessionmanager.lua	Tue Sep 30 19:52:00 2008 +0100
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/stanza_router.lua	Tue Sep 30 19:52:00 2008 +0100
@@ -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
--- a/main.lua	Tue Sep 30 19:49:36 2008 +0100
+++ b/main.lua	Tue Sep 30 19:52:00 2008 +0100
@@ -13,7 +13,10 @@
 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 @@
 	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 @@
 		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 @@
 
 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;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_legacyauth.lua	Tue Sep 30 19:52:00 2008 +0100
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_roster.lua	Tue Sep 30 19:52:00 2008 +0100
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/logger.lua	Tue Sep 30 19:52:00 2008 +0100
@@ -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
--- a/util/stanza.lua	Tue Sep 30 19:49:36 2008 +0100
+++ b/util/stanza.lua	Tue Sep 30 19:52:00 2008 +0100
@@ -136,3 +136,4 @@
 	return stanza("presence", attr);
 end
 
+return _M;
\ No newline at end of file