changeset 474:942738953ff3

mod_auth_dovecot: Replace with SASL proxying version.
author Kim Alvefur <zash@zash.se>
date Thu, 10 Nov 2011 11:24:31 +0100
parents 99b246b37809
children db5702bb9e41
files mod_auth_dovecot/auth_dovecot/mod_auth_dovecot.lua mod_auth_dovecot/auth_dovecot/sasl_dovecot.lib.lua mod_auth_dovecot/mod_auth_dovecot.lua
diffstat 3 files changed, 365 insertions(+), 247 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_auth_dovecot/auth_dovecot/mod_auth_dovecot.lua	Thu Nov 10 11:24:31 2011 +0100
@@ -0,0 +1,98 @@
+-- Dovecot authentication backend for Prosody
+--
+-- Copyright (C) 2010-2011 Waqas Hussain
+-- Copyright (C) 2011 Kim Alvefur
+--
+
+local name = "Dovecot SASL";
+local log = require "util.logger".init("auth_dovecot");
+
+local socket_path = module:get_option_string("dovecot_auth_socket", "/var/run/dovecot/auth-login");
+local socket_host = module:get_option_string("dovecot_auth_host", "127.0.0.1");
+local socket_port = module:get_option_string("dovecot_auth_port");
+
+local service_realm = module:get_option("realm");
+local service_name = module:get_option("service_name");
+local append_host = module:get_option_boolean("auth_append_host");
+local validate_domain = module:get_option_boolean("validate_append_host");
+local handle_appended = module:get_option_string("handle_appended");
+local util_sasl_new = require "util.sasl".new;
+
+local new_dovecot_sasl = module:require "sasl_dovecot".new;
+
+local new_sasl = function(realm)
+	return new_dovecot_sasl(
+		service_realm or realm,
+		service_name or "xmpp",
+
+		socket_port and { socket_path, socket_port } or socket_path,
+
+		{ --config
+			handle_domain = handle_appended or
+				(append_host and "split" or "escape"),
+			validate_domain = validate_domain,
+		}
+	);
+end
+
+do
+	local s = new_sasl(module.host)
+	assert(s, "Could not create a new SASL object");
+	assert(s.mechanisms, "SASL object has no mechanims method");
+	local m, _m = {}, s:mechanisms();
+	assert(not append_host or _m.PLAIN, "auth_append_host requires PLAIN, but it is unavailable");
+	for k in pairs(_m) do
+		table.insert(m, k);
+	end
+	log("debug", "Mechanims found: %s", table.concat(m, ", "));
+end
+
+provider = {
+	name = module.name:gsub("^auth_","");
+};
+
+function provider.test_password(username, password)
+	return new_sasl(module.host):plain_test(username, password);
+end
+
+if append_host then
+	new_sasl = function(realm)
+		return util_sasl_new(realm, {
+			plain_test = function(sasl, username, password, realm)
+				local prepped_username = nodeprep(username);
+				if not prepped_username then
+					log("debug", "NODEprep failed on username: %s", username);
+					return "", nil;
+				end
+				prepped_username = prepped_username .. "@" .. module.host;
+				return provider.test_password(prepped_username, password), true;
+			end,
+		});
+	end
+end
+
+function provider.get_password(username)
+	return nil, "Passwords unavailable for "..name;
+end
+
+function provider.set_password(username, password)
+	return nil, "Passwords unavailable for "..name;
+end
+
+function provider.user_exists(username)
+	local user_test = new_sasl(module.host);
+	user_test:select("PLAIN");
+	user_test:process(("\0%s\0"):format(username));
+	return user_test.username == username;
+end
+
+function provider.create_user(username, password)
+	return nil, "Account creation/modification not available with "..name;
+end
+
+function provider.get_sasl_handler()
+	return new_sasl(module.host);
+end
+
+module:add_item("auth-provider", provider);
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_auth_dovecot/auth_dovecot/sasl_dovecot.lib.lua	Thu Nov 10 11:24:31 2011 +0100
@@ -0,0 +1,267 @@
+-- Dovecot authentication backend for Prosody
+--
+-- Copyright (C) 2008-2009 Tobias Markmann
+-- Copyright (C) 2010 Javier Torres
+-- Copyright (C) 2010-2011 Matthew Wild
+-- Copyright (C) 2010-2011 Waqas Hussain
+-- Copyright (C) 2011 Kim Alvefur
+--
+--    Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+--
+--        * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+--        * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+--        * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+--
+--    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-- This code is based on util.sasl_cyrus and the old mod_auth_dovecot
+
+local log = require "util.logger".init("sasl_dovecot");
+
+local setmetatable = setmetatable;
+
+local s_match, s_gmatch = string.match, string.gmatch
+local t_concat = table.concat;
+local m_random = math.random;
+local tostring, tonumber = tostring, tonumber;
+
+local socket = require "socket"
+pcall(require, "socket.unix");
+local base64 = require "util.encodings".base64;
+local b64, unb64 = base64.encode, base64.decode;
+local jid_escape = require "util.jid".escape;
+local prepped_split = require "util.jid".prepped_split;
+local nodeprep = require "util.encodings".stringprep.nodeprep;
+
+--module "sasl_dovecot"
+local _M = {};
+
+local request_id = 0;
+local method = {};
+method.__index = method;
+local conn, supported_mechs, pid;
+
+local function connect(socket_info)
+	--log("debug", "connect(%q)", socket_path);
+	if conn then conn:close(); pid = nil; end
+	if not pid then pid = tonumber(tostring(conn):match("0x%x*$")) end
+
+	local socket_type = (type(socket_info) == "string") and "UNIX" or "TCP";
+
+	local ok, err;
+	if socket_type == "TCP" then
+		local socket_host, socket_port = unpack(socket_info);
+		conn = socket.tcp();
+		ok, err = conn:connect(socket_host, socket_port);
+		socket_path = ("%s:%d"):format(socket_host, socket_port);
+	elseif socket.unix then
+		conn = socket.unix();
+		ok, err = conn:connect(socket_path);
+	else
+		err = "luasocket was not compiled with UNIX sockets support";
+	end
+
+	if not ok then
+		log("error", "error connecting to dovecot %s socket at '%s'. error was '%s'", socket_type, socket_path, err);
+		return false;
+	end
+
+	-- Send our handshake
+	log("debug", "sending handshake to dovecot. version 1.1, cpid '%d'", pid);
+	if not conn:send("VERSION\t1\t1\n") then
+		return false
+	end
+	if not conn:send("CPID\t" .. pid .. "\n") then
+		return false
+	end
+
+	-- Parse Dovecot's handshake
+	local done = false;
+	supported_mechs = {};
+	while (not done) do
+		local line = conn:receive();
+		if not line then
+			return false;
+		end
+
+		--log("debug", "dovecot handshake: '%s'", line);
+		local parts = line:gmatch("[^\t]+");
+		local first = parts();
+		if first == "VERSION" then
+			-- Version should be 1.1
+			local major_version = parts();
+
+			if major_version ~= "1" then
+				log("error", "dovecot server version is not 1.x. it is %s.x", major_version);
+				conn:close();
+				return false;
+			end
+		elseif first == "MECH" then
+			local mech = parts();
+			supported_mechs[mech] = true;
+		elseif first == "DONE" then
+			done = true;
+		end
+	end
+	return conn, supported_mechs;
+end
+
+-- create a new SASL object which can be used to authenticate clients
+function _M.new(realm, service_name, socket_info, config)
+	--log("debug", "new(%q, %q, %q)", realm or "", service_name or "", socket_info or "");
+	local sasl_i = { realm = realm, service_name = service_name, socket_info = socket_info, config = config or {} };
+
+	request_id = request_id + 1;
+	sasl_i.request_id = request_id;
+	local conn, mechs = conn, supported_mechs;
+	if not conn then
+		conn, mechs = connect(socket_info);
+		if not conn then
+			return nil, "Socket connection failure";
+		end
+	end
+	sasl_i.conn, sasl_i.mechs = conn, mechs;
+	return setmetatable(sasl_i, method);
+end
+
+-- [[
+function method:send(...)
+	local msg = t_concat({...}, "\t");
+	local ok, err = self.conn:send(authmsg.."\n");
+	if not ok then
+		log("error", "Could not write to socket: %s", err);
+		return nil, err;
+	end
+	return true;
+end
+
+function method:recv()
+	local line, err = self.conn:receive();
+	--log("debug", "Sent %d bytes to socket", ok);
+	local line, err = self.conn:receive();
+	if not line then
+		log("error", "Could not read from socket: %s", err);
+		return nil, err;
+	end
+	return line;
+end
+-- ]]
+
+function method:plain_test(username, password, realm)
+	if self:select("PLAIN") then
+		return self:process(("\0%s\0%s"):format(username, password));
+	end
+end
+
+-- get a fresh clone with the same realm and service name
+function method:clean_clone()
+	--log("debug", "method:clean_clone()");
+	return _M.new(self.realm, self.service_name, self.socket_info, self.config)
+end
+
+-- get a list of possible SASL mechanims to use
+function method:mechanisms()
+	--log("debug", "method:mechanisms()");
+	return self.mechs;
+end
+
+-- select a mechanism to use
+function method:select(mechanism)
+	--log("debug", "method:select(%q)", mechanism);
+	if not self.selected and self.mechs[mechanism] then
+		self.selected = mechanism;
+		return true;
+	end
+end
+
+-- feed new messages to process into the library
+function method:process(message)
+	--log("debug", "method:process"..(message and "(%q)" or "()"), message);
+	--if not message then
+		--return "challenge";
+		--return "failure", "malformed-request";
+	--end
+	local request_id = self.request_id;
+	local authmsg;
+	if not self.started then
+		self.started = true;
+		authmsg = t_concat({
+			"AUTH",
+			request_id,
+			self.selected,
+			"service="..self.service_name,
+			"resp="..(message and b64(message) or "=")
+		}, "\t");
+	else
+		authmsg = t_concat({
+			"CONT",
+			request_id,
+			(message and b64(message) or "=")
+		}, "\t");
+	end
+	--log("debug", "Sending %d bytes: %q", #authmsg, authmsg);
+	local ok, err = self.conn:send(authmsg.."\n");
+	if not ok then
+		log("error", "Could not write to socket: %s", err);
+		return "failure", "internal-server-error", err
+	end
+	--log("debug", "Sent %d bytes to socket", ok);
+	local line, err = self.conn:receive();
+	if not line then
+		log("error", "Could not read from socket: %s", err);
+		return "failure", "internal-server-error", err
+	end
+	--log("debug", "Received %d bytes from socket: %s", #line, line);
+
+	local parts = line:gmatch("[^\t]+");
+	local resp = parts();
+	local id = tonumber(parts());
+
+	if id ~= request_id then
+		return "failure", "internal-server-error", "Unexpected request id"
+	end
+
+	local data = {};
+	for param in parts do
+		data[#data+1]=param;
+		local k,v = param:match("^([^=]*)=?(.*)$");
+		if k and #k>0 then
+			data[k]=v or true;
+		end
+	end
+
+	if data.user then
+		local handle_domain = self.config.handle_domain;
+		local validate_domain = self.config.validate_domain;
+		if handle_domain == "split" then
+			local domain;
+			self.username, domain = prepped_split(data.user);
+			if validate_domain and domain ~= self.realm then
+				return "failure", "not-authorized", "Domain mismatch";
+			end
+		elseif handle_domain == "escape" then
+			self.username = nodeprep(jid_escape(data.user));
+		else
+			self.username = nodeprep(data.user);
+		end
+		if not self.username then 
+			return "failure", "not-authorized", "Username failed NODEprep"
+		end
+	end
+
+	if resp == "FAIL" then
+		if data.temp then
+			return "failure", "temporary-auth-failure", data.reason;
+		elseif data.authz then
+			return "failure", "invalid-authzid", data.reason;
+		else
+			return "failure", "not-authorized", data.reason;
+		end
+	elseif resp == "CONT" then
+		return "challenge", unb64(data[1]);
+	elseif resp == "OK" then
+		return "success", data.resp and unb64(data.resp) or nil; 
+	end
+end
+
+return _M;
--- a/mod_auth_dovecot/mod_auth_dovecot.lua	Wed Nov 02 00:25:28 2011 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,247 +0,0 @@
--- Dovecot authentication backend for Prosody
---
--- Copyright (C) 2010 Javier Torres
--- Copyright (C) 2010-2011 Matthew Wild
--- Copyright (C) 2010-2011 Waqas Hussain
--- Copyright (C) 2011 Kim Alvefur
---
-
-pcall(require, "socket.unix");
-local datamanager = require "util.datamanager";
-local usermanager = require "core.usermanager";
-local log = require "util.logger".init("auth_dovecot");
-local new_sasl = require "util.sasl".new;
-local nodeprep = require "util.encodings".stringprep.nodeprep;
-local base64 = require "util.encodings".base64;
-local sha1 = require "util.hashes".sha1;
-
-local prosody = prosody;
-local socket_path = module:get_option_string("dovecot_auth_socket", "/var/run/dovecot/auth-login");
-local socket_host = module:get_option_string("dovecot_auth_host", "127.0.0.1");
-local socket_port = module:get_option_string("dovecot_auth_port");
-local append_host = module:get_option_boolean("auth_append_host", false);
-if not socket_port and not socket.unix then
-	error("LuaSocket was not compiled with UNIX socket support. Try using Dovecot 2.x with inet_listener support, or recompile LuaSocket with UNIX socket support.");
-end
-
-function new_provider(host)
-	local provider = { name = "dovecot", request_id = 0 };
-	log("debug", "initializing dovecot authentication provider for host '%s'", host);
-	
-	local conn;
-	-- Generate an id for this connection (must be a 31-bit number, unique per process)
-	local pid = tonumber(sha1(host, true):sub(1, 6), 16);
-	
-	-- Closes the socket
-	function provider.close(self)
-		if conn then
-			conn:close();
-			conn = nil;
-		end
-	end
-	
-	-- The following connects to a new socket and send the handshake
-	function provider.connect(self)
-		-- Destroy old socket
-		provider:close();
-		
-		local ok, err;
-		if socket_port then
-			log("debug", "connecting to dovecot TCP socket at '%s':'%s'", socket_host, socket_port);
-			conn = socket.tcp();
-			ok, err = conn:connect(socket_host, socket_port);
-		elseif socket.unix then
-			log("debug", "connecting to dovecot UNIX socket at '%s'", socket_path);
-			conn = socket.unix();
-			ok, err = conn:connect(socket_path);
-		else
-			err = "luasocket was not compiled with UNIX sockets support";
-		end
-		if not ok then
-			if socket_port then
-				log("error", "error connecting to dovecot TCP socket at '%s':'%s'. error was '%s'. check permissions", socket_host, socket_port, err);
-			else
-				log("error", "error connecting to dovecot UNIX socket at '%s'. error was '%s'. check permissions", socket_path, err);
-			end
-			provider:close();
-			return false;
-		end
-		
-		-- Send our handshake
-		log("debug", "sending handshake to dovecot. version 1.1, cpid '%d'", pid);
-		if not provider:send("VERSION\t1\t1\n") then
-			return false
-		end
-		if not provider:send("CPID\t" .. pid .. "\n") then
-			return false
-		end
-		
-		-- Parse Dovecot's handshake
-		local done = false;
-		local supported_mechs = {};
-		while (not done) do
-			local line = provider:receive();
-			if not line then
-				return false;
-			end
-			
-			log("debug", "dovecot handshake: '%s'", line);
-			local parts = line:gmatch("[^\t]+");
-			local first = parts();
-			if first == "VERSION" then
-				-- Version should be 1.1
-				local major_version = parts();
-				
-				if major_version ~= "1" then
-					log("error", "dovecot server version is not 1.x. it is %s.x", major_version);
-					provider:close();
-					return false;
-				end
-			elseif first == "MECH" then
-				local mech = parts();
-				supported_mechs[mech] = true;
-			elseif first == "DONE" then
-				-- We need PLAIN
-				if not supported_mechs.PLAIN then
-					log("warn", "server doesn't support PLAIN mechanism.");
-					provider:close();
-					return false;
-				end
-				done = true;
-			end
-		end
-		return true;
-	end
-	
-	-- Wrapper for send(). Handles errors
-	function provider.send(self, data)
-		local ok, err = conn:send(data);
-		if not ok then
-			log("error", "error sending '%s' to dovecot. error was '%s'", data, err);
-			provider:close();
-			return false;
-		end
-		return true;
-	end
-	
-	-- Wrapper for receive(). Handles errors
-	function provider.receive(self)
-		local line, err = conn:receive();
-		if not line then
-			log("error", "error receiving data from dovecot. error was '%s'", err);
-			provider:close();
-			return false;
-		end
-		return line;
-	end
-	
-	function provider.send_auth_request(self, username, password)
-		if not conn then
-			if not provider:connect() then
-				return nil, "Auth failed. Dovecot communications error";
-			end
-		end
-		
-		-- Send auth data
-		if append_host then
-			username = username .. "@" .. module.host;
-		end
-		local b64 = base64.encode(username .. "\0" .. username .. "\0" .. password);
-		provider.request_id = provider.request_id + 1 % 4294967296
-		
-		local msg = "AUTH\t" .. provider.request_id .. "\tPLAIN\tservice=XMPP\tresp=" .. b64;
-		log("debug", "sending auth request for '%s' with password '%s': '%s'", username, password, msg);
-		if not provider:send(msg .. "\n") then
-			return nil, "Auth failed. Dovecot communications error";
-		end
-		
-		
-		-- Get response
-		local line = provider:receive();
-		log("debug", "got auth response: '%s'", line);
-		if not line then
-			return nil, "Auth failed. Dovecot communications error";
-		end
-		local parts = line:gmatch("[^\t]+");
-		
-		-- Check response
-		local status = parts();
-		local resp_id = tonumber(parts());
-		
-		if resp_id  ~= provider.request_id then
-			log("warn", "dovecot response_id(%s) doesn't match request_id(%s)", resp_id, provider.request_id);
-			provider:close();
-			return nil, "Auth failed. Dovecot communications error";
-		end
-		
-		return status, parts;
-	end
-	
-	function provider.test_password(username, password)
-		log("debug", "test password '%s' for user %s at host %s", password, username, module.host);
-		
-		local status, extra = provider:send_auth_request(username, password);
-		
-		if status == "OK" then
-			log("info", "login ok for '%s'", username);
-			return true;
-		else
-			log("info", "login failed for '%s'", username);
-			return nil, "Auth failed. Invalid username or password.";
-		end
-	end
-
-	function provider.get_password(username)
-		return nil, "Cannot get_password in dovecot backend.";
-	end
-	
-	function provider.set_password(username, password)
-		return nil, "Cannot set_password in dovecot backend.";
-	end
-
-	function provider.user_exists(username)
-		log("debug", "user_exists for user %s at host %s", username, module.host);
-		
-		-- Send a request. If the response (FAIL) contains an extra
-		-- parameter like user=<username> then it exists.
-		local status, extra = provider:send_auth_request(username, "");
-		
-		local param = extra();
-		while param do
-			local parts = param:gmatch("[^=]+");
-			local name = parts();
-			local value = parts();
-			if name == "user" then
-				log("debug", "user '%s' exists", username);
-				return true;
-			end
-			
-			param = extra();
-		end
-		
-		log("debug", "user '%s' does not exists (or dovecot didn't send user=<username> parameter)", username);
-		return false;
-	end
-
-	function provider.create_user(username, password)
-		return nil, "Cannot create_user in dovecot backend.";
-	end
-
-	function provider.get_sasl_handler()
-		local getpass_authentication_profile = {
-			plain_test = function(sasl, username, password, realm)
-			local prepped_username = nodeprep(username);
-			if not prepped_username then
-				log("debug", "NODEprep failed on username: %s", username);
-				return "", nil;
-			end
-			return usermanager.test_password(prepped_username, realm, password), true;
-		end
-		};
-		return new_sasl(module.host, getpass_authentication_profile);
-	end
-	
-	return provider;
-end
-
-module:add_item("auth-provider", new_provider(module.host));