mod_saslauth_muc/mod_saslauth_muc.lua
changeset 284 3b96bba9f7e5
child 287 6144fe6161f1
equal deleted inserted replaced
283:10c3f6c6a04c 284:3b96bba9f7e5
       
     1 --
       
     2 -- mod_saslauth_muc
       
     3 --   This module implements http://xmpp.org/extensions/inbox/remote-auth.html for Prosody's MUC component
       
     4 --
       
     5 -- In your config:
       
     6 --   Component "conference.example.com" "muc"
       
     7 --       modules_enabled = { "saslauth_muc" };
       
     8 --
       
     9 --
       
    10 
       
    11 local timeout = 60; -- SASL timeout in seconds
       
    12 
       
    13 -- various imports
       
    14 local new_sasl = require "util.sasl".new;
       
    15 local st = require "util.stanza";
       
    16 local timer = require "util.timer";
       
    17 
       
    18 local jid_bare = require "util.jid".bare;
       
    19 local jid_prep = require "util.jid".prep;
       
    20 local base64 = require "util.encodings".base64;
       
    21 
       
    22 local hosts = hosts;
       
    23 local module = module;
       
    24 local pairs, next = pairs, next;
       
    25 local os_time = os.time;
       
    26 
       
    27 -- SASL sessions management
       
    28 local _rooms = {}; -- SASL data
       
    29 local function get_handler_for(room, jid) return _rooms[room] and _rooms[room][jid]; end
       
    30 local function remove_handler_for(room, jid) if _rooms[room] then _rooms[room][jid] = nil; end end
       
    31 local function create_handler_for(room_jid, jid)
       
    32 	_rooms[room_jid] = _rooms[room_jid] or {};
       
    33 	_rooms[room_jid][jid] = new_sasl(module.host, { plain = function(username, realm)
       
    34 		local muc = hosts[module.host].modules.muc;
       
    35 		local room = muc and muc.rooms[room_jid];
       
    36 		local password = room and room:get_password();
       
    37 		local ret = password and true or false;
       
    38 		return password, true;
       
    39 	end });
       
    40 	_rooms[room_jid][jid].timeout = os_time() + timeout;
       
    41 	return _rooms[room_jid][jid];
       
    42 end
       
    43 
       
    44 -- Timer to clear SASL sessions
       
    45 timer.add_task(timeout, function()
       
    46 	local now = os_time();
       
    47 	for room, handlers in pairs(_rooms) do
       
    48 		for jid, handler in pairs(handlers) do
       
    49 			if handler.timeout <= now then handlers[jid] = nil; end
       
    50 		end
       
    51 		if next(handlers) == nil then _rooms[room] = nil; end
       
    52 	end
       
    53 	return timeout;
       
    54 end);
       
    55 
       
    56 -- Stanza handlers
       
    57 module:hook("presence/full", function(event)
       
    58 	local origin, stanza = event.origin, event.stanza;
       
    59 	
       
    60 	if not stanza.attr.type then -- available presence
       
    61 		local room_jid = jid_bare(stanza.attr.to);
       
    62 		local room = hosts[module.host].modules.muc.rooms[room_jid];
       
    63 
       
    64 		if room and not room:get_role(stanza.attr.from) then -- this is a room join
       
    65 			if room:get_password() then -- room has a password
       
    66 				local x = stanza:get_child("x", "http://jabber.org/protocol/muc");
       
    67 				local password = x and x:get_child("password");
       
    68 				if not password then -- no password sent
       
    69 					local sasl_handler = get_handler_for(jid_bare(stanza.attr.to), stanza.attr.from);
       
    70 					if x and sasl_handler and sasl_handler.authorized then -- if already passed SASL
       
    71 						x:reset():tag("password", { xmlns = "http://jabber.org/protocol/muc" }):text(room:get_password());
       
    72 					else
       
    73 						origin.send(st.error_reply(stanza, "auth", "not-authorized")
       
    74 							:tag("sasl-required", { xmlns = "urn:xmpp:errors" }));
       
    75 						return true;
       
    76 					end
       
    77 				end
       
    78 			end
       
    79 		end
       
    80 	end
       
    81 end, 10);
       
    82 
       
    83 module:hook("iq-get/bare/urn:ietf:params:xml:ns:xmpp-sasl:mechanisms", function(event)
       
    84 	local origin, stanza = event.origin, event.stanza;
       
    85 
       
    86 	local reply = st.reply(stanza):tag("mechanisms", { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' });
       
    87 	for mechanism in pairs(create_handler_for(stanza.attr.to, true):mechanisms()) do
       
    88 		reply:tag("mechanism"):text(mechanism):up();
       
    89 	end
       
    90 	origin.send(reply:up());
       
    91 	return true;
       
    92 end);
       
    93 
       
    94 local function build_reply(stanza, status, ret, err_msg)
       
    95 	local reply = st.stanza(status, {xmlns = "urn:ietf:params:xml:ns:xmpp-sasl"});
       
    96 	if status == "challenge" then
       
    97 		reply:text(base64.encode(ret or ""));
       
    98 	elseif status == "failure" then
       
    99 		reply:tag(ret):up();
       
   100 		if err_msg then reply:tag("text"):text(err_msg); end
       
   101 	elseif status == "success" then
       
   102 		reply:text(base64.encode(ret or ""));
       
   103 	else
       
   104 		module:log("error", "Unknown sasl status: %s", status);
       
   105 	end
       
   106 	return st.reply(stanza):add_child(reply);
       
   107 end
       
   108 local function handle_status(stanza, status)
       
   109 	if status == "failure" then
       
   110 		remove_handler_for(stanza.attr.to, stanza.attr.from);
       
   111 	elseif status == "success" then
       
   112 		get_handler_for(stanza.attr.to, stanza.attr.from).authorized = true;
       
   113 	end
       
   114 end
       
   115 local function sasl_process_cdata(session, stanza)
       
   116 	local text = stanza.tags[1][1];
       
   117 	if text then
       
   118 		text = base64.decode(text);
       
   119 		if not text then
       
   120 			remove_handler_for(stanza.attr.to, stanza.attr.from);
       
   121 			session.send(build_reply(stanza, "failure", "incorrect-encoding"));
       
   122 			return true;
       
   123 		end
       
   124 	end
       
   125 	local status, ret, err_msg = get_handler_for(stanza.attr.to, stanza.attr.from):process(text);
       
   126 	handle_status(stanza, status);
       
   127 	local s = build_reply(stanza, status, ret, err_msg);
       
   128 	session.send(s);
       
   129 	return true;
       
   130 end
       
   131 
       
   132 module:hook("iq-set/bare/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
       
   133 	local session, stanza = event.origin, event.stanza;
       
   134 
       
   135 	if not create_handler_for(stanza.attr.to, stanza.attr.from):select(stanza.tags[1].attr.mechanism) then
       
   136 		remove_handler_for(stanza.attr.to, stanza.attr.from);
       
   137 		session.send(build_reply(stanza, "failure", "invalid-mechanism"));
       
   138 		return true;
       
   139 	end
       
   140 	return sasl_process_cdata(session, stanza);
       
   141 end);
       
   142 module:hook("iq-set/bare/urn:ietf:params:xml:ns:xmpp-sasl:response", function(event)
       
   143 	local session, stanza = event.origin, event.stanza;
       
   144 	if not get_handler_for(stanza.attr.to, stanza.attr.from) then
       
   145 		session.send(build_reply(stanza, "failure", "not-authorized", "Out of order SASL element"));
       
   146 		return true;
       
   147 	end
       
   148 	return sasl_process_cdata(session, event.stanza);
       
   149 end);
       
   150 module:hook("iq-set/bare/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event)
       
   151 	local session, stanza = event.origin, event.stanza;
       
   152 	remove_handler_for(stanza.attr.to, stanza.attr.from);
       
   153 	session.send(build_reply(stanza, "failure", "aborted"));
       
   154 	return true;
       
   155 end);